https://kyoun.tistory.com/143 참고

 

딜리게이트

 

이번 시간에는 언리얼 C++의 특징인 딜리게이트(Delegate)에 대해 알아보겠습니다.

딜리게이트(delegate)라는 단어를 사전에서 검색해보면 집단의 의사를 대표하는 대표자라고 나옵니다.
하지만 컴퓨터 프로그래밍에서 딜리게이트는 함수를 안전하게 호출할 수 있는 기능을 의미합니다. 

얼핏 들어보면 둘 간의 개념이 잘 매칭이 안되는데요, 잠깐 딜리게이트에 대해 알아보겠습니다. 

사실 C++ 언어에서는 딜리게이트라는 개념이 존재하지 않습니다.
딜리게이트는 C++ 의 다음 세대 언어인 C#에서 ( 정확히는 CLI 플랫폼 기반 ) 선보인 개념인데,

콜백 함수를 등록하기 위해 C 혹은 C++에서 사용한 함수 포인터가 문법이 복잡하고, 위험한 방식이었다면,
딜리게이트는 간편한 문법과 안전성을 갖춰 콜백 함수를 호출하기 위해 고안되었습니다. 

딜리게이트의 간편함과 안정성 외에도 이전 방식과 다른 큰 특징은 사전적 의미대로 집단의 의사를 대표합니다.
딜리게이트가 하나의 함수 뿐만 아니라 동일한 리턴값과 인자 타입을 가지는 여러 개를 관리할 수 있다는 것 입니다. 

딜리게이트의 이러한 특징은 C# 언어의 주요 패턴 중 하나인 발행/구독(Publish/Subscribe) 패턴의 구현으로 이어집니다.

C#의 발행/구독 패턴은 우리가 일상 생활에서 흔히 접하는 신문을 구독하는 절차와 유사한 방식입니다. 
구독자가 신문사에게 신문을 받아보겠다는 구독 의사를 밝히고 등록하면, 신문이 발행될 때마다
동일한 시간에 구독한 모든 구독자의 집으로 신문을 배달해주듯이, 특정 이벤트가 발생하면
딜리게이트에 등록된 모든 함수를 한꺼번에 호출할 수 있습니다.  

예를 들어 어떤 게임에서 보스와 보스가 스폰한 미니언(Minion)이 있어서 보스가 죽으면,
이 미니언(Minion)도 함께 죽게 만들고 싶다고 가정합시다. 

보스가 죽는 이벤트를 딜리게이트로 정의하고 미니언을 스폰할 때마다 각 미니언들이 이를 구독하게 설정해두면,
보스가 죽을 때, 명령 하나로 모든 미니언들에게 보스가 죽었다고 알려줄 수 있습니다. 


 딜리게이트 기능의 특징은 다음과 같이 요약할 수 있습니다.

- 함수 포인터직접 접근이 아닌 대리자를 통한 함수 호출 방식 
- 호출할 함수나 이를 포함하는 객체가 없어져도, 대리자가 체크해 안전하게 처리할 수 있음. 
- 동일한 형을 가진 함수 여러 개를 대리자가 묶어서 관리하고, 필요할 때 동시에 모두 호출하는 것이 가능함.

※ 딜리게이트 기능은 C++언어에서는 제공하지 않습니다만, 언리얼 C++은 자체적으로 프레임웍을 제작해 이 기능을 지원하고 있습니다.


 

언리얼에서 딜리게이트 기능을 사용하려면 먼저 매크로를 사용해 딜리게이트를 선언해야 합니다.
딜리게이트는 모든 함수 유형을 대변할 수 없고,
우리가 지정한 함수의 리턴값과 인자 타입가지는 함수 대표할 수 있습니다. 

그러면 기존 코드를 업그레이드해 직접 딜리게이트를 사용해봅시다. 

이번에 구현할 예제는 지난번에 제작한 두 언리얼 오브젝트인,
게임 인스턴스와 WebConnect를 활용해 볼텐데요, 게임 인스턴스가 WebConnect에게 아이디를 넘겨주면서,
네트웍상에서 사용자 아이디의 임시 토큰 데이터를 가져오도록 지시하고,
WebConnect는 서버에 질의한 결과를 딜리게이트를 통해 게임인스턴스에 전달하도록 구현해봅시다.

 

