ue-delegates-and-events

Wires up callbacks and events in Unreal Engine so different parts of your game can talk to each other and react to changes.

Installation
Run `npx skills add "https://github.com/kevinpbuckley/unreal-engine-skills" --skill "ue-delegates-and-events"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: ue-delegates-and-events
description: Wire up callbacks and events in Unreal C++ using delegates — single-cast
  (DECLARE_DELEGATE, DECLARE_DELEGATE_RetVal, payload variables), multicast
  (DECLARE_MULTICAST_DELEGATE, DECLARE_TS_MULTICAST_DELEGATE), and dynamic
  (DECLARE_DYNAMIC_MULTICAST_DELEGATE, BlueprintAssignable, AddDynamic, RemoveDynamic).
  Covers all binding forms (BindUObject, AddUObject, BindLambda, AddWeakLambda,
  BindRaw, AddSP), execution (Execute, ExecuteIfBound, Broadcast), FDelegateHandle
  lifetime management, safe unbinding, and DECLARE_EVENT. Use when implementing the
  observer pattern, exposing C++ events to Blueprints, decoupling game systems,
  binding overlap/hit/ability callbacks, or debugging delegate crashes and silent no-ops.
metadata:
  engine-version: "5.8"
  category: cpp-foundations
---

# Delegates & events

Delegates are Unreal's type-safe function-pointer/observer system. There are three
families — pick the right one before writing any binding code, because they differ in
Blueprint visibility, binding API, and serialization capability.

## When to use this skill

- One object needs to notify others when something happens (observer pattern).
- Exposing a C++ event that Blueprints can subscribe to (`BlueprintAssignable`).
- Decoupling systems: broadcast an event instead of calling a known class directly.
- Binding to overlap/hit/ability completion callbacks that require `UFUNCTION`.
- Crashes or silent no-ops from bad binding types, destroyed objects, or wrong macros.

## The three families

| Family | Macro prefix | Listeners | Blueprint? | Return value? |
|---|---|---|---|---|
| Single-cast | `DECLARE_DELEGATE*` | exactly one | no | yes (`_RetVal`) |
| Multicast | `DECLARE_MULTICAST_DELEGATE*` | many | no | no |
| Dynamic multicast | `DECLARE_DYNAMIC_MULTICAST_DELEGATE*` | many | **yes** | no |

**The decisive rule:** if Blueprints need to subscribe, use dynamic multicast. For
C++-only events with many listeners use multicast. For a single required callback
(possibly with a return value) use single-cast. For a stored callable that isn't an
event at all, use `TFunction<Ret(Args...)>`.

Suffixes encode the signature: `_OneParam`, `_TwoParams`, `_RetVal_OneParam`, etc.
The delegate system supports up to **9 parameters** and up to **4 payload variables**
(non-dynamic only). Dynamic delegate params must be **named** in the macro.

## Declaring

```cpp
// C++-only multicast — no Blueprint access
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float /*NewHealth*/);

// Dynamic multicast — Blueprint-assignable, params must be named
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDiedSignature, AActor*, Killer);

// Single-cast with return value — only one binding, can return bool
DECLARE_DELEGATE_RetVal_OneParam(bool, FCanInteract, AActor* /*Instigator*/);
```

Declare at global scope, namespace, or class scope — **not** inside a function body.

Expose as class members with the correct UPROPERTY specifier:

```cpp
UCLASS()
class MYGAME_API UHealthComponent : public UActorComponent
{
    GENERATED_BODY()
public:
    // C++-only: no UPROPERTY needed; bind from C++ with AddUObject/AddLambda
    FOnHealthChanged OnHealthChanged;

    // Blueprint-assignable: MUST be UPROPERTY(BlueprintAssignable)
    UPROPERTY(BlueprintAssignable, Category="Health")
    FOnDiedSignature OnDied;
};
```

## Binding

### C++-only delegates (single-cast and multicast)

```cpp
// Single-cast: bind to a UObject member function (weak ref — safe if object dies)
Delegate.BindUObject(this, &AMyClass::MyMethod);

// Single-cast: bind a lambda with no lifetime guard (unbind manually before capture dies)
Delegate.BindLambda([](float V){ /* no object captured — always safe */ });

// Single-cast: bind a lambda guarded by a UObject weak ref (safe — skipped if dead)
Delegate.BindWeakLambda(this, [this](float V){ Use(V); });

// Multicast: bind to a UObject member (weak ref — auto-skipped on GC)
FDelegateHandle H2 = Multi.AddUObject(this, &AMyHud::HandleHealthChanged);

