[UE5] Delegate(델리게이트)
최근 팀원과 프로젝트를 진행하던 중, 기능과 UI가 추가되면서 점점 주요 기능을 담당하는 클래스가 다른 클래스의 참조를 요구하는 경우가 늘어나고 다른 클래스의 로직 또한 전부 주요 클래스에 구현해 버리는 문제가 생겼다.
이를 해결하기 위한 디자인 패턴을 찾던 중 Delegate에 대해 알아보게 되어 이에 대해 포스팅 하려한다.
Delegate?
Delegate 패턴은 기능을 위임하는 Delegate를 지정하는 디자인 패턴으로 두 클래스 간 의존성을 줄이고, 기능 추가에 따른 코드 변경이 적어지며, 언리얼엔진에서는 이벤트 기반의 구조 설계까지 가능하게 하는 디자인 패턴이다.
언리얼 엔진에서의 Delegate 사용법을 예시, 코드와 함께 알아보겠다.
예시) 캐릭터의 적에게 타격받아 체력이 0이 되면 캐릭터가 죽는 애니메이션 재생, 적 캐릭터의 점수 증가, 점수현황 업데이트 등의 기능구현이 필요한 상황
Delegate를 사용하지 않고 이를 구현하려면 가장 단순하게는 Tick에서 HP를 체크하고 0이 되면 캐릭터의 체력을 관리하는 클래스 내부에서 캐릭터, 애니메이션, 점수판, ..., 앞으로 추가될 각종 상황에 맞는 클래스의 참조에서 각 로직을 모두 구현해야 한다.
캐릭터의 체력을 관리하는 클래스 내부에 캐릭터, 점수판, 애니메이션에서 이루어지는 로직이 작성되는 형태로 이는 클래스 설계 원칙에 좋은 방법이 아니다.
이 많은 기능을 위임받을 수 있는 Delegate의 선언이 필요하다.
Delegate 사용법
언리얼 엔진에서는 Delegate를 위한 매크로를 제공하고 있다.
Delegate의 선언은 아래와 같이 함수 시그니처에 따라 정해진 선언 매크로를 사용하면 된다.
파라미터가 없고 반환값이 없는 함수 시그니처의 Delegate를 선언하기로 하자.
언리얼 엔진에서는 여러 종류의 Delegate를 지원한다.
* Single-Cast: 함수 1개를 Bind 할 수 있는 Delegate
* Multi-Cast: 여러 함수를 Bind 할 수 있는 Delegate
* Dynamic: Serialize 되어 블루프린트 객체와도 Bind가 가능한 Delegate (Single, Multi와 같이 적용가능, ex. Dynamic Multi Cast, Dynamic Single Cast)
예시 상황에서는 여러 함수와의 Binding이 필요하기 때문에 Multi-Cast Delegate가 필요하다.
Dynamic Delegate의 경우 속도가 일반 Delegate보다 조금 느리며, 블루프린트 객체와도 Binding이 없기 때문에 사용할 Delegate의 종류는 Multi-Cast Delegate로 정해진다.
언리얼 엔진에서는 Delegate 시그니처의 prefix로 "F"를 사용하여야 한다.
//UPlayerHP.h - pseudo code
DECLARE_MULTICAST_DELEGATE(FNoHPDelegate);
UCLASS()
class UPlayerHP : public UActorComponent
{
GENERATED_BODY()
public:
FNoHPDelegate NoHPDelegate;
}
이제 각 클래스의 함수와 Delegate를 Bind할 차례다.
Single-Cast, Dynamic Single-Cast, Multi-Cast, Dynamic Multi-Cast에 따라 Bind하는 Delegate의 method 이름이 조금씩 다르다.
예시인 Multi-Cast는 AddUObject, AddRaw, AddLambda 등을 사용한다. (Bind 대상의 속성에 따라, Lambda식 사용에 따라 다름)
// APlayerCharacter.cpp - pseudo code
{
PlayerHP->NoHPDelegate.AddLambda([this]()->void{
IsDead = true;
})
}
// ...
// UCombatAnimation.cpp - pseudo code
{
PlayerHP->NoHPDelegate.AddLambda([this]()->void{
PlayDieAnimationMontage();
})
}
// ...
AddLambda를 사용하여 간단하게 함수를 Binding 할 수 있다.
마지막으로 Delegate를 Call하면 Binding된 함수들이 실행된다
Single Delegate에서는 .ExecuteIfBound()
Multi Delegate에서는 .Broadcast()를 통해 Delegate를 Call 할 수 있다.
// UPlayerHP.cpp - pseudo code
void UplayerHP::ReduceHP(float damage)
{
CHP = CHP - damage;
if (CHP < 0.0f)
{
NoHPDelegate.Broadcast();
}
}
Delegate를 실행하면 subscribe(Bind)중인 함수들이 모두 실행된다.
실행 순서는 따로 정의되어 있지 않아 함수가 Binding된 순서대로 실행되지 않기 때문에 이를 유의하여야 한다.
Event는 Multi-cast delegate와 매우 비슷하나 선언이 이루어진 클래스 내에서만 Event의 Broadcast(), IsBound(), Clear() invoke 할 수 있다는 것이 다른 점이다.
Reference
[1] https://benui.ca/unreal/delegates-advanced/