//UWebConnection.h

DECLARE_DELEGATE_OneParam(FTokenCompleteSignature, const FString&);  //인자 1개 델리게이트
DECLARE_DYNAMIC_DELEGATE(FTestDynamic);                              //다이나믹 델리게이트
DECLARE_MULTICAST_DELEGATE(FTestMutiDelegate);                       //멀티캐스트 델리게이트

UCLASS()
class WEBSERVICEK_API UWebConnection : public UObject
{
public:
....
	UFUNCTION()
	void RequestToken(const FString& UserID);

	UFUNCTION()
	void TestFunc();

	FTokenCompleteSignature TokenCompleteDelegate;
	//FTestMutiDelegate TestMuti;
	//FTestDynamic TestDynamic;

};
DECLARE_LOG_CATEGORY_EXTERN(WebConnection, Log, All);

 

UWebConnection::UWebConnection()
{

	TestMuti.AddLambda([this]()->void {TestFunc(); });
}


void UWebConnection::TestFunc()
{
	UE_LOG(WebConnection, Warning, TEXT("TestMuti"));
}


void UWebConnection::RequestToken(const FString& UserID)

{
	UE_LOG(WebConnection, Warning, TEXT("Request Token Call!"));
	TokenCompleteDelegate.ExecuteIfBound(TEXT("0LCJydGkiOiI2a3NjVE9pTUNESVZWM05qVTIyUnlTIn0.VJyMOicM"));
}

 

이제 게임 인스턴스에서는 WebConnect에 선언한 딜리게이트 타입과 동일하게 const FString& 인자 하나를 소유한 멤버 함수를 하나 만들고 WebConnection의 RequestToken 함수를 호출하기 전에 먼저 등록해줍시다. 
그러면 WebConnect의 작업이 끝나면 자동으로 WebConnect에 있는 딜리게이트에 의해 새로운 함수가 호출됩니다.  

아래는 이를 구현한 코드입니다.

 

//ABCGameInstance.h
class ABC_API UABCGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:

	UFUNCTION()
	void RequestTokenComplete(const FString& Token);
};

//ABCGameInstance.cpp
void UABCGameInstance::RequestTokenComplete(const FString& Token)
{
	AB_LOG(Warning, TEXT("Token : %s"), *Token);
}

void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);


    WebConnection->TokenCompleteDelegate.BindUObject(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny"));
    //WebConnection->TestMuti.Broadcast();
}

 


델리게이트 바인딩

 

딜리게이트를 바인딩할 시 주의할 점은 딜리게이트에 등록할 함수의 종류에 따라 호출하는 함수가 달라진다는 점입니다. 

언리얼 딜리게이트 시스템에 등록 가능한 함수는 다음과 같습니다.

  • 전역 C++ 함수 : BindStatic API를 사용해 등록
  • 전역 C++ 람다 함수 : BindLambda API를 사용해 등록
  • C++클래스 멤버 함수 : BindRaw  API를 사용해 등록
  • 공유포인터 클래스의 멤버 함수 (쓰레드 미지원) : BindSP API를 사용해 등록 
  • 공유포인터 클래스의 멤버 함수 (쓰레드 지원) : BindThreadSafeSP API를 사용해 등록
  • UFUNCTION 멤버 함수 : BindUFunction API를 사용해 등록
  • 언리얼 오브젝트의 멤버함수 : BindUObject API를 사용해 등록

위 API 목록 중에서 우리가 바인딩하려는 RequestTokenComplete 함수는
언리얼 오브젝트 ABGameInstance의 멤버 함수이므로, CreateUObject를 사용해 바인딩하여야 함을 알 수 있습니다. 


Input 바인딩

 

언리얼 엔진의 입력 처리도 위에서 진행한 방식과 유사하게 딜리게이트를 통해서 동작합니다. 
언리얼 시스템은 프로젝트의 설정에 입력한 입력 설정마다 딜리게이트를 가지는데,
이 각각의 딜리게이트에 우리가 지정한 언리얼 오브젝트의 멤버 함수를 바인딩하여 입력신호 값을 전달받도록 설계되어 있습니다.

