【UE5】デリゲートの基本的な使い方

プログラマーの尾関です。

今回は Unreal Engine でのデリゲートの基本的な使い方についてまとめてみました。

デリゲートは何か

デリゲートは、あるオブジェクトが別のオブジェクトの関数を動的に呼び出すことを可能にする仕組みです。

具体的には、以下の用途が考えられます。

デリゲートを使うメリット

デリゲートを使うことで得られるメリットは以下のとおりです。

  • 疎結合な設計:オブジェクト間の直接的な依存関係を排除し、一つのアクターが別のアクターやコンポーネントと柔軟に通信できるようにします
  • 多対多の通信:デリゲートは複数の関数やリスナーを登録できるため、一つのイベントが複数のアクターやシステムに通知されることが可能です(マルチキャストデリゲート)
  • Blueprintから呼び出せる:デリゲートはBlueprintでも使用可能であり、プログラマー以外でも視覚的にイベント駆動型ロジックを構築できます

処理によっては簡素な記述で書ける、オブジェクト間の依存関係が減ることで拡張しやすくなる、といったメリットがあります。

補足:デリゲートの「予測困難性」と「依存関係の隠蔽」によるデメリット

デリゲートで登録する処理は、通常は登録時よりも後に「遅延」して発生するものです。処理をあらかじめ登録しておくことで、発生条件を満たすかどうかを呼び出される方が判定する必要がなく、シンプルに記述することができます。ただ、そのデメリットとして「いつ呼び出されるかがわからない」という “予測困難性” や、呼び出される相手が不明になりやすい “依存関係の隠蔽” を生みやすい点で、モジュール全体の見通しが悪くなる可能性があります。

これらの問題の回避策としては、「明確な命名規則」と「ドキュメント化」により、モジュールの透明性を上げることです。またデリゲート登録時に「呼び出されるタイミング」を記述する、または呼び出し側のタイミング (BoundやBroadcastなど) に呼び出し条件を記載することで予測困難性を軽減できます。また登録されているデリゲートにデバッグ用の情報を付加し、デリゲートに対する情報をログで確認できるようにするといった方法も考えられます。

いずれにしてもデリゲートは便利であるものの、使い方によっては可読性が大きく低下することを考慮して、予防策を立てておくことが重要と言えます。

デリゲートにおけるバインドとは

デリゲートにおける「バインド」とは、デリゲートに関数を登録することです。デリゲートの種類によってはバインド可能な関数は1つだけの場合もあります。

4つのデリゲートの違いと特徴

Unreal Engineは「シングルキャスト or マルチキャスト」「静的か動的 (ダイナミック) か」という 2つの軸を持っています。

  • シングルキャストデリゲート:1つの関数のみ登録可能
  • マルチキャストデリゲート:複数の関数を登録可能
  • ダイナミック (動的) デリゲート:リフレクション(≒文字列)による関数の登録が可能

呼び出しは C++ のみで問題ないのか、BPでも呼び出したいのか、複数の関数を呼び出せるようにしたいのか、といった要件でデリゲートを使い分ける必要があります。

プログラムの実装の流れ

デリゲートはおおよそ以下の流れで実装していきます。

  1. (呼び出し側) デリゲートマクロの定義
  2. (呼び出し側) デリゲートをメンバ変数として定義する
  3. (呼び出し側) デリゲートの呼び出しタイミングとなる条件の定義と呼び出しの実行
  4. (呼び出される側) デリゲートの登録処理を行う

1. デリゲートマクロの定義

まずはデリゲートの型として、デリゲートマクロを定義する必要があります。

定義例としては以下のものが考えられます。

// シングルキャスト.
DECLARE_DELEGATE(FTimerEventCallback); 
// シングルキャスト (ダイナミック版).
DECLARE_DYNAMIC_DELEGATE(FTimerEventDynamicCallback);
// マルチキャスト.
DECLARE_MULTICAST_DELEGATE(FTimerEventMultiCallback);
// マルチキャスト (ダイナミック版).
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTimerEventDynamicMultiCallback);

シングル or マルチ、ダイナミック or 静的 でマクロを使い分ける必要があります。

また引数や戻り値付きのマクロを使いたい場合は、末尾に "_OneParam" や "_RetVal" がつけられたマクロを使用します。

// シングルキャスト (戻り値あり).
DECLARE_DELEGATE_RetVal(bool, FTimerEventCallback); 
// マルチキャスト (引数あり).
DECLARE_DELEGATE_RetVal_OneParam(FTimerEventMultiCallback, int);

2. デリゲートマクロのメンバ変数の定義

