KoreanFoodie's Study

언리얼 델리게이트 시스템 (Unreal Delegate System) 본문

Game Dev/Unreal C++ : Study

언리얼 델리게이트 시스템 (Unreal Delegate System)

GoldGiver 2022. 3. 31. 14:24

델리게이트

C# 에서는 발행자-구독자(Publisher-Subscriber) 패턴의 구현이 가능해, 특정 이벤트를 구독한 객체는 특정 이벤트가 발행되었을 때 원하는 콜백함수가 실행되도록 만들 수 있다. 이때, 앞서 이야기한 특정 이벤트를 다른 말로 "델리게이트"라고 한다.

C++ 에서는 델리게이트 시스템이 없지만, 언리얼은 자체적으로 델리게이트를 제공하고 있다. 델리게이트를 이용하는 것은 기존에 함수 포인터 등을 이용했던 것보다 간편하며 안전하다. 

대리자를 통해 함수를 호출하므로 호출할 함수나 이를 포함하는 객체가 없어져도 대리자가 체크해 안전하게 함수를 호출할 수 있으며, 동일한 형을 가진 함수 여러 개를 대리자가 묶어서 관리하고 필요할 때 동시에 모두 호출할 수도 있다.

 

델리게이트를 사용하기 위해서는 먼저 매크로를 이용해 델리게이트를 선언한다. 델리게이트는 우리가 지정한 함수의 리턴 값과 인자 타입을 가지는 함수만 대표할 수 있다. 이제 실제 예제를 보자.

 

Boss.h

DECLARE_DELEGATE_OneParam(FBossDyingMessageSignature, const FString& );

보스가 죽으면 FString 타입으로 다잉메시지를 전달할 델리게이트 타입을 선언했다. 델리게이트 타입 이름을 지정할 때는 주로 Signature 라는 이름을 사용한다. 위의 델리게이트 타입에서 파라미터는 한 개밖에 없다.

 

델리게이트 타입 선언이 완료되면, 이제 델리게이트 타입의 변수를 선언하자.

 

Boss.h

UFUNCTION()
void BossHasDied(const FString& DyingMessage);

FBossDyingMessageSignature BossDyingMessageDelegate;

BossHasDied 의 작업이 완료되면 델리게이트에 등록된 함수가 호출되도록 BossHasDied 함수를 구현할 것이다.

 

Boss.cpp

void UBoss::BossHasDied(const Fstring& DyingMessage)
{
	UE_LOG(Boss, Warning, TEXT("Boss Has Died!"));
	BossDyingMessageDelegate.ExecuteIfBound(TEXT(%s), *DyingMessage);
}

ExecuteIfBound 를 이용하면 델리게이트에 함수가 바인딩되어 있을 때만 함수를 호출하게 만들 수 있다!

 

이제 GameInstance 에서 Boss 에서 정의된 델리게이트를 이용, 바인딩된 함수를 호출해 보자.

 

MyGameInstance.h

UPROPERTY()
class UBoss* Boss;

UFUNCTION()
void BossDyingMessageComplete(const FString& message);

 

UMyGameInstance.cpp

void UMyGameInstance::DyingMessageComplete(FString& messsage)
{
	UE_LOG(MyGameInstance, Warning, TEXT("Received Message : %s"), *message);
}

void UMyGameInstance::Init()
{
	// 먼저 바인딩을 해 준다
	Boss->BossDyingMessageDelegate.BindUObject(this, &UMyGameInstance::DyingMessageComplete);
	
	// Boss 에 정의된 BossHasDied 를 호출하면 등록된 함수인 DyingMessageComplete 가 호출된다!
	Boss->BossHasDied(TEXT("Don't let them notice Boss has died!"));
}

먼저 Boss 오브젝트 내의 BossDyingMessageDelegate 에 DyingMessageComplete 를 바인딩해 준다. 그리고 BossHasDied 를 호출하면, BossHasDied 내에서 BossDyingMessageDelegate 에 바인딩된 함수인 DyingMessageComplete 를 호출하게 된다. 따라서 출력은 다음과 같을 것이다 :

Boss Has Died!
Received Message : Don't let them notice Boss has died! // 바인딩된 함수 호출

 