이를 진행하기 위해 블루프린트 강좌에서 진행한 입력에 따른 캐릭터의 이동 기능을 C++로 구현해보겠습니다. 

 

먼저 프로젝트 설정의 입력으로 가서 Axis Mapping에 UpDown과 LeftRight 입력을 아래 그림과 같이 설정합시다.

입력의 설정

 

그리고 각 입력을 처리하도록 두 함수를 만들어 바인딩 시킵시다.
그리고 Tick 이벤트에서는 두 입력 값을 조합해 캐릭터를 회전시키는 로직을 추가합시다.
이를 위해서는 
ABPawn에 FloatingPawnMovement 컴포넌트를 추가해 주고,
스프링암의 값과 액터의 회전이 무관하게 세팅해준 후 AutoPossessPlayer 변수 값을 변경해
바로 조종할 수 있도록 만들어줍시다.
( 블루프린트강좌 2-10 예제에서는 컨트롤러의 회전을 사용해 캐릭터를 움직였지만, 이번 강좌에서는 컨트롤러의 회전이 아닌 액터의 회전을 사용했습니다. ) 

 

아래는 이를 구현한 코드입니다. 

 

//ABPawn.h
#pragma once

#include "GameFramework/Pawn.h"
#include "ABPawn.generated.h"

UCLASS(config=Game)
class ARENABATTLE_API AABPawn : public APawn
{
    GENERATED_BODY()

public:
    AABPawn();

    virtual void BeginPlay() override;
    virtual void Tick( float DeltaSeconds ) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;



    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="Collision")
    class UCapsuleComponent* Body;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Visual")
    class USkeletalMeshComponent* Mesh;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Helper")
    class UArrowComponent* Arrow;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
    class USpringArmComponent* SpringArm;

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
    class UCameraComponent* Camera;
 
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Movement")  //추가
    class UFloatingPawnMovement* Movement;  //추가

    UPROPERTY(config, BlueprintReadWrite, EditDefaultsOnly, Category = "Stat")
    float MaxHP;

    UPROPERTY(BlueprintReadWrite, EditInstanceOnly, Category = "Stat")
    float CurrentHP;

private:
   UPROPERTY(config)
    TArray<FStringAssetReference> CharacterAssets;
	
    ////밑으로 추가됨
    float CurrentLeftRightVal;
    float CurrentUpDownVal;

    UFUNCTION()
    void UpDownInput(float NewInputVal);

    UFUNCTION()
    void LeftRightInput(float NewInputval);

};
// ABPawn.cpp

#include "ArenaBattle.h"
#include "ABGameInstance.h"
#include "Kismet/KismetMathLibrary.h"
#include "ABPawn.h"
 
AABPawn::AABPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    Body = CreateDefaultSubobject<UCapsuleComponent>("Capsule");
    RootComponent = Body;
    
    Mesh = CreateDefaultSubobject<USkeletalMeshComponent>("Mesh");
    Mesh->SetupAttachment(Body);

    Arrow = CreateDefaultSubobject<UArrowComponent>("Arrow");
    Arrow->SetupAttachment(Body); 

    SpringArm = CreateDefaultSubobject<USpringArmComponent>("SpringArm");
    SpringArm->SetupAttachment(Body); 

    Camera = CreateDefaultSubobject<UCameraComponent>("Camera");
    Camera->SetupAttachment(SpringArm);

    Movement = CreateDefaultSubobject<UFloatingPawnMovement>("Movement"); 

    Body->SetCapsuleSize(34.0f, 88.0f);
    Mesh->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.0f, 0.0f));

    static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_Warrior(TEXT("SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard'"));
    Mesh->SetSkeletalMesh(SK_Warrior.Object);

    SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
    SpringArm->TargetArmLength = 650.0f;
    SpringArm->bInheritPitch = false;
    SpringArm->bInheritYaw = false;
    SpringArm->bInheritRoll = false; 

    MaxHP = 100.0f;
    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

 


void AABPawn::Tick( float DeltaTime )
{
    Super::Tick( DeltaTime );
    FVector InputVector = FVector(CurrentUpDownVal, CurrentLeftRightVal, 0.0F);
    if (InputVector.SizeSquared() > 0.0F)
    {
        FRotator TargetRotation = UKismetMathLibrary::MakeRotFromX(InputVector);
        SetActorRotation(TargetRotation);
        AddMovementInput(GetActorForwardVector());
    }
}


void AABPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
   Super::SetupPlayerInputComponent(InputComponent);
    InputComponent->BindAxis("LeftRight", this, &AABPawn::LeftRightInput);
    InputComponent->BindAxis("UpDown", this, &AABPawn::UpDownInput);
}
 

void AABPawn::LeftRightInput(float NewInputVal)
{
    CurrentLeftRightVal = NewInputVal;
} 

void AABPawn::UpDownInput(float NewInputVal)
{
    CurrentUpDownVal = NewInputVal;
}

 


딜리게이트를 이용한 비동기 리소스 로딩

 

이번에는 딜리게이트를 사용해 캐릭터에서 비동기 방식으로 리소스를 로딩하도록 변경해봅시다.
언리얼 엔진에서는 비동기로딩 방식을 위해 FStreamableManager 클래스에서 AsyncLoadRequest라는 API를 제공합니다.  

이 API에는 결과를 받아오기 위한 딜리게이트 FStreamableDelegate를 전달하는 것이 필요한데,
이 딜리게이트는 StreamableManager.h에 아래와 같이 선언되어 있습니다.

이는 리턴값이 void이고 인자가 없는 함수 형식을 의미합니다. 

DECLARE_DELEGATE( FStreamableDelegate );
//비동기로딩에 사용하기 위해 이미 선언된 딜리게이트

 

ABPawn의 멤버 변수로 해당 딜리게이트를 선언하고 이를 전달해도 되지만,
아래와 같이 Create로 시작하는 API 셋을 사용하면 딜리게이트를 즉시 생성해 필요한 곳에 전달하는 것이 가능합니다. 아래 코드에서 주석을 처리한 부분과 비교해보시기 바랍니다. 

 

 //ABPawn.h
{
public:
    UFUNCTION()
    void CharacterMeshDeferred();
    //FStreamableDelegate StreamableDelegate;
 
private:
    int32 NewIndex;
}
 

 

//ABPawn.cpp
void AABPawn::BeginPlay()
{
    Super::BeginPlay();
    CurrentHP = MaxHP;
    NewIndex = FMath::RandRange(0, CharacterAssets.Num() - 1);
    UABGameInstance* ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
    if (ABGameInstance)
    {
        //StreamableDelegate.BindUObject(this, &AABPawn::CharacterMeshDeferred);
        //ABGameInstance->AssetLoader.RequestAsyncLoad(CharacterAssets[NewIndex], StreamableDelegate);
        ABGameInstance->AssetLoader.RequestAsyncLoad(CharacterAssets[NewIndex], FStreamableDelegate::CreateUObject(this, &AABPawn::CharacterMeshDeferred));
    }
}

void AABPawn::CharacterMeshDeferred()
{
    AB_LOG_CALLONLY(Warning);
    TAssetPtr<USkeletalMesh> NewCharacter(CharacterAssets[NewIndex]);
    if (NewCharacter)
    {
        Mesh->SetSkeletalMesh(NewCharacter.Get());
    }
}

지금까지 알아본 딜리게이트 선언은 하나의 딜리게이트에 하나의 함수만 연결(Binding)해 실행하는 기능이었습니다. 


멀티캐스트, 다이나믹, 다이나믹 멀티캐스트

 

이번 강좌 도입부에서 설명한 하나의 딜리게이트에 여러 개의 함수를 연결하는 기능은 우리가 지금까지 실습한

DECLARE_DELEGATE 매크로가 아닌 MULTICAST를 붙인
DECLARE_MULTICAST_DELEGATE 매크로를 사용해야 합니다.

MULTICAST 딜리게이트는 Execute API 대신에 Broadcast API를 사용해야 딜리게이트에 연결된 모든 함수가 실행됩니다. (Broadcast API에는 BroadcastIfBound라는 함수는 없습니다. 아무 연결이 없으면 그냥 아무일도 안 일어납니다. ) 

 

언리얼 엔진에서는 딜리게이트 종류에 MULTICAST 외에도 다이나믹(Dynamic) 딜리게이트라는 것을 제공합니다. 
다이나믹 딜리게이트함수포인터가 아닌, 함수의 이름을 기반으로 등록해 호출하는 방식입니다.