// Multicast: bind a lambda guarded by a UObject weak ref
FDelegateHandle H3 = Multi.AddWeakLambda(this, [this](float V){ Use(V); });

// Multicast: bare lambda — no lifetime guard; MUST remove before capture dies
FDelegateHandle H4 = Multi.AddLambda([](float V){ /* no this captured */ });
```

### Dynamic delegates — `AddDynamic` / `BindDynamic`

Dynamic delegates only accept `UFUNCTION`-marked methods. Use the macro wrappers
which auto-generate the function name string at compile time:

```cpp
// The handler MUST be a UFUNCTION() with the exact parameter signature
UFUNCTION()
void HandleDied(AActor* Killer);

// In BeginPlay or equivalent — AddDynamic wraps AddDynamic() macro internally
Health->OnDied.AddDynamic(this, &AMyHud::HandleDied);
```

## Executing and broadcasting

```cpp
// Multicast and dynamic multicast — Broadcast() to all bound listeners
OnHealthChanged.Broadcast(NewHealth);

// Single-cast — always check before calling Execute
if (CanInteract.IsBound())
{
    bool bOk = CanInteract.Execute(Instigator);
}
// Or use the safe form (no-op if unbound; cannot return a value)
CanInteract.ExecuteIfBound(Instigator);
```

- `Broadcast` is always safe to call even with zero bindings.
- `Execute` asserts if unbound — use only when you guarantee a binding exists.
- Multicast delegates cannot have return values; remove `_RetVal` from the macro.

## Payload variables

Non-dynamic delegates can bake extra arguments into the binding at bind time.
These extra arguments are appended after the delegate's declared parameters:

```cpp
DECLARE_DELEGATE_OneParam(FOnTick, float /*DeltaTime*/);

FOnTick D;
int32 MyId = 7;
// MyMethod signature: void MyMethod(float DeltaTime, int32 Id)
D.BindUObject(this, &AMyActor::MyMethod, MyId);
D.Execute(DeltaSeconds);  // calls MyMethod(DeltaSeconds, 7)
```

Payloads work with `Bind*`/`Add*` — up to four additional variables.

## Unbinding and lifetime

```cpp
// Multicast: remove one binding by handle
Multi.Remove(Handle);

// Multicast: remove all bindings for one object
Multi.RemoveAll(this);

// Dynamic multicast: remove a specific binding
Health->OnDied.RemoveDynamic(this, &AMyHud::HandleDied);

// Single-cast: unbind
Delegate.Unbind();

