icon안동민 개발노트

이벤트 바인딩 기법


 언리얼 엔진의 이벤트 시스템은 게임 로직을 효과적으로 구현하고 관리할 수 있게 해줍니다. 이 절에서는 다양한 이벤트 유형과 C++에서의 이벤트 바인딩 기법을 살펴보겠습니다.

언리얼 엔진의 이벤트 유형

  1. 액터 이벤트 : 액터의 생명주기와 관련된 이벤트
  2. 컴포넌트 이벤트 : 컴포넌트의 상태 변화와 관련된 이벤트
  3. 입력 이벤트 : 사용자 입력과 관련된 이벤트
  4. 콜리전 이벤트 : 물체 간 충돌과 관련된 이벤트
  5. 타이머 이벤트 : 시간 기반 이벤트

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 정적 바인딩

  1. 동적 바인딩 : 런타임에 바인딩되며, UFUNCTION() 매크로가 필요합니다.
UFUNCTION()
void OnSomeEvent();
 
SomeActor->OnEventHappened.AddDynamic(this, &AMyActor::OnSomeEvent);
  1. 정적 바인딩 : 컴파일 시간에 바인딩되며, 더 빠르지만 리플렉션을 지원하지 않습니다.
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();

이벤트 기반 프로그래밍의 장점과 주의점

 장점

  1. 느슨한 결합 : 시스템 간 의존성 감소
  2. 모듈성 : 기능 추가 및 제거가 용이
  3. 확장성 : 새로운 기능을 쉽게 추가 가능

 주의점

  1. 복잡성 증가 : 과도한 사용 시 코드 흐름 파악이 어려울 수 있음
  2. 성능 오버헤드 : 많은 이벤트 발생 시 성능 저하 가능성
  3. 디버깅 어려움 : 이벤트 체인 추적이 복잡할 수 있음

성능 최적화를 위한 이벤트 사용 전략

  1. 이벤트 발생 빈도 최적화 : 불필요한 이벤트 발생 줄이기
  2. 이벤트 핸들러 최적화 : 핸들러 내 로직을 가볍게 유지
  3. 정적 바인딩 활용 : 성능이 중요한 경우 정적 바인딩 사용
  4. 이벤트 풀링 : 자주 사용되는 이벤트 객체 재사용

 예시

// 이벤트 풀링
TArray<FMyEvent*> EventPool;
 
FMyEvent* GetEventFromPool()
{
    if (EventPool.Num() > 0)
    {
        return EventPool.Pop();
    }
    return new FMyEvent();
}
 
void ReturnEventToPool(FMyEvent* Event)
{
    Event->Reset();
    EventPool.Push(Event);
}

멀티스레드 환경에서의 이벤트 처리

  1. 스레드 안전성 확보 : 동기화 메커니즘 사용
  2. 락 프리 알고리즘 활용 : 성능 향상을 위해 가능한 경우 사용
  3. 이벤트 큐 사용 : 다중 스레드에서 안전하게 이벤트 처리

 예시

// 스레드 안전 이벤트 큐
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

  1. 명확한 이벤트 명명 규칙 사용
DECLARE_EVENT(FMyClass, FOnSomethingHappenedEvent)
  1. 이벤트 문서화
/** 플레이어가 데미지를 받을 때 발생하는 이벤트 */
DECLARE_EVENT_TwoParams(AMyCharacter, FOnTakeDamageEvent, float, AActor*)
  1. 이벤트 핸들러의 범위 제한
SomeActor->OnEventHappened.AddWeakLambda(this, [this]()
{
    if (IsValid(this))
    {
        // 이벤트 처리 로직
    }
});
  1. 컴포넌트 기반 이벤트 시스템 구축
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UEventManagerComponent : public UActorComponent
{
    GENERATED_BODY()
 
public:
    DECLARE_EVENT(UEventManagerComponent, FOnLevelCompleteEvent)
    FOnLevelCompleteEvent OnLevelComplete;
 
    void TriggerLevelComplete()
    {
        OnLevelComplete.Broadcast();
    }
};
  1. 이벤트 우선순위 설정
SomeActor->OnEventHappened.AddUObject(this, &AMyActor::HighPriorityHandler, FEventPriority::High);
SomeActor->OnEventHappened.AddUObject(this, &AMyActor::NormalPriorityHandler, FEventPriority::Normal);

 이벤트 바인딩 기법은 언리얼 엔진에서 유연하고 확장 가능한 게임 시스템을 구축하는 데 핵심적인 역할을 합니다. 다양한 이벤트 유형을 적절히 활용하고, 동적/정적 바인딩을 상황에 맞게 선택하며, 람다 함수를 통한 간결한 이벤트 처리 등을 통해 효율적인 코드를 작성할 수 있습니다.

 그러나 이벤트 기반 프로그래밍의 장점을 최대한 활용하면서도, 과도한 사용으로 인한 복잡성 증가와 성능 저하를 주의해야 합니다. 멀티스레드 환경에서의 안전한 이벤트 처리와 성능 최적화 전략을 적용함으로써, 안정적이고 효율적인 게임 시스템을 구축할 수 있습니다.

 지속적인 프로파일링과 코드 리뷰를 통해 이벤트 시스템의 효율성을 모니터링하고 개선하는 것이 중요합니다.