デリゲートマクロで定義したデリゲート型は、クラスや構造体に定義します。
また、メンバ変数は UCLASS や USTRUCT である必要はなく、Blueprint に公開しないのであれば、UPROPERTY() は不要です。

DECLARE_DELEGATE(FTimerEventCallback); // 通常版.

struct FTimerEvent
{
	float TriggerTime = 0.f; // トリガー時間.
	FTimerEventHandle Handle = {}; // イベントハンドル.

	// BPに公開しない場合は UPROPERTY は不要
	FTimerEventCallback Delegate; // コールバック関数.
};
Information

ダイナミックデリゲートについては、通常は Blueprintで呼び出す前提のデリゲートなので、UCLASS や USTRUCT の UPOPERTY() として定義する必要があります。

また、Buleprintから直接バインドしたい場合はダイナミックマルチキャストデリゲートとして宣言しなければなりません。

// ダイナミック・マルチキャストの宣言.
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTimerEventDynamicCallback);

UCLASS()
class SV_API ACPP_GameTimer : public AActor
{
	GENERATED_BODY()
	
public:
	// この定義によりBlueprintでの使用が可能になります.
	UPROPERTY(BlueprintAssignable)
	FTimerEventDynamicCallback TimerEventDelegate;
};

3. デリゲートの呼び出しタイミングとなる条件の定義と呼び出しの実行

何らかの条件を満たしたときにデリゲートを実行する必要があります。

例えば「3秒後に爆弾が爆発する」処理であれば「3秒後」が条件で「爆弾が爆破する」がデリゲートにより実行される処理です。ただ、呼び出し側は「3秒後に発生する何かの処理」というだけで、何が実行されるかを知る必要はありません。

デリゲートの実行関数は、シングルキャストデリゲートとマルチキャストデリゲートで呼び出し名が異なります。

  • シングルキャストデリゲート:Execute()
  • マルチキャストデリゲート:Broadcast()

シングルキャストの対象となるのが「1つ」であるのに対して、マルチキャストは「複数」なので Broadcast (=複数の対象に同時に発信する) という名称となっています。

コールバックの問題点として、呼び出し先が有効かどうかを判定する必要があります。シングルキャストの場合は IfBound() で判定すると安全にデリゲートを実行できます。

if(Delegate.IsBound() == false) {
  return; // デリゲートが無効な場合は実行しない.
}

// デリゲートを実行.
Delegate.Execute();

シンプルに無効だったら実行しない、という処理で良いなら ExecuteIfBoud() で良いです。

// デリゲートが有効な場合のみ実行.
Delegate.ExecuteIfBound();

マルチキャストの場合は、呼び出し先が無効であるかどうかを気にせずにそのまま Broadcast() で実行し、呼び出し先が無効であれば実行されず、また無効になることで自動でデリゲートから削除されます。(※UFUNCTION() の関数のみ)

Information

無効な関数がガベージコレクションにより自動でデリゲートから削除されるためには、UFUNCTION() である関数でなければいけません。そのため基本的にマルチキャストを扱う場合には、ダイナミックマルチキャストデリゲート (DECLARE_DYNAMIC_MULTICAST_DELEGATE) を使うのが安全です。

4. デリゲートの登録処理 (バインド) を行う

プログラムでの実行順では、デリゲートの実行よりも先に行いますが、実装順としては呼び出し側の実装よりも後に書かれることが多いので最後の紹介としました。

バインド関数のよく使うものとして、以下の4つを上げておきます。

ただ、特別な理由がない限りは「BindUObject」「BindDynamic」の利用がおすすめです。理由として、それ以外は参照先の安全性のチェックが煩雑になるためです。ただパフォーマンスの問題やゲームの仕様によっては BindRaw や BindSP (シェアードポインタでの登録) といった運用も考えられます。

最後に

長々と書きましたが、デリゲートの選び方としてはおおよそ以下の方針になると思います。

  • C++でのみ使用する。高速で処理したい。戻り値が必要
    →シングルキャストデリゲート (DECLARE_DELEGATE)
  • C++でのみ使用して、複数の関数をバインドしたい
    →マルチキャストデリゲート (DECLARE_MULTICAST_DELEGATE)
  • BPからも呼び出したい
    →マルチキャストダイナミックデリゲート (DECLARE_MULTICAST_DELEGATE)
Information

細かいテクニックですが、シングルキャストデリゲートは UFUNCTION() や UPROERTY() を書く必要がないので、#ifdef などで条件付きコンパイルが可能という利点もあります。

以上、デリゲートの基本的な使い方でした。この記事がデリゲートを扱うための参考になればなによりです。

\ 最新情報をチェック /