UE5 C++ 델리게이트 완전 가이드
개요 — 델리게이트란 무엇인가
Section titled “개요 — 델리게이트란 무엇인가”델리게이트(Delegate)는 함수를 변수처럼 저장하고 나중에 호출할 수 있는 타입 안전(type-safe) 함수 포인터입니다. 언리얼 엔진 C++에서 델리게이트는 이벤트 시스템의 핵심을 이루며, UI 버튼 클릭, 사망 이벤트, 체력 변경 알림 등 다양한 게임플레이 이벤트를 느슨하게 결합(loose coupling)하는 데 사용됩니다.
일반 C++ 함수 포인터나 std::function과 달리 UE 델리게이트는 다음을 제공합니다.
| 특징 | 설명 |
|---|---|
| 타입 안전성 | 시그니처가 맞지 않으면 컴파일 오류 발생 |
| UObject 통합 | GC와 연동되어 소멸된 객체에 대한 안전한 처리 |
| Blueprint 연동 | Dynamic Delegate는 Blueprint에서 구독 가능 |
| 멀티캐스트 지원 | 여러 함수를 동시에 호출하는 브로드캐스트 패턴 |
1. 델리게이트 세 가지 유형 비교
Section titled “1. 델리게이트 세 가지 유형 비교”언리얼 엔진의 델리게이트는 크게 세 가지로 나뉩니다.
| 유형 | 바인딩 수 | Blueprint 노출 | 직렬화 | 대표 사용처 |
|---|---|---|---|---|
| Single Delegate | 1개 | 불가 | 불가 | 내부 콜백, 팩토리 패턴 |
| Multicast Delegate | N개 | 불가 | 불가 | 이벤트 브로드캐스트 |
| Dynamic Delegate | N개 (Multicast) / 1개 (Single) | 가능 | 가능 | Blueprint 연동 이벤트 |
1.1 Single Delegate
Section titled “1.1 Single Delegate”하나의 함수만 바인딩할 수 있습니다. 가장 가볍고 빠릅니다. Execute()로 호출합니다.
1.2 Multicast Delegate
Section titled “1.2 Multicast Delegate”여러 함수를 동시에 바인딩할 수 있습니다. Broadcast()로 등록된 모든 함수를 호출합니다. 반환값이 없어야 합니다.
1.3 Dynamic Delegate
Section titled “1.3 Dynamic Delegate”이름 기반 바인딩 방식을 사용하며 직렬화가 가능합니다. Blueprint에서 이벤트로 노출할 수 있습니다. 런타임 오버헤드가 약간 있습니다.
2. DECLARE_DELEGATE 매크로 계열
Section titled “2. DECLARE_DELEGATE 매크로 계열”UE 델리게이트를 선언할 때는 전처리 매크로를 사용합니다. 매크로 이름이 유형과 파라미터 수를 나타냅니다.
// ---- Single Delegate ----DECLARE_DELEGATE(FOnSimpleEvent) // 파라미터 없음DECLARE_DELEGATE_OneParam(FOnDamageDealt, float) // 파라미터 1개: floatDECLARE_DELEGATE_TwoParams(FOnActorHit, AActor*, FVector) // 파라미터 2개DECLARE_DELEGATE_RetVal(bool, FOnCanPickUp) // 반환값 있는 Single
// ---- Multicast Delegate ----DECLARE_MULTICAST_DELEGATE(FOnGameStarted)DECLARE_MULTICAST_DELEGATE_OneParam(FOnScoreChanged, int32)DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, float) // NewHP, MaxHP
// ---- Dynamic Multicast (Blueprint 노출 가능, UPROPERTY와 함께 사용) ----DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPlayerDied)DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnItemPickedUp, APickupItem*, Item)DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAttributeChanged, float, NewValue, float, MaxValue)
// ---- Dynamic Single (저장 및 직렬화 가능) ----DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(bool, FOnQueryTarget, AActor*, Target)매크로 이름 규칙을 정리하면 다음과 같습니다.
DECLARE_ [DYNAMIC_] [MULTICAST_] DELEGATE [_RetVal] [_OneParam | _TwoParams | ...]DYNAMIC: 이름 기반 바인딩, Blueprint 노출 및 직렬화 지원MULTICAST: 여러 바인딩 허용, 반환값 불가_RetVal: 반환값 지원 (Single만 가능)_OneParam,_TwoParams…: 파라미터 수 (최대_NineParams까지)
3. 바인딩 방법
Section titled “3. 바인딩 방법”3.1 Single/Multicast Delegate 바인딩
Section titled “3.1 Single/Multicast Delegate 바인딩”UCLASS()class MYGAME_API AMyCharacter : public ACharacter{ GENERATED_BODY()
public: // Multicast — 외부에서 구독 가능하도록 public DECLARE_MULTICAST_DELEGATE_OneParam(FOnDeathSignature, AMyCharacter*); FOnDeathSignature OnDeath;
private: // Single — 내부 콜백용 DECLARE_DELEGATE(FOnRespawnReady); FOnRespawnReady OnRespawnReady;
void Die(); void HandleRespawn();};void AMyCharacter::BeginPlay(){ Super::BeginPlay();
// ---- BindUObject: UObject 멤버 함수 바인딩 (GC 안전) ---- // Single Delegate OnRespawnReady.BindUObject(this, &AMyCharacter::HandleRespawn);
// ---- BindRaw: 일반 C++ 객체 바인딩 (GC 미관리, 주의 필요) ---- // FMyHelper* Helper = new FMyHelper(); // OnRespawnReady.BindRaw(Helper, &FMyHelper::OnReady);
// ---- BindLambda: 람다 바인딩 ---- OnRespawnReady.BindLambda([]() { UE_LOG(LogTemp, Log, TEXT("Respawn is ready!")); });
// ---- BindStatic: 정적(static) 함수 바인딩 ---- // OnRespawnReady.BindStatic(&UMyLibrary::StaticCallback);}
void AMyCharacter::Die(){ // Multicast: AddUObject로 여러 구독자 추가 가능 // (외부에서 구독하는 예시는 3.3절 참고)
// Multicast Broadcast OnDeath.Broadcast(this);
// Single Execute (바인딩된 경우에만) if (OnRespawnReady.IsBound()) { OnRespawnReady.Execute(); }}
void AMyCharacter::HandleRespawn(){ UE_LOG(LogTemp, Log, TEXT("Character respawning..."));}3.2 Dynamic Multicast Delegate 바인딩
Section titled “3.2 Dynamic Multicast Delegate 바인딩”Dynamic Delegate는 AddDynamic / RemoveDynamic 매크로를 사용합니다.
#pragma once
#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "PickupItem.generated.h"
// 헤더 최상위(클래스 외부)에 선언DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnItemPickedUp, APickupItem*, PickedItem);
UCLASS(BlueprintType, Blueprintable)class MYGAME_API APickupItem : public AActor{ GENERATED_BODY()
public: // BlueprintAssignable: Blueprint에서 이 이벤트를 구독 가능 UPROPERTY(BlueprintAssignable, Category = "Pickup") FOnItemPickedUp OnItemPickedUp;
UFUNCTION(BlueprintCallable, Category = "Pickup") void PickUp(AActor* Picker);};// PlayerCharacter.cpp — 외부에서 이벤트 구독void APlayerCharacter::BeginPlay(){ Super::BeginPlay();
// 월드의 모든 APickupItem을 찾아 이벤트 구독 TArray<AActor*> FoundItems; UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickupItem::StaticClass(), FoundItems);
for (AActor* Actor : FoundItems) { if (APickupItem* Item = Cast<APickupItem>(Actor)) { // AddDynamic: 함수 이름을 문자열로 등록 (리플렉션 사용) Item->OnItemPickedUp.AddDynamic(this, &APlayerCharacter::OnItemPickedUp); } }}
void APlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason){ // RemoveDynamic: 구독 해제 — EndPlay에서 반드시 정리 // Dynamic Delegate는 UObject 소멸 시 자동 해제되지 않으므로 명시적 해제 권장 // (엔진이 내부적으로 처리하지만 명시적 해제가 안전함) Super::EndPlay(EndPlayReason);}
// UFUNCTION 매크로 필수 — AddDynamic 내부 리플렉션이 함수를 찾아야 함UFUNCTION()void APlayerCharacter::OnItemPickedUp(APickupItem* PickedItem){ UE_LOG(LogTemp, Log, TEXT("Player picked up: %s"), *PickedItem->GetName());}3.3 외부에서 Multicast 구독
Section titled “3.3 외부에서 Multicast 구독”// GameModeBase.cpp — 캐릭터의 사망 이벤트를 구독하는 예시void AMyGameMode::OnPlayerCharacterSpawned(AMyCharacter* Character){ if (Character) { // AddUObject로 구독 — 핸들을 저장해 나중에 개별 해제 가능 Character->OnDeath.AddUObject(this, &AMyGameMode::HandlePlayerDeath); }}
void AMyGameMode::HandlePlayerDeath(AMyCharacter* DeadCharacter){ UE_LOG(LogTemp, Warning, TEXT("Player died: %s"), *DeadCharacter->GetName()); // 리스폰 로직, 점수 처리 등}4. 바인딩 함수 비교표
Section titled “4. 바인딩 함수 비교표”| 함수 | 대상 | GC 안전 | 비고 |
|---|---|---|---|
BindUObject / AddUObject | UObject 멤버 함수 | O | 가장 권장 |
BindRaw / AddRaw | 일반 C++ 객체 멤버 함수 | X | 객체 수명 직접 관리 필요 |
BindLambda / AddLambda | 람다 | 캡처 주의 | UObject 캡처 시 TWeakObjectPtr 사용 권장 |
BindStatic / AddStatic | 정적 함수 | O | 상태 없는 함수에 적합 |
BindSP / AddSP | TSharedPtr 객체 멤버 함수 | O (SharedPtr 기준) | 비 UObject에 안전 |
AddDynamic | UObject 멤버 함수 (Dynamic만) | O | UFUNCTION 필수 |
5. 실전 이벤트 설계 패턴
Section titled “5. 실전 이벤트 설계 패턴”5.1 사망 이벤트 — Multicast 패턴
Section titled “5.1 사망 이벤트 — Multicast 패턴”여러 시스템(게임모드, UI, 사운드, AI)이 캐릭터 사망을 동시에 감지해야 할 때 Multicast를 사용합니다.
#pragma once
#include "CoreMinimal.h"#include "Components/ActorComponent.h"#include "HealthComponent.generated.h"
DECLARE_MULTICAST_DELEGATE_OneParam(FOnDeathSignature, AActor* /*DeadActor*/);DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHealthChangedSignature, float /*NewHealth*/, float /*MaxHealth*/);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))class MYGAME_API UHealthComponent : public UActorComponent{ GENERATED_BODY()
public: UHealthComponent();
// 외부 구독용 — C++ 전용 이벤트 FOnDeathSignature OnDeath; FOnHealthChangedSignature OnHealthChanged;
// Blueprint 노출용 이벤트 UPROPERTY(BlueprintAssignable, Category = "Health") FOnItemPickedUp OnDeathBP; // Dynamic 버전을 별도로 선언해 사용
UFUNCTION(BlueprintCallable, Category = "Health") void ApplyDamage(float DamageAmount);
UFUNCTION(BlueprintPure, Category = "Health") float GetHealthPercent() const { return MaxHealth > 0.f ? CurrentHealth / MaxHealth : 0.f; }
UFUNCTION(BlueprintPure, Category = "Health") bool IsAlive() const { return CurrentHealth > 0.f; }
protected: virtual void BeginPlay() override;
private: UPROPERTY(EditDefaultsOnly, Category = "Health") float MaxHealth = 100.f;
UPROPERTY(VisibleAnywhere, Category = "Health") float CurrentHealth = 0.f;
bool bIsDead = false;
void HandleDeath();};#include "HealthComponent.h"
UHealthComponent::UHealthComponent(){ PrimaryComponentTick.bCanEverTick = false;}
void UHealthComponent::BeginPlay(){ Super::BeginPlay(); CurrentHealth = MaxHealth;}
void UHealthComponent::ApplyDamage(float DamageAmount){ if (bIsDead || DamageAmount <= 0.f) { return; }
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.f, MaxHealth);
// 체력 변경 이벤트 브로드캐스트 OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
if (CurrentHealth <= 0.f) { HandleDeath(); }}
void UHealthComponent::HandleDeath(){ if (bIsDead) { return; }
bIsDead = true;
// 사망 이벤트 — 구독한 모든 시스템에 알림 OnDeath.Broadcast(GetOwner());}5.2 UI 버튼 이벤트 — Dynamic Delegate 패턴
Section titled “5.2 UI 버튼 이벤트 — Dynamic Delegate 패턴”UI 위젯에서 발생하는 버튼 클릭 이벤트를 C++ 로직에 연결합니다.
#pragma once
#include "CoreMinimal.h"#include "Blueprint/UserWidget.h"#include "PauseMenuWidget.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnResumeRequested);DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnQuitRequested);
UCLASS()class MYGAME_API UPauseMenuWidget : public UUserWidget{ GENERATED_BODY()
public: // Blueprint에서 버튼에 연결한 클릭 이벤트를 C++로 전달 UPROPERTY(BlueprintAssignable, Category = "UI|Events") FOnResumeRequested OnResumeRequested;
UPROPERTY(BlueprintAssignable, Category = "UI|Events") FOnQuitRequested OnQuitRequested;
protected: // Blueprint에서 버튼 OnClicked에 연결 UFUNCTION(BlueprintCallable, Category = "UI") void HandleResumeClicked();
UFUNCTION(BlueprintCallable, Category = "UI") void HandleQuitClicked();};// PlayerController.cpp — 메뉴 이벤트 구독void AMyPlayerController::OpenPauseMenu(){ if (!PauseMenuWidget) { PauseMenuWidget = CreateWidget<UPauseMenuWidget>(this, PauseMenuWidgetClass); }
if (PauseMenuWidget) { PauseMenuWidget->AddToViewport();
// Dynamic Delegate로 UI 이벤트를 컨트롤러 함수에 연결 PauseMenuWidget->OnResumeRequested.AddDynamic(this, &AMyPlayerController::ResumeGame); PauseMenuWidget->OnQuitRequested.AddDynamic(this, &AMyPlayerController::QuitGame); }}
UFUNCTION()void AMyPlayerController::ResumeGame(){ if (PauseMenuWidget) { PauseMenuWidget->RemoveFromParent(); } SetPause(false);}
UFUNCTION()void AMyPlayerController::QuitGame(){ UKismetSystemLibrary::QuitGame(GetWorld(), this, EQuitPreference::Quit, false);}5.3 람다 바인딩 시 안전한 UObject 캡처
Section titled “5.3 람다 바인딩 시 안전한 UObject 캡처”람다에서 UObject를 캡처할 때 this를 직접 캡처하면 객체가 소멸된 후 람다가 실행될 때 크래시가 발생할 수 있습니다.
// 위험한 패턴 — this 직접 캡처void AMyActor::SetupCallback(){ SomeDelegate.BindLambda([this]() { // this가 소멸되면 크래시 DoSomething(); });}
// 안전한 패턴 — TWeakObjectPtr로 약한 참조 캡처void AMyActor::SetupSafeCallback(){ TWeakObjectPtr<AMyActor> WeakThis(this);
SomeDelegate.BindLambda([WeakThis]() { if (WeakThis.IsValid()) { WeakThis->DoSomething(); } });}| 상황 | 권장 델리게이트 유형 |
|---|---|
| 단일 내부 콜백 | DECLARE_DELEGATE (Single) |
| 여러 시스템이 구독하는 C++ 이벤트 | DECLARE_MULTICAST_DELEGATE |
| Blueprint에서 구독/발행 모두 필요 | DECLARE_DYNAMIC_MULTICAST_DELEGATE + UPROPERTY(BlueprintAssignable) |
| 직렬화 또는 저장이 필요한 바인딩 | DECLARE_DYNAMIC_DELEGATE |
핵심 규칙 요약
Section titled “핵심 규칙 요약”AddDynamic을 사용하는 함수에는 반드시UFUNCTION()매크로가 필요합니다.BindRaw는 비 UObject 객체에만 사용하며, 객체 수명을 직접 보장해야 합니다.- 람다에서
UObject를 캡처할 때는TWeakObjectPtr로 감싸서 사용합니다. Broadcast()도중 델리게이트 목록이 변경되는 경우(내부에서Remove호출 등)에 대비해IsBound()체크를 습관화합니다.