이벤트 바인딩 기법
언리얼 엔진의 이벤트 시스템은 게임 로직을 효과적으로 구현하고 관리할 수 있게 해줍니다.
이 절에서는 다양한 이벤트 유형과 C++에서의 이벤트 바인딩 기법을 살펴보겠습니다.
언리얼 엔진의 이벤트 유형
- 액터 이벤트 : 액터의 생명주기와 관련된 이벤트
- 컴포넌트 이벤트 : 컴포넌트의 상태 변화와 관련된 이벤트
- 입력 이벤트 : 사용자 입력과 관련된 이벤트
- 콜리전 이벤트 : 물체 간 충돌과 관련된 이벤트
- 타이머 이벤트 : 시간 기반 이벤트
C++에서의 이벤트 선언 및 바인딩
액터 이벤트 예시
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
UFUNCTION()
void OnActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
};
AMyActor::AMyActor()
{
OnActorBeginOverlap.AddDynamic(this, &AMyActor::OnActorBeginOverlap);
}
void AMyActor::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Log, TEXT("Actor BeginPlay"));
}
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
UE_LOG(LogTemp, Log, TEXT("Actor EndPlay"));
}
void AMyActor::OnActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
UE_LOG(LogTemp, Log, TEXT("Actor Begin Overlap with %s"), *OtherActor->GetName());
}
컴포넌트 이벤트 예시
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UMyComponent : public UActorComponent
{
GENERATED_BODY()
public:
UMyComponent();
protected:
virtual void BeginPlay() override;
UFUNCTION()
void OnComponentActivated(UActorComponent* Component, bool bReset);
};
UMyComponent::UMyComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UMyComponent::BeginPlay()
{
Super::BeginPlay();
OnComponentActivated.AddDynamic(this, &UMyComponent::OnComponentActivated);
}
void UMyComponent::OnComponentActivated(UActorComponent* Component, bool bReset)
{
UE_LOG(LogTemp, Log, TEXT("Component Activated: %s"), *Component->GetName());
}
입력 이벤트 예시
UCLASS()
class MYGAME_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMyPlayerController();
protected:
virtual void SetupInputComponent() override;
void MoveForward(float Value);
void MoveRight(float Value);
};
AMyPlayerController::AMyPlayerController()
{
}
void AMyPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
InputComponent->BindAxis("MoveForward", this, &AMyPlayerController::MoveForward);
InputComponent->BindAxis("MoveRight", this, &AMyPlayerController::MoveRight);
}
void AMyPlayerController::MoveForward(float Value)
{
if (Value != 0.0f && GetPawn())
{
GetPawn()->AddMovementInput(GetPawn()->GetActorForwardVector(), Value);
}
}
void AMyPlayerController::MoveRight(float Value)
{
if (Value != 0.0f && GetPawn())
{
GetPawn()->AddMovementInput(GetPawn()->GetActorRightVector(), Value);
}
}
동적 바인딩 vs 정적 바인딩
- 동적 바인딩 : 런타임에 바인딩되며, UFUNCTION() 매크로가 필요합니다.
UFUNCTION()
void OnSomeEvent();
SomeActor->OnEventHappened.AddDynamic(this, &AMyActor::OnSomeEvent);
- 정적 바인딩 : 컴파일 시간에 바인딩되며, 더 빠르지만 리플렉션을 지원하지 않습니다.
void OnSomeEvent();
SomeActor->OnEventHappened.AddUObject(this, &AMyActor::OnSomeEvent);
람다 함수를 이용한 이벤트 바인딩
SomeActor->OnEventHappened.AddLambda([this]()
{
UE_LOG(LogTemp, Log, TEXT("Event happened!"));
// 추가 로직
});
이벤트 바인딩 해제
// 특정 함수 바인딩 해제
SomeActor->OnEventHappened.RemoveDynamic(this, &AMyActor::OnSomeEvent);
// 모든 바인딩 해제
SomeActor->OnEventHappened.Clear();
이벤트 기반 프로그래밍의 장점과 주의점
장점
- 느슨한 결합 : 시스템 간 의존성 감소
- 모듈성 : 기능 추가 및 제거가 용이
- 확장성 : 새로운 기능을 쉽게 추가 가능
주의점
- 복잡성 증가 : 과도한 사용 시 코드 흐름 파악이 어려울 수 있음
- 성능 오버헤드 : 많은 이벤트 발생 시 성능 저하 가능성
- 디버깅 어려움 : 이벤트 체인 추적이 복잡할 수 있음
성능 최적화를 위한 이벤트 사용 전략
- 이벤트 발생 빈도 최적화 : 불필요한 이벤트 발생 줄이기
- 이벤트 핸들러 최적화 : 핸들러 내 로직을 가볍게 유지
- 정적 바인딩 활용 : 성능이 중요한 경우 정적 바인딩 사용
- 이벤트 풀링 : 자주 사용되는 이벤트 객체 재사용
예시
// 이벤트 풀링
TArray<FMyEvent*> EventPool;
FMyEvent* GetEventFromPool()
{
if (EventPool.Num() > 0)
{
return EventPool.Pop();
}
return new FMyEvent();
}
void ReturnEventToPool(FMyEvent* Event)
{
Event->Reset();
EventPool.Push(Event);
}
멀티스레드 환경에서의 이벤트 처리
- 스레드 안전성 확보 : 동기화 메커니즘 사용
- 락 프리 알고리즘 활용 : 성능 향상을 위해 가능한 경우 사용
- 이벤트 큐 사용 : 다중 스레드에서 안전하게 이벤트 처리
예시
// 스레드 안전 이벤트 큐
class FThreadSafeEventQueue
{
private:
TQueue<FEvent*> EventQueue;
FCriticalSection QueueLock;
public:
void EnqueueEvent(FEvent* Event)
{
FScopeLock Lock(&QueueLock);
EventQueue.Enqueue(Event);
}
bool DequeueEvent(FEvent*& OutEvent)
{
FScopeLock Lock(&QueueLock);
return EventQueue.Dequeue(OutEvent);
}
};
Best Practices
- 명확한 이벤트 명명 규칙 사용
DECLARE_EVENT(FMyClass, FOnSomethingHappenedEvent)
- 이벤트 문서화
/** 플레이어가 데미지를 받을 때 발생하는 이벤트 */
DECLARE_EVENT_TwoParams(AMyCharacter, FOnTakeDamageEvent, float, AActor*)
- 이벤트 핸들러의 범위 제한
SomeActor->OnEventHappened.AddWeakLambda(this, [this]()
{
if (IsValid(this))
{
// 이벤트 처리 로직
}
});
- 컴포넌트 기반 이벤트 시스템 구축
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UEventManagerComponent : public UActorComponent
{
GENERATED_BODY()
public:
DECLARE_EVENT(UEventManagerComponent, FOnLevelCompleteEvent)
FOnLevelCompleteEvent OnLevelComplete;
void TriggerLevelComplete()
{
OnLevelComplete.Broadcast();
}
};
- 이벤트 우선순위 설정
SomeActor->OnEventHappened.AddUObject(this, &AMyActor::HighPriorityHandler, FEventPriority::High);
SomeActor->OnEventHappened.AddUObject(this, &AMyActor::NormalPriorityHandler, FEventPriority::Normal);
이벤트 바인딩 기법은 언리얼 엔진에서 유연하고 확장 가능한 게임 시스템을 구축하는 데 핵심적인 역할을 합니다.
다양한 이벤트 유형을 적절히 활용하고, 동적/정적 바인딩을 상황에 맞게 선택하며, 람다 함수를 통한 간결한 이벤트 처리 등을 통해 효율적인 코드를 작성할 수 있습니다.