【Unreal Engine】Event Dispatcherによるイベント駆動設計の実装方法

作成: 2025-12-12最終更新: 2026-07-19

Event Dispatcherで発生源と処理者を分離する疎結合設計を図解。Blueprintでの作る・登録する・鳴らすの3拍子、Unbind忘れの事故、C++のマルチキャストデリゲート実装まで。

「アイテムを拾ったら、インベントリを更新して、UIに通知して、SEを鳴らす」。素直に書くと、アイテムのBlueprintがインベントリとUIとサウンドを全部知っている状態になります。音の仕様を変えたいだけなのに、開くのはアイテムのBlueprintです。

この絡まりをほどく道具が Event Dispatcher です。アイテムは「拾われたよ」と ベルを鳴らすだけ で、誰が聴いて何をするかは聴く側が決めます。この記事では、Dispatcherの実装を「作る・登録する・鳴らす」の3拍子で解説し、Unbind忘れの事故と、C++でのデリゲート実装までを扱います。

ベルを鳴らす人形と、灯りがともる3軒の家。Event Dispatcherのイメージ

この記事でわかること

  • Dispatcherの本質= 発生源と処理者の完全な分離(Observerパターン)
  • Blueprint実装の3拍子: 作る(Dispatcher)→ 登録する(Bind)→ 鳴らす(Call)
  • Unbind忘れ が引き起こすクラッシュと、その防ぎ方
  • C++の マルチキャストデリゲートBlueprintAssignable / AddDynamic
  • 実践:アイテム取得の 通知網 を組む

Sponsored

なぜ必要か:名指しの連絡網は破綻する

Event DispatcherはUEにおける デリゲート(Delegate) の代表的な実装で、「イベントの発生を、関心のある全員に通知する」仕組みです。いわゆる Observerパターン にあたります。役割分担はこうなります。

役割すること知らないこと
発生源(Caller)ベルを鳴らす(Call)誰が聴いているか
処理者(Listener)ベルに登録する(Bind)発生源の中身
Event Dispatcher両者を仲介するベル両者の中身(だから依存を断ち切れる)
アイテムが全員を名指しで呼ぶと線が絡まるが、ベルを鳴らすだけなら発生源は鳴らすことだけ考えればいい

名指しの直接呼び出しでは、リアクションが1つ増えるたびに 発生源の修正 が必要です。Dispatcherなら、新しい聴衆がBindするだけで済み、発生源のBlueprintは一文字も変わりません。

Sponsored

実践:アイテム取得の通知網を組む

冒頭の「拾ったら3つが動く」を、実際に組んでみましょう。RPGの素材拾い、ローグライクの宝箱、シューティングのパワーアップ。「取得の瞬間に複数のシステムが反応する」構図は、どのジャンルでも同じです。実装は 3拍子 で進みます。

Event Dispatcher実装の3拍子。ベルを作る、聴く人が登録、起きたら鳴らす

再現用の準備: レベルに BP_Item を1個、BP_InventoryManager を1個置きます。Managerには変数 TargetItemBP_Item 型・ Instance Editable にチェック)を作り、レベル上のアイテムを指しておきます(Instance Editableにすると、レベルに置いた個体ごとに相手を指定できます → Blueprint変数入門)。この「1個ずつ手で指す」だるさを覚えておくと、後半のおまけで出てくる中央Dispatcherのありがたみが効いてきます。

拍子1:ベルを作る(Dispatcherの定義)

BP_Item のMy Blueprintパネル「Event Dispatchers」で OnItemPickedUp を作成します。引数(Inputs)は、作成したDispatcherを選択し、詳細パネルの Inputs 欄の「+」から追加します。聴く側が知りたい最小限の情報だけを載せます。ここでは拾ったプレイヤーの参照(Instigator)です。

拍子2:聴く人が登録する(Bind)

通知を受けたい側、たとえば BP_InventoryManager のBeginPlayで、アイテムの参照から Bind Event to OnItemPickedUp ノードを繋ぎ、Eventピンにカスタムイベントを接続します。

BP_InventoryManager(聴く側)
Event BeginPlay → Bind Event to OnItemPickedUp(Target: アイテムの参照)
  Event ピン ← Custom Event: HandleItemPickup(Instigator を受け取る)
HandleItemPickup → インベントリへ追加 → UI更新
BP_InventoryManagerでEvent BeginPlayからBind Event to OnItemPickedUpのイベントピンをCustom Event HandleItemPickupへ繋ぐノードグラ��フ

Bind Event to OnItemPickedUp の右側にある赤い四角のイベントピンから、Custom Event を作って繋ぐのがBindの実体です。ここで 「このベルが鳴ったら、この処理を呼んで」 という登録が完成します。同じ要領で、UIウィジェットとサウンド担当もそれぞれBindします。 聴衆は何人いてもかまいません

拍子3:起きたら鳴らす(Call)

発生源の BP_Item は、拾われた瞬間にベルを鳴らすだけです。

BP_Item(発生源)
On Component Begin Overlap → (プレイヤー判定) → Call OnItemPickedUp(Instigator: Overlapした相手) → Destroy Actor
BP_ItemでOn Component Begin OverlapからCast To BP_Player、Call OnItemPickedUp、Destroy Actorへ繋ぐノードグラフ

発生源がやることは Call OnItemPickedUp の1ノードだけです。 誰が聴いているかを一切知らずに、ただベルを鳴らして自分は消える 。この身軽さが疎結合の正体です。

プレイヤーが宝石を拾うとベルが鳴り、バッグ・UI・スピーカーの3つが動く。拾った1回で、3つが動く

