Skip to content

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 Delegate1개불가불가내부 콜백, 팩토리 패턴
Multicast DelegateN개불가불가이벤트 브로드캐스트
Dynamic DelegateN개 (Multicast) / 1개 (Single)가능가능Blueprint 연동 이벤트

하나의 함수만 바인딩할 수 있습니다. 가장 가볍고 빠릅니다. Execute()로 호출합니다.

여러 함수를 동시에 바인딩할 수 있습니다. Broadcast()로 등록된 모든 함수를 호출합니다. 반환값이 없어야 합니다.

이름 기반 바인딩 방식을 사용하며 직렬화가 가능합니다. Blueprint에서 이벤트로 노출할 수 있습니다. 런타임 오버헤드가 약간 있습니다.


UE 델리게이트를 선언할 때는 전처리 매크로를 사용합니다. 매크로 이름이 유형과 파라미터 수를 나타냅니다.

// ---- Single Delegate ----
DECLARE_DELEGATE(FOnSimpleEvent) // 파라미터 없음
DECLARE_DELEGATE_OneParam(FOnDamageDealt, float) // 파라미터 1개: float
DECLARE_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까지)

MyCharacter.h
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();
};
MyCharacter.cpp
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..."));
}

Dynamic Delegate는 AddDynamic / RemoveDynamic 매크로를 사용합니다.

PickupItem.h
#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());
}
// 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());
// 리스폰 로직, 점수 처리 등
}

함수대상GC 안전비고
BindUObject / AddUObjectUObject 멤버 함수O가장 권장
BindRaw / AddRaw일반 C++ 객체 멤버 함수X객체 수명 직접 관리 필요
BindLambda / AddLambda람다캡처 주의UObject 캡처 시 TWeakObjectPtr 사용 권장
BindStatic / AddStatic정적 함수O상태 없는 함수에 적합
BindSP / AddSPTSharedPtr 객체 멤버 함수O (SharedPtr 기준)비 UObject에 안전
AddDynamicUObject 멤버 함수 (Dynamic만)OUFUNCTION 필수

여러 시스템(게임모드, UI, 사운드, AI)이 캐릭터 사망을 동시에 감지해야 할 때 Multicast를 사용합니다.

HealthComponent.h
#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();
};
HealthComponent.cpp
#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++ 로직에 연결합니다.

PauseMenuWidget.h
#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
  • AddDynamic을 사용하는 함수에는 반드시 UFUNCTION() 매크로가 필요합니다.
  • BindRaw는 비 UObject 객체에만 사용하며, 객체 수명을 직접 보장해야 합니다.
  • 람다에서 UObject를 캡처할 때는 TWeakObjectPtr로 감싸서 사용합니다.
  • Broadcast() 도중 델리게이트 목록이 변경되는 경우(내부에서 Remove 호출 등)에 대비해 IsBound() 체크를 습관화합니다.