// Multicast: remove everything
Multi.Clear();
```

**Lifetime rules by binding type:**

| Binding | Tracks lifetime? | Action on dead object |
|---|---|---|
| `AddUObject` / `BindUObject` | yes (weak UObject ptr) | binding skipped, auto-compacted |
| `AddDynamic` / `BindDynamic` | yes (weak UObject ptr) | binding skipped |
| `AddWeakLambda` / `BindWeakLambda` | yes (weak UObject ptr) | lambda not called |
| `AddSP` / `BindSP` | yes (weak shared ptr) | binding skipped |
| `AddLambda` / `BindLambda` | **no** | crash if capture is dead |
| `AddRaw` / `BindRaw` | **no** | crash if object is dead |

For `AddLambda` and `AddRaw`, store the returned `FDelegateHandle` and call
`Remove(Handle)` in `EndPlay` or the destructor — before any captured object dies.

## DECLARE_EVENT (legacy)

`DECLARE_EVENT(OwnerType, EventName)` creates a `TMulticastDelegate` subclass whose
`Broadcast` is only accessible to `OwnerType` (friend). It is marked deprecated in
the source comment (`DelegateCombinations.h:30`) — prefer plain `DECLARE_MULTICAST_DELEGATE`
with a private broadcast method for the same encapsulation pattern.

## Thread-safe multicast

`DECLARE_TS_MULTICAST_DELEGATE*` produces a `TMulticastDelegate` parameterized with
`FDefaultTSDelegateUserPolicy` — the invocation list is guarded by a read-write lock.
Use it when bindings are added/removed or broadcast from multiple threads. The
per-binding callbacks themselves are not thread-safe; synchronize their bodies
separately. (`DelegateCombinations.h:26`)

## Decision guide

| Need | Solution |
|---|---|
| Blueprints subscribe | `DECLARE_DYNAMIC_MULTICAST_DELEGATE*` + `UPROPERTY(BlueprintAssignable)` |
| C++-only, many listeners | `DECLARE_MULTICAST_DELEGATE*` |
| One handler, possible return value | `DECLARE_DELEGATE*` or `DECLARE_DELEGATE_RetVal*` |
| Stored callable (not an event) | `TFunction<Ret(Args...)>` or `TUniqueFunction` |
| Cross-thread broadcasting | `DECLARE_TS_MULTICAST_DELEGATE*` |

## Gotchas

- **`AddDynamic` target is not a `UFUNCTION`** — compile or registration error; the
  bound function must be `UFUNCTION()` with the exact declared signature.
- **`AddLambda` / `AddRaw` capturing `this` without removing before destruction** —
  the broadcast will crash. Store the handle and call `Remove(Handle)` in `EndPlay`.
- **`Execute` on an unbound single-cast delegate** — asserts. Always `IsBound()` first
  or use `ExecuteIfBound`.
- **Dynamic multicast, not multicast, for Blueprints** — `DECLARE_MULTICAST_DELEGATE`
  is never `BlueprintAssignable`; only `DECLARE_DYNAMIC_MULTICAST_DELEGATE` is.
- **Param count/type mismatch with the `_NParams` suffix** — will not compile.
- **Dynamic param names not provided** — dynamic macros require a name for each param;
  omitting them is a compile error.
- **Modifying the invocation list during `Broadcast`** — adding/removing from inside a
  handler is deferred until broadcast completes; the delegate handles this safely.
- **`DECLARE_EVENT` for new code** — the source marks it deprecated; use plain
  multicast with a friend access pattern instead.

## References & source material

Engine source (UE 5.8, under `Engine/Source/Runtime/Core/Public/`):
- `Delegates/DelegateCombinations.h` — all `DECLARE_*` macros: single-cast:20,
  multicast:23, TS multicast:26, event:32, dynamic:35, dynamic multicast:38.
- `Delegates/Delegate.h` — `TDelegate`/`TMulticastDelegate` concepts, binding table,
  payload variable documentation; `UE_PRIVATE_DECLARE_DELEGATE`:205,
  `UE_PRIVATE_DECLARE_MULTICAST_DELEGATE`:209, `UE_PRIVATE_DECLARE_EVENT`:221,
  `UE_PRIVATE_DECLARE_DYNAMIC_DELEGATE`:231, `UE_PRIVATE_DECLARE_DYNAMIC_MULTICAST_DELEGATE`:239.
  (The old `FUNC_DECLARE_*` forms are deprecated in 5.8, `:247`–`:252`.)
- `Delegates/DelegateSignatureImpl.inl` — `TDelegateRegistration`/`TDelegate`:327,
  `BindLambda`:138, `BindWeakLambda`:163, `BindUObject`:288; `TMulticastDelegateRegistration`:743,
  `AddLambda`:823, `AddWeakLambda`:853, `AddUObject`:1020, `Remove`:1062,
  `TMulticastDelegate::Broadcast`:1133, `TDynamicDelegate`:1161,
  `TDynamicMulticastDelegate`:1288 (renamed from `TBaseDynamicDelegate`/
  `TBaseDynamicMulticastDelegate` in 5.8).
- `Delegates/MulticastDelegateBase.h` — `TMulticastDelegateBase`, `Clear`:119,
  `IsBound`:131, `RemoveAll`:173.
- `Delegates/IDelegateInstance.h` — `FDelegateHandle`:15 (the handle type returned
  by `Add*`; stores a `uint64` ID for O(N) lookup/removal).
- `Templates/Function.h` — `TFunction<Ret(Args...)>` / `TUniqueFunction` for stored
  callables that are not event delegates.

Official docs (UE 5.8):
- Delegates (single-cast) —
  <https://dev.epicgames.com/documentation/unreal-engine/delegates-and-lambda-functions-in-unreal-engine>
- Multicast Delegates —
  <https://dev.epicgames.com/documentation/unreal-engine/multicast-delegates-in-unreal-engine>
- Dynamic Delegates —
  <https://dev.epicgames.com/documentation/unreal-engine/dynamic-delegates-in-unreal-engine>

Deep-dive references in this skill:
- [references/delegate-types-matrix.md](references/delegate-types-matrix.md) — full
  macro-to-type mapping, param/payload limits, TS variant, event pattern.
- [references/binding-and-lifetime.md](references/binding-and-lifetime.md) — every
  binding form, safety guarantees, payload syntax, `FDelegateHandle` patterns.
- [references/dynamic-and-blueprint.md](references/dynamic-and-blueprint.md) — dynamic
  delegate mechanics, `AddDynamic`/`RemoveDynamic`, Blueprint event dispatcher wiring,
  `UDELEGATE` specifier, serialization notes.

Ships with 3 supporting files:

  • references/binding-and-lifetime.md
  • references/delegate-types-matrix.md
  • references/dynamic-and-blueprint.md

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us