델리게이트를 바인딩할 시, 델리게이트에 등록할 함수의 종류에 따라 호출하는 함수가 달라진다.

  • 전역 C++ 함수 : BindStatic API를 사용해 등록
  • 전역 C++ 람다 함수 : BindLambda API를 사용해 등록
  • C++클래스 멤버 함수 : BindRaw  API를 사용해 등록
  • 공유포인터 클래스의 멤버 함수 (쓰레드 미지원) : BindSP API를 사용해 등록 
  • 공유포인터 클래스의 멤버 함수 (쓰레드 지원) : BindThreadSafeSP API를 사용해 등록
  • UFUNCTION 멤버 함수 : BindUFunction API를 사용해 등록
  • 언리얼 오브젝트의 멤버함수 : BindUObject API를 사용해 등록
위 API 목록 중에서 우리가 바인딩하려는 BossDyingMessageComplete 함수는 언리얼 오브젝트 MyGameInstance의 멤버 함수이므로, CreateUObject를 사용해 바인딩하여야 한다!

 

 

MULTICAST & DYNAMIC

위의 델리게이트는 하나의 델리게이트에 하나의 함수만 바인딩하고 있다. 하지만 DECLARE_DELEGATE 대신 DECLARE_MULTICAST_DELEGATE 매크로를 붙이면 여러 개의 함수를 바인딩할 수 있다. 또한 MULTICAST 델리게이트는 Execute API 대신 Broadcast API 를 사용해야 한다. (바인딩된 함수가 없으면 아무 일도 안 일어남)

 

또한 언리얼에서는 다이나믹(Dynamic) 델리게이트를 제공하는데, 다이나믹 델리게이트는 함수의 이름을 기반으로 등록해 호출하는 방식이다. 따라서 간편하지만, 동작이 느린 단점이 있다. 다이나믹 방식으로 델리게이트를 선언하려면 인자의 이름이 일치해야 하며, 델리게이트의 선언도 함수 인자 하나당 타입과 이름 정보가 들어가야 한다.

다이나믹 델리게이트 시스템은 블루프린트 함수와의 연동을 지원한다. 그리고 블루프린트에서 사용할 수 있게 하려면 기본적으로 MULTICAST 기능을 지원해야 하므로, 블루프린트와의 연동을 위해서는 DECLARE_DYNAMIC_MULTICAST_DELEGATE 매크로를 사용하게 된다!

위의 DECLARE_DELEGATE_OneParam 예제를 조금 확장해 DECLARE_DYNAMIC_MULTICAST_DELEGATE 로 만들어 보자.

 

Boss.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FBossDyingMessageSignature, const FString&, DyingMessage);

UCLASS()
class BOSS_API UBoss : public UObject
{
	...

	UFUNCTION()
	void BossHasDied(const FString& BossName);

	UPROPERTY(BlueprintAssignable, Category = "Boss")
	FBossDyingMessageSignature BossDyingMessageDelegate;
}

BlueprintAssignable 로 델리게이트를 추가해햐 블루프린트에서 델리게이트를 검색할 수 있다!

 

Boss.cpp

void UBoss::BossHasDied(const FString& BossName)
{
	UE_LOG(Boss, Warning, TEXT("Boss Has Died!"));
	BossDyingMessageDelegate.Broadcast(TEXT(%s), *BossName);
}

 

MyGameInstance.h

UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Boss")
class UBoss* Boss;

UFUNCTION()
void BossDyingMessageComplete(const FString& DyingMessage);
// Delegate 에 선언된 인자 이름 DyingMessage 와 일치함

 

MyGameInstance.cpp

void UMyGameInstance::BossDyingMessageComplete(const FString& DyingMessage)
{
	UE_LOG(Warning, TEXT("%s said not to let them I died!"), *DyingMessage);
}

void UMyGameInstance::Init()
{
    Super::Init();
    
    Boss->BossDyingMessageDelegate.AddDynamic(this, &UMyGameInstance::BossDyingMessageComplete);
    Boss->BossHasDied(TEXT("King"));
}

 

출력 결과는 다음과 같을 것이다!

Boss Has Died!
King said not to let them I died // 델리게이트를 통한 호출

 

Comments