#1. Delegate
1. 개념
// SomeActor.h
DECLARE_DELEGATE(FSomeDelegate);
UClass()
class SomeActor : public AActor
{
GENERATE_BODY()
public:
SomeActor();
public:
FSomeDelegate SomeDelegate;
};
// AnotherActor.h
class SomeActor;
UClass()
class AnotherActor : public AActor
{
GENERATE_BODY()
public:
AnotherActor();
public:
UFUNCTION()
void CallbackBoundToSomeDelegate();
};
Delegate는 C++의 함수 포인터 개념을 확장한 것으로, 객체와 함수의 동적 바인딩을 지원하는 Unreal Engine의 핵심적인 기능입니다. 일반적으로, C++의 Delegate는 하나 이상의 함수에 대한 참조를 캡슐화하는 객체입니다. 이 객체를 통해 함수를 간접적으로 호출할 수 있으며, 런 타임에 함수를 동적으로 바인딩하거나 해제할 수 있습니다.
2. 특징
- 타입 안정성: 컴파일 시점에 컴파일러는 Delegate에 바인딩된 함수들의 시그니처가 Delegate의 것과 일치하는지 확인합니다. 따라서, 동적 바인딩되는 함수들의 시그니처 불일치로 인한 오류 발생을 방지합니다.
- 성능 향상: 런 타임 타입 검사를 수행하지 않기 때문에, 런 타임 오버헤드가 적습니다.
- 동적 바인딩: 런 타임에 Delegate에 함수를 등록하거나, 변경할 수 있습니다. 이를 통해, 코드 작성에 유연성을 제공합니다.
- 자원 관리: 이미 소멸한 객체의 Delegate는 무효화됩니다. 따라서, 이미 소멸된 객체에 대한 호출 작업을 방지합니다.
- 비동기 프로그래밍: 콜백 매커니즘을 활용해 비동기 프로그래밍을 지원합니다. 예를 들면, 작업 도중 이벤트 발생 알림을 위한 Delegate 실행 시 바인딩된 콜백 함수들이 호출되어 현재 작업 흐름 외 백그라운드에서 다른 작업들을 비동기적으로 처리할 수 있습니다.
- 블루프린트 호환성: Dynamic 유형의 Delegate는 C++ 환경과 블루프린트 스크립팅 시스템 간의 상호작용을 지원합니다.
3. 단점
- 성능 저하: Delegate는 함수 포인터를 통해 바인딩된 함수들을 간접적으로 호출하므로, 직접적인 호출보다 느립니다. 더불어, Delegate는 컴파일 시점에 타입 검사가 필요하고, Delegate 객체 자체는 추가적인 메모리 공간이 요구됩니다.
- 복잡성: Delegate는 간접적인 함수 호출을 수행하므로, 흐름 파악이 어려울 수 있습니다. 특히, 런 타임에 동적 바인딩을 수행하는 Delegate는 더 어려울 수 있습니다. 이에 더해, Delegate를 통한 순환 참조가 발생할 수 있으므로 주의해야 합니다.
4. 종류
- Single-Cast: 단일 함수만 바인딩 가능한 Delegate입니다.
- Multi-cast: 여러 함수를 바인딩할 수 있는 Delegate입니다.
- Dynamic: 블루프린트와 직렬화가 가능한 Delegate입니다.
#2. Single-Cast Delegate
1. 개념
Single-cast delegate는 오직 하나의 함수만 바인딩 가능한 Delegate 유형입니다. 만약, 새로운 바인딩 작업을 수행할 경우 이전 함수는 새로운 함수로 대체됩니다.
2. 특징
- 오직 하나의 함수만 바인딩 가능합니다.
- 새로운 바인딩은 이전 바인딩을 덮어씁니다.
3. 코드
1. 반환 유형
DECLARE_DELEGATE_RetVal(float, FCalculateValue);
class AMyActor : public AActor
{
FCalculateValue CalculateDelegate;
void Setup()
{
CalculateDelegate.BindUObject(this, &AMyActor::CalculateHealth);
}
float CalculateHealth()
{
return 100.0f;
}
void UseDelegate()
{
if (CalculateDelegate.IsBound())
{
float Result = CalculateDelegate.Execute();
UE_LOG(LogTemp, Log, TEXT("Calculated Value: %f"), Result);
}
}
};
2. 한 개 이상의 Parameter
DECLARE_DELEGATE_TwoParams(FOnDataReceived, const FString&, int32);
class ADataHandler : public AActor
{
FOnDataReceived OnDataReceived;
void Setup()
{
OnDataReceived.BindUObject(this, &ADataHandler::HandleData);
}
void HandleData(const FString& Data, int32 Count)
{
UE_LOG(LogTemp, Log, TEXT("Received: %s, Count: %d"), *Data, Count);
}
void ReceiveData(const FString& NewData, int32 NewCount)
{
if (OnDataReceived.IsBound())
{
OnDataReceived.Execute(NewData, NewCount);
}
}
};
3. Dynamic(블루프린트 호환)
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(float, FCalculateDamage, float, BaseValue);
UCLASS()
class UDamageCalculator : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable, Category = "Damage")
FCalculateDamage OnCalculateDamage;
UFUNCTION(BlueprintCallable, Category = "Damage")
float CalculateFinalDamage(float InitialDamage)
{
if (OnCalculateDamage.IsBound())
{
return OnCalculateDamage.Execute(InitialDamage);
}
return InitialDamage; // 기본값 반환
}
};
// C++에서 사용
float MyDamageCalculation(float BaseValue)
{
return BaseValue * 1.5f;
}
// 바인딩 예제
UDamageCalculator* DamageCalc = NewObject<UDamageCalculator>();
DamageCalc->OnCalculateDamage.BindStatic(&MyDamageCalculation);
#3. Multi-cast
1. 개념
Multi-cast Delegate는 다중 함수 바인딩을 지원하는 Delegate입니다. Multi-cast Delegate는 Broadcast()를 통해 바인딩된 함수를 한 번에 모두 호출할 수 있습니다. 따라서, 여러 구독자가 존재하는 이벤트 시스템에 적합합니다.
2. 특징
- 다중 바인딩
- 브로드캐스팅
- 유연성
- 이벤트 시스템
3. 코드
1. Multi-cast
DECLARE_MULTICAST_DELEGATE(FOnGameEvent)
class AGameManager : public AActor
{
public:
FOnGameEvent OnGameStart;
FOnGameEvent OnGameEnd;
void StartGame()
{
// 게임 시작 로직
OnGameStart.Broadcast();
}
void EndGame()
{
// 게임 종료 로직
OnGameEnd.Broadcast();
}
};
class APlayerController : public APlayerController
{
void SetupGameEvents(AGameManager* GameManager)
{
GameManager->OnGameStart.AddUObject(this, &APlayerController::OnGameStarted);
GameManager->OnGameEnd.AddUObject(this, &APlayerController::OnGameEnded);
}
void OnGameStarted()
{
UE_LOG(LogTemp, Log, TEXT("Player: Game Started!"));
}
void OnGameEnded()
{
UE_LOG(LogTemp, Log, TEXT("Player: Game Ended!"));
}
};
2. Parameter를 가진 Multi-cast
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnScoreChanged, int32, const FString&)
class AScoreManager : public AActor
{
public:
FOnScoreChanged OnScoreChanged;
void UpdateScore(int32 NewScore, const FString& PlayerName)
{
// 점수 업데이트 로직
OnScoreChanged.Broadcast(NewScore, PlayerName);
}
};
class AHUD : public AHUD
{
void SetupScoreEvents(AScoreManager* ScoreManager)
{
ScoreManager->OnScoreChanged.AddUObject(this, &AHUD::UpdateScoreDisplay);
}
void UpdateScoreDisplay(int32 NewScore, const FString& PlayerName)
{
UE_LOG(LogTemp, Log, TEXT("HUD: Player %s score changed to %d"), *PlayerName, NewScore);
// HUD 업데이트 로직
}
};
3. Dynamic
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
UCLASS()
class ACharacter : public ACharacter
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable, Category = "Character")
FOnHealthChanged OnHealthChanged;
UFUNCTION(BlueprintCallable, Category = "Character")
void SetHealth(float NewHealth)
{
Health = NewHealth;
OnHealthChanged.Broadcast(Health);
}
private:
UPROPERTY(VisibleAnywhere, Category = "Character")
float Health;
};
// C++에서 사용
void AGameMode::SetupHealthListeners(ACharacter* Character)
{
Character->OnHealthChanged.AddDynamic(this, &AGameMode::OnCharacterHealthChanged);
}
void AGameMode::OnCharacterHealthChanged(float NewHealth)
{
UE_LOG(LogTemp, Log, TEXT("GameMode: Character health changed to %f"), NewHealth);
}
// 블루프린트에서도 이 Delegate에 함수를 바인딩할 수 있음
#3. Event
1. 개념
Event는 특정 상황이 발생했음을 알리는 메커니즘으로, 객체의 상태 변화나 중요한 동작 발생을 다른 객체에게 통지하는 데 사용됩니다. 발행-구독 모델을 따르며, 이벤트 발생 주체와 이벤트 처리 주체를 분리합니다. 이에 더해, 다중 구독자를 지원함으로써, 비동기 프로그래밍을 지원합니다. Unreal에서 정의하는 Event는 기존의 Event 개념을 확장해 C++ 환경과 블루프린트 스크립팅 시스템의 상호작용을 지원합니다. 따라서, C++ 환경에서 발행한 Event를 블루프린트 환경에서 구독하는 작업이 가능해집니다. 일반적으로, Unreal의 Event는 UFUNCTION() 매크로와 함께 사용하며, BlueprintImplementableEvent 또는 BlueprintNativeEvent 지정자를 사용합니다. DECLARE_EVENT 매크로를 활용한 Event 선언도 가능합니다. 이 경우, 해당 이벤트는 친구 클래스 혹은 클래스 내부에서만 활용 가능한 Event 활용 방식이며, C++ 환경에서만 활용할 때 사용합니다.
2. 특징
- 발행-구독 모델
- 다중 구독 지원
- 동적 바인딩 지원
- C++ 환경과 블루프린트 스크림팅 시스템 간 상호작용 지원
3. 단점
- 복잡성
- 성능 저하
4. Delegate vs Event
- Delegate는 주로 C++ 환경에서 활용하며, Event는 C++ 환경에서 발행하고 블루프린트에서 구현되는 특별한 함수입니다. Event의 UFUNCTION() 지정자가 BlueprintImplementableEvent일 경우, 블루프린트에서만 활용 가능하며, BlueprintNativeEvent일 경우, C++ 환경에서 기본 구현을 마치고 상세 내용을 블루프린트에서 오버라이드 합니다.
- Delegate는 Event보다 성능이 더 좋습니다. Event는 블루프린트 스크립팅 시스템과 상호작용하여 비교적 성능이 떨어집니다. 특히, Dynamic Delegate가 아닌 일반적인 Delegate의 경우 더욱 그렇습니다.
- Delegate는 C++ 환경에서 확장이 더 용이하며, Event는 블루프린트 환경에서 확장이 더 용이합니다.
5. 코드
1. BlueprintImplementableEvent
UCLASS()
class AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent, Category = "Character")
void OnHealthChanged(float NewHealth);
void TakeDamage(float Damage)
{
Health -= Damage;
OnHealthChanged(Health); // 블루프린트에서 구현
}
private:
float Health;
};
2. BlueprintNativeEvent
UCLASS()
class AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, Category = "Character")
void OnHealthChanged(float NewHealth);
void TakeDamage(float Damage)
{
Health -= Damage;
OnHealthChanged(Health); // C++ 구현이 있지만 블루프린트에서 오버라이드 가능
}
protected:
// C++ 구현 (블루프린트에서 오버라이드하지 않으면 이 함수가 호출됨)
virtual void OnHealthChanged_Implementation(float NewHealth)
{
UE_LOG(LogTemp, Log, TEXT("Health changed to %f"), NewHealth);
}
private:
float Health;
};
3. C++ 전용 Event
// MyActor.h
DECLARE_EVENT_OneParam(AMyActor, FOnHealthChangedEvent, float);
class AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
// 이벤트 객체
FOnHealthChangedEvent OnHealthChanged;
// 이벤트를 발생시키는 함수
void TakeDamage(float Damage);
// 이벤트에 대한 접근자 (다른 클래스에서 이벤트에 바인딩할 때 사용)
FOnHealthChangedEvent& GetOnHealthChangedEvent() { return OnHealthChanged; }
private:
float Health;
};
// MyActor.cpp
AMyActor::AMyActor()
{
Health = 100.0f;
}
void AMyActor::TakeDamage(float Damage)
{
Health -= Damage;
// 이벤트 발생
OnHealthChanged.Broadcast(Health);
}
// 사용 예제 (다른 클래스에서)
void AGameMode::BeginPlay()
{
Super::BeginPlay();
AMyActor* MyActor = GetWorld()->SpawnActor<AMyActor>();
if (MyActor)
{
// 이벤트에 함수 바인딩
MyActor->GetOnHealthChangedEvent().AddUObject(this, &AGameMode::OnActorHealthChanged);
}
}
void AGameMode::OnActorHealthChanged(float NewHealth)
{
UE_LOG(LogTemp, Log, TEXT("Actor health changed to: %f"), NewHealth);
}
C++ 전용 이벤트 선언 방식 및 활용 방법은 UFUNCTION() 지정자를 활용하는 방식 보다 성능이 더 좋습니다. C++ 환경에서 주로 활용하는 이벤트 선언 방식으로 블루프린트 스크립팅과 상호작용 오버헤드가 없기 때문입니다. 더불어, 이러한 Event 활용 방식은 객체 내부 혹은 친구 클래스에서만 바인딩 작업이 가능하기 때문에 유연성은 부족하지만, 더욱 엄격한 관리를 통한 가독성 향상 및 복잡성 저하에 기여합니다.
'게임개발 > Unreal C++' 카테고리의 다른 글
[Unreal]#비동기 프로그래밍 (0) | 2024.08.21 |
---|---|
[Unreal]#ESlateVisibility, UI 가시성 (0) | 2024.08.21 |
[Unreal]#스마트 포인터 (0) | 2024.07.30 |
[Unreal]#생명 주기 함수 (0) | 2024.07.24 |
[Unreal]#UBlueprintAsyncActionBase (0) | 2024.07.01 |