[UE5] 언리얼 엔진의 델리게이트

연하·2024년 5월 23일
0

Unreal Engine Study

목록 보기
6/9

[이득우의 언리얼 프로그래밍]
강의를 보고 정리한 내용입니다

강한 결합과 느슨한 결합

class Card
{
public:
	Card(int InId) : Id(InId) {}
    int Id = 0;
};

class Person
{
public:
	Person(Card InCard) : IdCard(InCard) {}
    
protected:
	Card IdCard;
};

Card가 없는 경우 Person이 만들어질 수 없다. 이 때 Person은 Card에 대한 의존성을 가진다고 한다. 만약 핸드폰에서도 인증할 수 있는 새로운 카드가 도입된다면, 코드를 변경해야 할 것이다.

class ICheck
{
public:
	virtual bool Check() = 0;
};

class Card : public ICheck
{
public:
	Card(int InId) : Id(InId) {}
   	virtual bool Check() { return true; }
private:
	int Id = 0;
};

class Person
{
public:
	Person(ICheck* InCheck) : Check(InCheck) {}
protected:
	ICheck* Check;
};

왜 Person이 Card가 필요할까? 출입을 확인해야 하기 때문이다. 출입에 관련된 추상적인 설계에 의존하면, 새로운 출입 인증방식인 핸드폰이 추가되더라도 ICheck를 상속받아 만들면 Person을 고칠 필요가 없어진다. 이러한 느슨한 결합 구조는 유지 보수를 손쉽게 만들어준다.

델리게이트(Delegate)

그런데 이렇게 추상화 작업을 위해서 매번 인터페이스를 만드는 것이 번거로울 수 있다. 그렇다면 어떤 행동(함수)를 오브젝트처럼 관리하면 어떨까? 이걸 위해 언리얼 C++은 델리게이트를 지원한다. 느슨한 결합 구조를 간편하고 안정적으로 구현할 수 있다.

발행 구독 디자인 패턴

푸시 형태의 알림을 구현할 때 유용하게 사용하는 설계 방법이다. 시스템의 객체는 콘텐츠 제작자와 구독자로 구분된다. 콘텐츠 제작자는 콘텐츠를 생산하는데, 자기를 대신해 줄 발행자를 새롭게 만들어준다. 발행자는 콘텐츠를 배포하는 역할을 담당하게 되는데, 구독자는 발행자로부터 배포된 콘텐츠를 받아서 소비하게 된다. 이런 구조를 가지게 되면 콘텐츠 제작자는 콘텐츠 제작에만 전념할 수 있고, 구독자는 콘텐츠를 받아 소비하는데만 전념할 수 있다. 중간에 발행자라는 존재가 있기 때문에, 제작자와 구독자는 느슨한 결합 관계를 유지할 수 있게 된다. 이러한 구조는 유지보수가 쉽고, 유연하게 활용될 수 있으며 테스트가 쉽다.

언리얼 델리게이트의 선언

<설계 시 고려사항>

  • 어떤 데이터를 전달하고 받을 것인가? 인자의 수와 각각의 타입을 설계해주어야 한다.
  • C++ 프로그래밍에만 사용할 것인지, 블루프린트 함수와도 사용할 것인지를 고려해야 한다.
  • 어떤 함수와 연결(클래스 외부에 설계된 C++ 함수, 전역에 설계된 정적 함수, 언리얼 오브젝트의 멤버 함수)될 것인지 고려해야 한다.

<매크로>

설계를 끝낸 뒤, 여기에 맞는 매크로를 찾아야 한다.

DECLARE_{델리게이트 유형}DELEGATE{함수정보}

델리게이트 유형

  • 일대일 형태로 C++만 지원한다면 유형은 공란으로 둔다
  • 일대다 형태로 C++만 지원한다면 MULTICAST를 선언한다
  • 일대일 형태로 블루프린트를 지원한다면 DYNAMIC을 선언한다.
  • 일대다형태로 블루프린트를 지원한다면 DYNAMICMULTICAST를 조합한다

함수 정보

  • 인자가 없고 반환값도 없으면 공란으로 둔다.
  • 인자가 하나고 반환값이 없으면 OneParam으로 지정한다.
  • 인자가 세 개고 반환값이 있으면 RetVal_ThreeParams로 지정한다.(MULTICAST는 반환값을 지원하지 않는다)
  • 파라미터는 최대 9개까지 지원한다.

언리얼 델리게이트의 설계

두개의 인자를 가진 MULTICAST DELEGATE를 선언해보자.

DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);

델리게이트 이름 지정은 보통 F로 시작하고, 클래스 이름을 넣고, 이벤트가 발생했다는 뜻의 On을 넣은 다음에 알릴 내용(학사정보가 변경됐다)의 동사(?)를 붙혀준다. 그리고 마지막에 Signature라는 접미사를 많이 붙힌다.

이렇게 선언한 델리게이트는 멤버변수로 선언해주면 된다.

// CourseInfo.h

DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);

UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
	GENERATED_BODY()

public:
	UCourseInfo();
    
    FCourseInfoOnChangedSignature OnChanged;
    
    void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
    
private:

	FString Contents;
};
// CourseInfo.cpp

#include "CourseInfo.h"

UCourseInfo::UCourseInfo()
{
	Contents = TEXT("기존 학사 정보");
}

void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	Contents = InNewContents;
    
    UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
    OnChanged.Broadcast(InSchoolName, Contents);
}
// Student.h

UCLASS()
class UNREALDELEGATE_API UStudent : public UPerson, public ILessonInterface
{
	GENERATED_BODY()
    
public:
	UStudent();
    virtual void DoLesson() override;
    void GetNotification(const FString& School, const FString& NewCourseInfo);
}
// Student.cpp

void GetNotification(const FString& School, const FString& NewCourseInfo);
{
	UE_LOG(LogTemp, Log, TEXT("[Student] %s님이 %s로부터 받은 메시지 : %s"), *Name, *School,*NewCourseInfo);
}

여기서 눈여겨 볼 점은, Student의 어디에도 CourseInfo에 대한 헤더를 포함하지 않았다는점이다. CourseInfo 또한, 어디에도 Student, Staff, Teacher의 헤더를 포함하지 않는다.

이제, 이 중간의 객체를 만들자. 여기서는 MyGameInstance를 사용한다.

// MyGameInstance.h

UCLASS()
class UNREALDELEGATE_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
public:
	UMyGameInstance();
    virtual void Init() override;
    
private:
	UPROPERTY()
    TObjectPtr<class UCourseInfo> CourseInfo;
    
    UPROPERTY()
    FString SchoolName;
};
// MyGameInstance.cpp

void UMyGameInstance::Init()
{
	Super::Init();
    
    CourseInfo = NewObject<UCourseInfo>(this);
    UE_LOG(LogTemp, Log, TEXT("============="));
    
    UStudent* Student1 = NewObject<UStudent>();
    Student1->SetName(TEXT("학생1"));
    UStudent* Student2 = NewObject<UStudent>();
    Student2->SetName(TEXT("학생2"));
    UStudent* Student3 = NewObject<UStudent>();
    Student3->SetName(TEXT("학생3"));
    
    CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
    CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
    CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);
    
    CouseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
    
    UE_LOG(LogTemp, Log, TEXT("============="));
}

0개의 댓글