Playしてアイテムを拾うと、インベントリ・UI・SEが同時に反応します。

確認として、サウンド担当のBindを 切って みてください。音だけが止まり、他は正常に動き続けます。Bindを戻したら、今度は新しく「実績システム」をBindしてみてください。 BP_Item を一切触らずに リアクションが1つ増えます。発生源を変えずに増やせる。これがDispatcherを使う理由です。

ポイントは2つです。

  • 引数は最小限に: ベルに乗せるのは「誰が・何を」程度の最小情報。大量のデータが要るなら、聴く側が参照経由で取りに行く方がシグネチャを綺麗に保てます
  • イベント名は「何が起きたか」: OnItemPickedUpOnHealthChanged のように過去の事実を名乗らせます。「何をすべきか」(UpdateUI など)を名乗らせると、発生源が処理者の事情を知っている密結合な名前になってしまいます
Sponsored

最重要の注意:BindしたらUnbindまでがセット

Dispatcherで唯一、命に関わるルールがこれです。 聴衆が消えるときは、登録も消す

Unbindを忘れると消えた相手に鳴らし続けて事故になる。消える前にハサミで登録を切る。BindしたらUnbindまでがセット

Bindは「発生源が聴衆への参照を持つ」ことでもあります。聴衆(特に頻繁に生成・破棄されるWidget)がUnbindせずに消えると、破棄済みオブジェクトへの参照が残り、次のCallで Accessed Noneエラーやクラッシュ の原因になります。Widgetなら破棄前(Remove from Parentの前後やDestruct)で、Actorなら EndPlay で、Unbind Event from ◯◯ を呼ぶのが作法です。

補足: 「Ownerが自分のComponentのDispatcherをBindする」ように 寿命が一致するペア なら、破棄も同時なので実質問題になりません(→ Blueprint Componentの記事)。危ないのは 寿命が違う相手 をBindするときです。

Sponsored

C++での実装:マルチキャストデリゲート

C++では、Event Dispatcherの正体は ダイナミック・マルチキャストデリゲート です。BlueprintAssignable を付けると、Blueprint側からもBindできるDispatcherとして公開されます。

// Item.h
// デリゲート型の宣言(引数2つ版。引数の数でマクロ名が変わる)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FOnItemPickedUpSignature, AItem*, PickedUpItem, APlayerCharacter*, Instigator);

UCLASS()
class AItem : public AActor
{
    GENERATED_BODY()

public:
    // BlueprintからもBindできるDispatcherとして公開
    UPROPERTY(BlueprintAssignable, Category = "Events")
    FOnItemPickedUpSignature OnItemPickedUp;
};
// Item.cpp — 鳴らす側
void AItem::OnOverlapBegin(/* ... */)
{
    // ベルを鳴らす(Blueprint の Call に相当)
    OnItemPickedUp.Broadcast(this, Cast<APlayerCharacter>(OtherActor));
}
// InventoryManager.cpp — 聴く側
void AInventoryManager::BeginPlay()
{
    Super::BeginPlay();
    if (AItem* Item = GetItemReference())
    {
        // 登録(Blueprint の Bind に相当)
        Item->OnItemPickedUp.AddDynamic(this, &AInventoryManager::HandleItemPickup);
    }
}

void AInventoryManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    if (AItem* Item = GetItemReference())
    {
        // 登録解除(Unbind に相当)。AddDynamic と必ずペアで
        Item->OnItemPickedUp.RemoveDynamic(this, &AInventoryManager::HandleItemPickup);
    }
    Super::EndPlay(EndPlayReason);
}

対応関係は素直で、 Call=Broadcast、Bind=AddDynamic、Unbind=RemoveDynamic です。C++側の書き方の基礎はBlueprintからC++への第一歩で扱っています。

おまけ:先に知っておくと良いこと

  • 「Bindしたいのに参照が取れない」問題: 実行中にスポーンするアイテムに、UIが個別にBindして回るのは大変です。定石は 「みんなが知っている場所」にベルを置く ことです。GameStateやGameInstanceに OnAnyItemPickedUp を作り、アイテムはそこへ鳴らしに行き、UIはそこにBindします(→ GameStateの記事
生まれては消えるアイテム達はGameStateのベルを鳴らしに来て、ずっと居るUI達はそこにBindする。ベルはみんなが知っている場所に吊るす
  • ボタンのOnClickedも同じ仕組み: UMGのButtonにある OnClicked は、エンジンが用意したEvent Dispatcherそのものです。知らないうちにDispatcherを使っていた、ということになります
  • 使いすぎ注意: すべての通信を放送にすると「この処理、誰が呼んだの?」の追跡が難しくなります。1対1で固定の関係なら直接呼び出しで十分です。使い分けの全体像は通信3方式の総論

まとめ

  • Event Dispatcher= 発生源と処理者を分離するベル 。発生源は鳴らすだけ、聴衆は登録するだけ
  • 実装は3拍子: 作る → Bind → Call 。C++では Broadcast / AddDynamic / RemoveDynamic
  • 鉄の掟は 「BindしたらUnbindまでがセット」 。寿命の違う相手とのBindが事故ポイント
  • リアクションの追加・削除で 発生源が無傷 。これが疎結合の実利

放送と対になるもう1つの疎結合、「宛名のいらない手紙」はBlueprint Interfaceの記事で深掘りします。

あなたのゲームで、いちばん多くのシステムが反応すべき瞬間はどこでしょうか。敵撃破か、レベルアップか。その瞬間に、最初のベルを吊るしてみてください。