
언리얼 엔진에서 Delegate는 이벤트를 처리하는 방법입니다. 이번 글에서는 TBaseStaticDelegateInstance와 FDefaultDelegateUserPolicy를 통해 Delegate의 작동 방식을 심도 있게 다루고 델리게이트의 다양한 사용 방법을 알아보겠습니다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDelegateSignature, const DelegateInfo&, Info);
FDelegateSignature InfoDelegate;
InfoDelegate.Broadcast(Info);이것은 하나의 값의 변화에 해당하는 델리게이트를 등록하고 이 값의 변화를 감지하고 있습니다. 그런데 만약 델리게이트가 두 개, 세 개 ... 몇 만개가 된다면 어떻게 될까요?
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDelegateSignature, const DelegateInfo&, Info);
FDelegateSignature InfoDelegate;
InfoDelegate.Broadcast(Info);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDelegateSignature2, const DelegateInfo2&, Info2);
FDelegateSignature2 InfoDelegate2;
InfoDelegate2.Broadcast(Info2);
...
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDelegateSignature10000, const DelegateInfo10000&, Info10000);
FDelegateSignature10000 InfoDelegate10000;
InfoDelegate10000.Broadcast(Info10000);
저 3줄짜리 코드를 계속해서 써야한다면 굉장히 괴로울 것입니다. 어떻게 하면 쉽게 만들 수 있을까요? 저 수많은 델리게이트를 쓰기 편하게 만드는 방법이 무엇일까요?
그 해결의 실마리를 Signature의 부모 클래스인 TBaseDynamicMulticastDelegate에 찾아가서 코드를 살펴보면 발견할 수 있습니다.
결론부터 말하자면 TMap을 사용해서 Signature에 해당하는 함수의 포인터를 매핑하면 됩니다.
언리얼 엔진의 Delegate는 사용자 정의 정책을 통해 확장성과 커스터마이징이 가능합니다. 이 정책은 FDefaultDelegateUserPolicy 구조로 정의되며, 세 가지 주요 클래스를 포함합니다:
FDefaultDelegateUserPolicy 구조struct FDefaultDelegateUserPolicy
{
using FDelegateInstanceExtras = IDelegateInstance;
using FThreadSafetyMode = FNotThreadSafeDelegateMode;
using FDelegateExtras = TDelegateBase<FThreadSafetyMode>;
using FMulticastDelegateExtras = TMulticastDelegateBase<FDefaultDelegateUserPolicy>;
};
FDelegateInstanceExtras:
IDelegateInstance를 상속받아 Delegate 인스턴스에 추가 데이터를 저장하거나 바인딩된 객체를 관리합니다.FDelegateExtras:
GetDelegateInstanceProtected() 메서드를 사용합니다.FMulticastDelegateExtras:
이 구조는 Delegate에 직접 통합되어 커스터마이징 가능한 Delegate 동작을 제공합니다. 이를 통해 Delegate 호출 시 쓰레드 안전성이나 객체 관리 방식을 제어할 수 있습니다.
TBaseStaticDelegateInstance는 정적 함수 포인터를 Delegate로 사용할 수 있도록 설계된 클래스입니다. 이 클래스는 Delegate 바인딩과 실행을 효율적으로 처리합니다.
잘 보면 이 부분이 보이는데요. 함수의 포인터를 사용하는 예시를 보여주고 있습니다. 그렇다면 함수 포인터를 TMap에 매핑해서 저장해놓는다면 이 TMap을 단순히 for문으로 돌려주기만하면 몇만줄의 코드를 작성하지 않아도 되겠죠.
using FFuncPtr = RetValType(*)(ParamTypes..., VarTypes...);
template <typename RetValType, typename... ParamTypes, typename UserPolicy, typename... VarTypes>
class TBaseStaticDelegateInstance : public TCommonDelegateInstanceState<RetValType(ParamTypes...), UserPolicy, VarTypes...>
{
using FFuncPtr = RetValType(*)(ParamTypes..., VarTypes...);
explicit TBaseStaticDelegateInstance(FFuncPtr InStaticFuncPtr, InVarTypes&&... Vars)
: Super(Forward<InVarTypes>(Vars)...), StaticFuncPtr(InStaticFuncPtr)
{
check(StaticFuncPtr != nullptr);
}
RetValType Execute(ParamTypes... Params) const final
{
checkSlow(StaticFuncPtr != nullptr);
return this->Payload.ApplyAfter(StaticFuncPtr, Forward<ParamTypes>(Params)...);
}
bool ExecuteIfSafe(ParamTypes... Params) const final
{
checkSlow(StaticFuncPtr != nullptr);
(void)this->Payload.ApplyAfter(StaticFuncPtr, Forward<ParamTypes>(Params)...);
return true;
}
};
FFuncPtr:
RetValType(*)(ParamTypes..., VarTypes...) 형식으로 정의됩니다.Execute:
Payload.ApplyAfter를 사용하여 바인딩된 데이터와 함께 함수를 실행합니다.ExecuteIfSafe:
생성자:
InStaticFuncPtr를 통해 Delegate가 호출할 정적 함수를 전달받습니다.Vars는 추가적으로 Delegate 실행에 필요한 데이터를 캡처합니다.TBaseStaticDelegateInstance를 사용하여 정적 함수를 Delegate로 바인딩하는 예제는 다음과 같습니다:
float StaticFunctionExample(float Value, int32 Multiplier)
{
return Value * Multiplier;
}
void BindStaticFunction()
{
TBaseStaticDelegateInstance<float(float, int32), FDefaultDelegateUserPolicy>::FFuncPtr StaticFunc = &StaticFunctionExample;
TBaseStaticDelegateInstance<float(float, int32), FDefaultDelegateUserPolicy> DelegateInstance(StaticFunc);
// Delegate 실행
float Result = DelegateInstance.Execute(5.0f, 10);
UE_LOG(LogTemp, Log, TEXT("Result: %f"), Result); // 출력: Result: 50.0
}
Gameplay Attribute를 TBaseStaticDelegateInstance와 결합하여 값 변화를 처리하는 예제입니다. 다음처럼 사용하면 TMap으로 매핑하여 손쉽게 사용이 가능합니다.
template<class T>
using TStaticFuncPtr = typename TBaseStaticDelegateInstance<T, FDefaultDelegateUserPolicy>::FFuncPtr;
class AURA_API UAuraAttributeSet : public UAttributeSet
{
TMap<FGameplayTag, TStaticFuncPtr<FGameplayAttribute()>> TagsToAttributes;
}
// Attributeset의 초기화 함수
void UAuraAttributeSet::UAuraAttributeSet()
{
AttributeDelegates.Add(GameplayTag_Health, &UAuraAttributeSet::GetHealthAttribute);
AttributeDelegates.Add(GameplayTag_Strength, &UAuraAttributeSet::GetStrengthAttribute);
}
// 컨트롤러 내부의 함수
// FindAttributeInfoForTag는 for문을 돌면서 GameplayTag에 존재하는 값과 매개변수 값이 일치하면 해당 정보를 리턴함. 아니면 비어있는 값 리턴.
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
FDelegateInfo Info = AttributeInfo->FindAttributeInfoForTag(Pair.Key);
Info.AttributeValue = Pair.Value().GetNumericValue(AS);
AttributeInfoDelegate.Broadcast(Info);
}
}
또는
TMap<FGameplayTag, TBaseStaticDelegateInstance<FGameplayAttribute()>::FFuncPtr> AttributeDelegates;
// Attributeset의 초기화 함수
void UAuraAttributeSet::UAuraAttributeSet()
{
AttributeDelegates.Add(GameplayTag_Health, &UAuraAttributeSet::GetHealthAttribute);
AttributeDelegates.Add(GameplayTag_Strength, &UAuraAttributeSet::GetStrengthAttribute);
}
// 컨트롤러 내부의 함수
// FindAttributeInfoForTag는 for문을 돌면서 GameplayTag에 존재하는 값과 매개변수 값이 일치하면 해당 정보를 리턴함. 아니면 비어있는 값 리턴.
void UAttributeMenuWidgetController::BroadcastInitialValues()
{
UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
check(AttributeInfo);
for (auto& Pair : AS->TagsToAttributes)
{
FDelegateInfo Info = AttributeInfo->FindAttributeInfoForTag(Pair.Key);
Info.AttributeValue = Pair.Value().GetNumericValue(AS);
AttributeInfoDelegate.Broadcast(Info);
}
}
TBaseStaticDelegateInstance는 Delegate를 TMap에 저장하여 매핑하게 만들 수 있는 실마리였습니다. 이를 활용하여 GAS와 Delegate를 결합하면 더 효율적이고 유지보수 가능한 코드를 작성할 수 있습니다.