이름 기반이다보니 저장할 수 있다는 특징이 있습니다만, 반면에 동작이 느리다는 단점이 있습니다.
다이나믹 방식으로 딜리게이트를 선언하려면 인자의 이름까지 정확히 일치해야 합니다.
그래서 딜리게이트의 선언도 함수 인자 하나당 타입과 이름 정보가 들어가야 합니다.  

다이나믹 딜리게이트 시스템이 필요한 이유는
딜리게이트 시스템에 C++ 함수 뿐만 아니라 블루프린트 함수도 연결할 수 있게 하기 위해서입니다.
하지만 블루프린트에서 사용할 수 있게 하려면 기본적으로 MULTICAST 기능도 지원해주어야 합니다.
(1개가 아닌 여러 함수를 실행해야 해서?)

따라서 딜리게이트를 블루프린트의 함수와도 연동하고 싶은 경우에는 DYNAMIC과 MULTICAST가 합쳐진 DECLARE_DYNAMIC_MULTICAST_DELEGATE 매크로를 사용해야 합니다. 

아래는 WebConnection에 선언한 딜리게이트를 블루프린트에서도 사용할 수 있도록 확장한 예입니다.
함수를 연결할 때에는 AddDynamic API를 사용해주면 됩니다

 

//WebConnection.h
#include "UObject/NoExportTypes.h"
#include "WebConnection.generated.h"
 

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTokenCompleteSignature, const FString& , Token);
 
UCLASS()
class WEBSERVICE_API UWebConnection : public UObject
{
    GENERATED_BODY()

public:
....

    UPROPERTY(BlueprintAssignable, Category="WebService")
    FTokenCompleteSignature TokenCompleteDelegate;

};
 
DECLARE_LOG_CATEGORY_EXTERN(WebConnection, Log, All);


//WebConnection.cpp
void UWebConnection::RequestToken(const FString& UserID)
{
	UE_LOG(WebConnection, Warning, TEXT("Request Token Call!"));

	TokenCompleteDelegate.Broadcast(TEXT("0LCJydGkiOiI2a3NjVE9pTUNESVZWM05qVTIyUnlTIn0.VJyMOicM"));
}

 

//ABGameInstance.h
{
public:
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "WebService")
	class UWebConnection* WebConnection;
}    

//ABGameInstance.cpp
void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);

    WebConnection->TokenCompleteDelegate.AddDynamic(this, &UABGameInstance::RequestTokenComplete);
    WebConnection->RequestToken(TEXT("destiny"));
}

 

추가로 WebConnection.h의 딜리게이트 변수 선언에 보이는 것처럼
UPROPERTY에 BlueprintAssignable 키워드를 추가해주어야 블루프린트에서 딜리게이트를 검색할 수 있됩니다.
아래 그림은 ABGameInstance를 확장한 블루프린트에서 블루프린트 함수로 이벤트를 받을 수 있게 연결한 예시입니다.

 

2개의 델리게이트 실행화면

 

블루프린트에서 블루프린트 함수를 딜리게이트에 연결하는 모습

 

 

이번에도 강좌가 길었는데, 정리하면 다음과 같습니다.

- 딜리게이트는 함수 포인터 대신 간편한 문법으로 안전하게 호출해주는 대리자 개념. 콜백, 이벤트 구독에 많이 사용됨.
- 하나의 딜리게이트가 모든 유형의 함수를 커버할 수 없기 때문에 대리할 함수 유형을 매크로로 지정해야 한다.
- 언리얼 엔진의 입력 시스템은 딜리게이트를 통한 입력 값의 전달 방식으로 이루어져있다.  
- 연결(바인딩)할 함수의 성격에 따라 다양한 API가 존재한다. 
- 딜리게이트형선언::Create~ 함수를 사용해 즉석에서 딜리게이트를 제작해 넘겨주는 것도 가능하다. 
- 같은 유형을 가진 여러 함수를 묶어서 발행/구독 모델처럼 사용할 때는 MULTICAST 계열 매크로를 사용한다.
- 블루프린트와 연동시에는 DYNAMIC_MULTICAST 계열 매크로를 사용한다. 

+ Recent posts