이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해 강의를 수강한 후 정리한 내용입니다.
🖥️ 언리얼 C++의 델리게이트를 활용한 발행 구독 디자인 패턴(Publisher-Subscriber Pattern)의 구현
ICheck
를 상속받은 새로운 카드 인터페이스를 선언해 해결한다. (카드가 구현하도록)ICheck
에 의존하면 된다.ICheck
인터페이스를 상속받는 출입도구 클래스가 check()
가상함수를 구현하도록 하면, Person 클래스에서는 수정이 필요 없다.델리게이트는 한국어로 대리자를 의미한다. 델리게이트는 함수를 참조하는 대리자이다. 함수가 아니라 함수 자체를 인자로 넘겨주는 ‘형식’이라는 것에 유의하자.
대리 실행이 왜 필요할까? 클래스 사이 의존성을 약화(=느슨한 결합) 시킬 수 있기 때문이다.
Delegate (델리게이트)로 C++ 오브젝트 상의 멤버 함수 호출을 일반적이고 유형적으로 안전한 방식으로 할 수 있습니다. 델리게이트를 사용하여 임의 오브젝트의 멤버 함수에 동적으로 바인딩시킬 수 있으며, 그런 다음 그 오브젝트에서 함수를 호출할 수 있습니다. 호출하는 곳에서 오브젝트의 유형을 몰라도 말이지요.
위 공식 문서에서 눈에 띄는 특징이 있다면 델리게이트의 안전성에 대해 강조하고 있다는 점. 함수 포인터보다 안정화 되어있고, 항상 참조로 전달해야 함을 알 수 있다.
게임 개발에서의 예시로 어떤 것이 있을까 생각해보았다.
델리게이트가 N개의 함수를 대리한다고 했는데, 일종의 ⛓️체인 효과⛓️ 를 줄 수 있다고 생각한다.
예를 들어, Monster클래스가 있다. Monster는 체력(HP)를 가지고 있다. Monster는 플레이어에게 공격을 받아 HP가 깎일 수 있다.
이 때, Monster의 체력바와 피가 튀는 이펙트가 출력된다고 가정하자면,
구현하기 나름이겠지만 체력바는 Monster의 HP가 바뀔 때만 다시 그리면 되고, 피가 튀는 이펙트도 HP가 깎일 때 만들어지면 된다.
이럴 때 그 클래스들은 Monster의 OnDamaged 델리게이트에 해당 함수를 바인드하면 된다.
void
를 반환하고 FString
을 인자로 가지는 함수 타입을 객체처럼 사용하고 싶다.
class FLogWriter
{
void WriteToLog( FString );
};
그럴 때는 DECLARE_DELEGATE_OneParam
라는 매크로를 사용해서 위 함수의 형태를 객체처럼 지정한다.
⇒ FString
을 인자로 가지는 FStringDelegate
을 만들겠다!
DECLARE_DELEGATE_OneParam( FStringDelegate, FString );
객체처럼 사용하고 싶다고 했으니, 델리게이트 FStringDelegate
을 멤버 변수에 등록해 사용한다.
class FMyClass
{
FStringDelegate WriteToLogDelegate;
};
엮고자 하는 클래스의 멤버함수를 묶어주는 함수가 BindSP
이다.
TSharedRef< FLogWriter > LogWriter(new FLogWriter() );
WriteToLogDelegate.BindSP( LogWriter, &FLogWriter::WriteToLog );
묶은 다음 실행할 때는 Execute
를 사용한다.
이렇게 하면 연결된 객체 정보를 몰라도 원하는 함수를 호출할 수 있게 된다.
WriteToLogDelegate.Execute(TEXT("델리게이트에 바인딩된 함수 호출"));
델리게이트에 함수를 바인딩하기 전 Execute()를 호출하면 assert 가 발동되는데, 이런 경우를 피하고 싶다면 아래와 하는 방법이 있다.
WriteToLogDelegate.ExecuteIfBound(TEXT("함수가 바인딩되었을 때만 실행"));
공식 문서 페이지에서 찾아보았다.
함수명 | 설명 | 비고 |
---|---|---|
Execute() | 델리게이트에 바인딩된 함수를 실행하는 함수다. 호출 전에 isBound() 함수를 이용해 델리게이트가 바인딩되어 있는지 확인해야 한다. Single-cast 델리게이트와 Multi-cast 델리게이트 모두에서 사용할 수 있다. | 델리게이트 실행하기 |
ExecuteIfBound() | 델리게이트에 바인딩된 함수를 실행하는 함수다. isBound() 함수를 호출하지 않아도 되며, Single-cast 델리게이트와 Multi-cast 델리게이트 모두에서 사용할 수 있다. | 델리게이트 실행하기 |
isBound() | 델리게이트가 바인딩되어 있는지를 확인하는 함수다. 바인딩되어 있지 않은 델리게이트를 호출하면 앱이 충돌할 수 있으므로, 반드시 호출 가능 여부를 확인해야 한다. Single-cast 델리게이트와 Multi-cast 델리게이트 모두에서 사용할 수 있다. | 자주 쓸 함수. |
아래 코드 참고 | ||
AddDynamic() | Multi-cast 델리게이트에서 이벤트를 처리할 메소드를 등록하는 함수다. Single-cast 델리게이트에서는 BindUFunction() 함수를 사용한다. | 자주 쓸 함수 |
Broadcast() | Multi-cast 델리게이트에서 바인딩된 모든 함수를 실행하는 함수다. 바인딩된 함수의 실행 순서는 정의되어 있지 않으므로, 순서에 의존하는 처리를 구현하는 것은 지양해야 한다. 또한, Multi-cast 델리게이트는 값을 반환할 수 없다. | 델리게이트 실행하기. |
자주 쓸 함수.
아래 코드 참고
if(FDelegate.isBound())
FDelegate.Broadcast(param있을 경우 param입력)
https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff649664(v=pandp.10)?redirectedfrom=MSDN
언리얼 엔진은 발행 구독 패턴 구현을 위해 델리게이트 기능을 제공한다.
그러니 델리게이트의 사용 방법에 대해 더 이해할 수 있도록 이 패턴을 알아보자!
💡 학교에서 진행하는 온라인 수업 활동을 예시로 들어보자면!!
UFUNCTION
으로 지정된 블루프린트 함수와 사용할 것인가일대일 형태로 C++만 지원한다면 공란
DECLARE_DELEGATE
일대다 형태로 C++만 지원한다면 MULTICAST 를 선언
DECLARE_MULTICAST
일대일 형태로 블루프린트를 지원한다면 DYNAMIC 을 선언
DECLARE_DYNAMIC
일대다 형태로 블루프린트를 지원한다면 DYNAMIC, MULTICAST 를 조합
- DECLARE_DYNAMIC_MULTICAST
C++ | 블루프린트 | |
---|---|---|
일대일 | DECLARE_DELEGATE | DECLARE_DYNAMIC |
일대다 | DECLARE_MULTICAST | DECLARE_DYNAMIC_MULTICAST |
DECLARE_DELEGATE
DECLARE_DELEGATE_OneParam
DECLARE_DELEGATE_RetVal_ThreeParams
💡 학사 정보 알림 서비스 만들기
**DECLARE_MULTICAST_DELEGATE_TwoParams
** 매크로를 사용한다.학사 정보를 관리하기 위해, 먼저 UObject
를 상속받는 CourseInfo
클래스를 생성해준다.
그리고 앞서 정한 선언 매크로 **DECLARE_MULTICAST_DELEGATE_TwoParams
** 를 이용해서 델리게이트를 정의하는데,
보통 언리얼 엔진에서 델리게이트의 이름을 지을 때 Signature 라는 접미사를 붙여준다.
또한 델리게이트 자료형 이름 앞에 F
접두사를 붙이지 않으면 에러가 발생한다.
// CourseInfo.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);
이렇게 선언된 FCourseInfoOnChangedSignature
델리게이트는 마치 객체처럼, 멤버 변수로 선언할 수 있다.
OnChanged
을 선언해주었다.
// CourseInfo.h
UCLASS()
class UEPARTONE_API UCourseInfo : public UObject
{
GENERATED_BODY()
public:
UCourseInfo();
FCourseInfoOnChangedSignature OnChanged;
};
그 다음으로 FCourseInfoOnChangedSignature
델리게이트에 어떤 함수가 바인딩이 되면 송출해줘야 하는데, 내용이 변경되면 송출한다고 했다.
외부에서 학사 정보를 변경할 때 지정하는 함수ChangeCourseInfo
와, 학사 정보 콘텐츠가 담길 변수 Contents
를 선언해주었다.
// CourseInfo.h
UCLASS()
class UEPARTONE_API UCourseInfo : public UObject
{
GENERATED_BODY()
public:
UCourseInfo();
FCourseInfoOnChangedSignature OnChanged;
// 외부에서 학사 정보를 변경할 때
void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);
private:
FString Contents; // 학사 정보 내용
};
새로운 학사 정보가 들어오면 변경되도록 ChangeCourseInfo
함수를 구현해준다.
그리고 OnChanged
라는 Multi-cast 델리게이트에서 바인딩된 모든 함수를 실행하도록 한다.
// 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); // OnChanged에 연결된 모든 함수에 브로드캐스트한다.
}
이번에는 Student
클래스를 수정해본다.
학사 알림 델리게이트의 구독자로서, 똑같은 형식을 가지는 함수를 선언해줘야 한다.
// Student.h
public:
void GetNotification(const FString& School, const FString& NewCourseInfo);
// Student.cpp
void UStudent::GetNotification(const FString& School, const FString& NewCourseInfo)
{
UE_LOG(LogTemp, Log, TEXT("[Student] %s 님이 %s 로부터 받은 메시지: %s."), *Name, *School, *NewCourseInfo);
}
지금까지의 과정을 돌아보면, Student
, CourseInfo
클래스는 어디에도 서로에 대한 헤더를 포함하지 않고 있다.
⇒ 각 클래스는 개별적으로 자신이 해야 할 작업에만 집중할 수 있다는 장점을 발휘 중.
중간에서 이를 중재하는 학교 역할을 하는 객체를 MyGameInstance
로 해주자.
학교 MyGameInstance
는 학사 시스템 CourseInfo
을 소유하도록 한다.
앞선 1_8강: 언리얼 C++ 설계2 - 컴포지션 강의에서 언리얼 오브젝트를 생성할 때 컴포지션 정보를 자동으로 구축할 수 있다 고 하였다. 이를 활용해보자. NewObject<UCourseInfo>()
에 this
를 매개변수로 전달함으로써, 생성되는 CourseInfo
객체의 아우터로 현재 객체 this
를 설정해줄 수 있다. 이로써 MyGameInstance
가 CourseInfo
를 가지는 컴포지션 관계가 설정된다.
// MyGameInstance.h
private:
UPROPERTY()
TObjectPtr<class UCourseInfo> CourseInfo;
// MyGameInstance.cpp
CourseInfo = NewObject<UCourseInfo>(this); // 컴포지션 관계
Student1
객체의 경우 해당 구문을 실행하면 자동으로 소멸되므로 굳이 Outer 를 설정해주지 않아도 괜찮다.
AddUObject
함수를 이용하면 Student
클래스 인스턴스와 멤버 변수를 묶어줄 수 있다.
이로 인해 구독을 위한 연결이 되었다.
마지막으로 ChangeCourseInfo
으로 변경된 학사 정보를 발행하도록 하자.
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "CourseInfo.h"
void UMyGameInstance::Init()
{
Super::Init();
CourseInfo = NewObject<UCourseInfo>(this); // CourseInfo의 Outer로 MyGameInstance를 설정해서, MyGameInstance가 CourseInfo를 가지는 컴포지션 관계를 설정한다.
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);
CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
UE_LOG(LogTemp, Log, TEXT("=========================================="));
}
데이터 기반의 디자인 패턴을 설계할 때 사용할 수 있는 델리게이트에 대해 배웠다.
1️⃣ 언리얼 델리게이트를 활용한 예제를 고안하고 직접 구현해보세요.
위에서 예시를 들었던 것처럼, Monster 클래스가 Hero(Tanker, Dealer) 에게 공격받을 때 체력(HP) 데이터가 깎이는 것을 로그로 출력해보겠습니다.
먼저 Monster 클래스를 만들어보겠습니다.
Monster를 공격할 수 있는 Hero 인스턴스는 Tanker, Dealer 총 2개이며,
인자는 Monster 의 체력을 나타내는 HP
, Monser를 공격한 Hero의 이름으로 총 두 개입니다.
이름 | 수 | |
---|---|---|
Monster를 공격할 수 있는 Hero | Tanker, Dealer | 2 |
인자 | HP, InName | 2 |
아래와 같은 델리게이트 매크로를 이용해서 FMonsterDamagedSignature
델리게이트를 선언했습니다.
// Monster.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FMonsterDamagedSignature, int32, const FString&);
// Monster.h
UCLASS()
class UEPARTONE_API UMonster : public UObject
{
GENERATED_BODY()
public:
UMonster();
FMonsterDamagedSignature OnDamaged;
void ChangeOnDamaged(int32 Damage, const FString& InName);
private:
int32 HP;
};
몬스터의 초기 체력을 설정해주고, OnDamaged
에 연결된 모든 함수에 브로드캐스트가 가능하도록 설정했습니다.
// Monster.cpp
UMonster::UMonster()
{
HP = 100;
UE_LOG(LogTemp, Log, TEXT("=========================================="));
UE_LOG(LogTemp, Log, TEXT("[Monster] %dHP 몬스터가 스폰되었습니다."), HP);
}
void UMonster::ChangeOnDamaged(int32 Damage, const FString& InName)
{
HP -= Damage;
UE_LOG(LogTemp, Log, TEXT("[Monster] 몬스터가 %s 로부터 %d데미지을 받아서 %dHP가 되었습니다."), *InName, Damage, HP);
OnDamaged.Broadcast(Damage, InName); // OnDamaged에 연결된 모든 함수에 브로드캐스트한다.
}
다음으로 Hero 클래스와 이를 상속받는 Tanker, Dealer 클래스입니다.
// Hero.h
UCLASS()
class UEPARTONE_API UHero : public UObject
{
GENERATED_BODY()
public:
UHero();
void GetNotificationOnAttack(int32 Damages, const FString& InNames);
FORCEINLINE const FString& GetName() const { return Name; }
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
FORCEINLINE int32 GetDamage() { return Damage; }
FORCEINLINE void SetDamage(int32 InDamage) { Damage = InDamage; }
protected:
UPROPERTY()
FString Name;
UPROPERTY()
int32 Damage;
};
Monster HP알림 델리게이트의 구독자로서, 똑같은 형식을 가지는 함수를 선언해줍니다.
// Hero.cpp
void UHero::GetNotificationOnAttack(int32 Damages, const FString& InNames)
{
UE_LOG(LogTemp, Log, TEXT("[Hero %s] Monster 가 %d 의 데미지를 받았습니다."), *Name, Damages);
}
탱커의 이름을 홍탱커로, 탱커의 공격력을 5로 정해줍니다.
// Tanker.cpp
UTanker::UTanker()
{
Name = TEXT("홍탱커");
Damage = 5;
}
딜러의 이름을 홍딜러로, 딜러의 공격력을 10으로 정해줍니다.
// Dealer.cpp
UDealer::UDealer()
{
Name = TEXT("홍딜러");
Damage = 10;
}
Monster, Hero 가 존재하는 게임의 필드는 MyGameInstance입니다.
// MyGameInstance.h
UPROPERTY()
TObjectPtr<class UMonster> Monster;
OnDamaged
델리게이트에 이벤트를 할당해줍니다.
void UMyGameInstance::Init()
{
Super::Init();
UTanker* Tanker = NewObject<UTanker>();
UDealer* Dealer = NewObject<UDealer>();
Monster = NewObject<UMonster>();
Monster->OnDamaged.AddUObject(Tanker, &UHero::GetNotificationOnAttack);
Monster->OnDamaged.AddUObject(Dealer, &UHero::GetNotificationOnAttack);
TArray<UHero*> Heroes = { Tanker, Dealer };
for (auto Hero : Heroes)
{
UE_LOG(LogTemp, Log, TEXT("=========================================="));
Monster->ChangeOnDamaged(Hero->GetDamage(), Hero->GetName());
}
UE_LOG(LogTemp, Log, TEXT("=========================================="));
}
홍탱커, 홍딜러가 Monster 를 각각 공격해서 HP 데이터가 5, 10 만큼 감소되는 것이 출력되었습니다.
또한 ChangeOnDamaged
, GetNotificationOnAttack
으로 홍탱커, 홍딜러에게 메시지가 각각 출력되는 것을 확인할 수 있습니다.