【UE4 C++】デリゲートの使い方まとめ

この記事では、Unreal Engine の C++ でデリゲートを扱う方法についてまとめています。

執筆時に扱った Unreal Engine のソースコードのバージョンは 4.25.4
プラットフォームは Windows 10 (64ビット版) を対象としています。
ビルドに使用したアプリ(IDE)は Microsoft Visual Studio 2019 です。

以降、Unreal Engine 4 を UE4 と略記します。

デリゲートの種類

UE4 のデリゲートは以下の4種類あります。

  1. シングルキャスト
  2. マルチキャスト
  3. ダイナミック
  4. イベント

以下でそれぞれについて説明します。

シングルキャスト(Singlecast)

任意の関数ひとつをデリゲートにバインド(結びつけ)できます。

マルチキャスト(Multicast)

複数の関数をデリゲートにバインドでき、バインドしたすべての関数を一度に呼び出すことができます。

ダイナミック(Dynamic)

デリゲートはシリアライズされ、リフレクションを使って関数を呼び出すことができます。

イベント(Event)

マルチキャストに近いですが、任意のクラス内で定義して使います。
マルチキャストと同様に、バインドしたすべての関数を一度に呼び出すことができます。

デリゲートを宣言できる場所

デリゲートは以下の3つのスコープで宣言することができます。

  1. グローバル・スコープ
  2. namespace の中
  3. クラスの中
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

// 1. グローバル・スコープ

namespace ほげほげ
{
	// 2. namespace の中
}

UCLASS()
class もげ
{
	GENERATED_BODY()

public: か protected: か private:

	// 3. クラスの中
};

namespace ほげ~ん
{
	UCLASS()
	class もげら
	{
		GENERATED_BODY()

	public: か protected: か private:

		// 2. + 3. namespace の中にあるクラスの中
	};
}

デリゲートの宣言方法

専用のマクロを使って、デリゲートを宣言します。
デリゲートに登録する関数は、戻り値あり/なしに対応していて、引数の数は0個~9個まで指定可能です。

具体的な宣言文を以下で紹介していきます。

シングルキャストデリゲートの宣言
戻り値なし、引数なし関数をバインドできるデリゲートの宣言方法

DECLARE_DELEGATE(デリゲート名);

// 戻り値なし、引数なし関数をバインドできるデリゲートの宣言
DECLARE_DELEGATE(FSampleDelegate);

戻り値なし、引数1個

DECLARE_DELEGATE_OneParam(デリゲート名,引数1の型);

// 戻り値なし、引数1個の関数をバインドできるデリゲートの宣言
DECLARE_DELEGATE_OneParam(FSampleDelegate, const int32);

戻り値なし、引数2個以上

DECLARE_DELEGATE_TwoParams(デリゲート名,引数1の型,引数2の型);
DECLARE_DELEGATE_ThreeParams(デリゲート名,引数1の型,引数2の型,引数3の型);
DECLARE_DELEGATE_FourParams(デリゲート名,略…);
DECLARE_DELEGATE_FiveParams(デリゲート名,略…);
DECLARE_DELEGATE_SixParams(デリゲート名,略…);
DECLARE_DELEGATE_SevenParams(デリゲート名,略…);
DECLARE_DELEGATE_EightParams(デリゲート名,略…);
DECLARE_DELEGATE_NineParams(デリゲート名,略…);

// 戻り値なし、引数4個の関数をバインドできるデリゲートの宣言
DECLARE_DELEGATE_FourParams(FSampleDelegate, struct FTest&, const int32, const float, const class USampleActor&);

戻り値あり、引数なし

DECLARE_DELEGATE_RetVal(戻り値の型,デリゲート名)

戻り値あり、引数1個

DECLARE_DELEGATE_RetVal_OneParam(戻り値の型,デリゲート名,引数1の型);

戻り値あり、引数2個以上

DECLARE_DELEGATE_RetVal_TwoParams(戻り値の型,デリゲート名,引数1の型,引数2の型);
DECLARE_DELEGATE_RetVal_ThreeParams(戻り値の型,デリゲート名,引数1の型,引数2の型,引数3の型);
DECLARE_DELEGATE_RetVal_FourParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_FiveParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_SixParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_SevenParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_EightParams(戻り値の型,デリゲート名,略…);
DECLARE_DELEGATE_RetVal_NineParams(戻り値の型,デリゲート名,略…);

シングルキャスト・デリゲートのバインド方法

シングルキャスト・デリゲートに関数ポインタをバインドする(結びつける)方法です。
バインドすることで、関数を呼び出せるようになります。

BindStatic(static関数名, 引数リスト)

どのクラスにも属していない C++ のグローバルな static 関数をデリゲートにバインドします。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE(FSampleDelegate);

static void StaticSampleFunction()
{
	UE_LOG(LogTemp, Log, TEXT("called StaticSampleFunction()"));
}

UCLASS()
class USampleClass : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		SampleDelegate.BindStatic(StaticSampleFunction);
	}
	void Invoke()
	{
		SampleDelegate.ExecuteIfBound();
	}
private:
	FSampleDelegate SampleDelegate;
};
BindLambda(ラムダ式, 引数リスト)

C++ のラムダ式をデリゲートにバインドします。
厳密に言うと、ラムダ式以外のファンクタをバインドしても動作しますが、ラムダ式で使うことを主目的としています。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE_RetVal_OneParam(int, FSampleDelegate, int);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		SampleDelegate.BindLambda(
		[](int a){
			UE_LOG(LogTemp, Log, TEXT("called Lambda(%d)"), a);
			a += 10;
			return a;
		});
	}
	void Invoke()
	{
		if (SampleDelegate.IsBound() == false) { return; }
		int ReturnedValue = SampleDelegate.Execute(Count);
		UE_LOG(LogTemp, Log, Text("ReturnedValue=%d"), ReturnedValue);
	}
private:
	FSampleDelegate SampleDelegate;
	int Count = 0;
};
BindRaw(オブジェクトのポインタ, メンバ関数のポインタ, 引数リスト)

C++ の生ポインタをデリゲートにバインドします。
生ポインタは参照されているかどうかを考慮しないので、オブジェクトが削除された後にデリゲートが呼ばれた場合、安全ではない可能性があります。デリゲートを実行するときは注意が必要です。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE_TwoParams(FSampleDelegate, int, float);

class FBindRawSampleClass
{
public:
	void OnDelegateCalled(int a, float b)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d, %f)"), a, b);
	}
};

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USampleClass()
	{
		BindRawSample = new FBindRawSampleClass();
	}
	~USampleClass()
	{
		delegate BindRawSample;
	}
	void Register()
	{
		SampleDelegate.BindRaw(BindRawSample, &FBindRawSampleClass::OnDelegateCalled);
	}
	void Invoke()
	{
		SampleDelegate.ExecuteIfBound(123, 3.14f);
	}
private:
	FSampleDelegate SampleDelegate;
	FBindRawSampleClass* BindRawSample = nullptr;
};
BindSP(シェアードリファレンス, メンバ関数のポインタ, 引数リスト)

シェアードリファレンスをデリゲートにバインドします。
具体的には TSharedRef のインスタンスか TSharedPtr::ToSharedRef() を渡します。
これは UE4 のガーベジコレクタで管理していないオブジェクトのポインタをバインドするためのものです。
この関数はスレッドセーフではありません。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE(FSampleDelegate);

UCLASS()
class USampleClass : public UObject
{
	GENERATED_BODY()
public:
	USampleClass()
	{
		BindSPSample = MakeShareable(new FBindSPSampleClass());
	}
	void Register()
	{
		SampleDelegate.BindSP(BindSPSample.ToSharedRef(), &FBindSPSampleClass::OnDelegateCalled);
	}
	void Invoke()
	{
		SampleDelegate.ExecuteIfBound(123, 3.14f);
	}
private:
	FSampleDelegate SampleDelegate;
	TSharedPtr<class FBindSPSampleClass> BindSPSample = nullptr;
};

class FBindSPSampleClass
{
public:
	void OnDelegateCalled()
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled()"));
	}
};
BindThreadSafeSP(略…)

BindSP() のスレッドセーフ版です。
スレッドセーフ版は並列処理に対する安全性は高まりますが、速度が犠牲になるため、確実に必要になる場合にのみ使用します。

BindUObject(UObject継承クラスのポインタ, メンバ関数のポインタ, 引数リスト)

UObject を継承しているクラスのメンバ関数をデリゲートにバインドします。
UObject を継承していないクラスのポインタを指定するとコンパイルエラーが出ます。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE(FSampleDelegate);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		SampleDelegate.BindUObject(this, &USampleClass::OnDelegateCalled);
	}
	void Invoke() const
	{
		SampleDelegate.ExecuteIfBound();
	}
private:
	FSampleDelegate SampleDelegate;
	void OnDelegateCalled()
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled()"));
	}
};
BindUFunction(UObject継承クラスのポインタ, “メンバ関数名”, 引数リスト)

バインド関数の中で唯一、メンバ関数に文字列を使ってバインドできる方法です。
その代わり、バインドするメンバ関数には、UFUNCTION() マクロを使ってリフレクション対象にする必要があります。
UFUNCTION() マクロを使うメンバ関数を持つクラスは、 UCLASS() マクロを使っている必要があり、UCLASS() マクロを使うには UObject を継承する必要があります。
なので、バインドするクラスのポインタは UObject を継承している必要があります。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DELEGATE_OneParam(FSampleDelegate, int32);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		auto FunctionName = GET_FUNCTION_NAME_CHECKED(USampleClass, OnDelegateCalled);
		SampleDelegate.BindUFunction(this, FunctionName);
	}
	void Invoke() const
	{
		SampleDelegate.ExecuteIfBound(12345);
	}
private:
	FSampleDelegate SampleDelegate;

	UFUNCTION()
	void OnDelegateCalled(int32 a)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d)"), a);
	}
};
BindWeakLambda(オブジェクトのポインタ, ラムダ式, 引数リスト)

バージョン 4.21 で追加された関数ですが、使い道がよく分かりません。

使い方を理解するためにいくつかサンプルコードを作りましたが、この関数を使わず BindLambda() でバインドした場合でも、Execute() を実行する前に IsBound() でチェックするか、ExecuteIfBound() を使えばクラッシュを防ぐことができました。
なので、どういう状況でこのバインド関数を使う必要があるのか?が分かりません。

ブループリント用でしょうか…?

Bind* 関数の3番目の引数って何?

これまでに記載した「使用例」を見ていただければお分かりいただけると思いますが、バインド関数の3番目の引数「引数リスト」は一度も使用していません。
使用例でこの引数を使わなかったのは、先にこの引数リストの使い方を例示してしまうと、UE4 のデリゲートについてよく知らない人が混乱しそうだからです。
長々と説明するより、コードを見ていただく方が早いので、以下に使用例を記載します。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

// 戻り値と引数を持たないデリゲートを宣言
DECLARE_DELEGATE(FSampleDelegate);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		// バインド関数に引数を2つ指定
		SampleDelegate.BindUFunction(this, "OnDelegateCalled", 1, 2);
	}
	void Invoke() const
	{
		// デリゲートを実行する際に引数を指定していない
		SampleDelegate.ExecuteIfBound();
	}
private:
	FSampleDelegate SampleDelegate;

	// 実際に呼び出される関数は引数を2つ持っている
	UFUNCTION()
	void OnDelegateCalled(int32 a, int32 b)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d, %d)"), a, b);
	}
};

DECLARE_DELEGATE(FSampleDelegate); 戻り値も引数もないデリゲートとして宣言しているのですが、バインドしている関数(OnDelegateCalled)は2つの引数を持っています。
このように、宣言とは異なる数の引数を渡すことができます。
増やした分の引数を、バインド関数の「引数リスト」に渡すことができます。

逆の使い方もできます。
以下で例示します。

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

// 戻り値なし、2つの引数を持つデリゲートとして宣言
DECLARE_DELEGATE_TwoParams(FSampleDelegate, int32, int32);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	void Register()
	{
		// OnDelegateCalled() は引数を1つしか持っていませんがバインドできます。
		SampleDelegate.BindUFunction(this, "OnDelegateCalled");
	}
	void Invoke() const
	{
		// 問題なく呼び出せますが、2番目の引数は無視されます。
		SampleDelegate.ExecuteIfBound(888, 999);
	}
private:
	FSampleDelegate SampleDelegate;

	UFUNCTION()
	void OnDelegateCalled(int32 a)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnDelegateCalled(%d)"), a);
	}
};

引数の数は重要ではないようです。

マルチキャスト・デリゲート
宣言

DECLARE_MULTICAST_DELEGATE(デリケート名)
DECLARE_MULTICAST_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_MULTICAST_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_MULTICAST_DELEGATE_ThreeParams(略)
DECLARE_MULTICAST_DELEGATE_FourParams(略)
DECLARE_MULTICAST_DELEGATE_FiveParams(略)
DECLARE_MULTICAST_DELEGATE_SixParams(略)
DECLARE_MULTICAST_DELEGATE_SevenParams(略)
DECLARE_MULTICAST_DELEGATE_EightParams(略)
DECLARE_MULTICAST_DELEGATE_NineParams(略)

DECLARE_MULTICAST_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_MULTICAST_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_MULTICAST_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_MULTICAST_DELEGATE_RetVal_ThreeParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_FourParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_FiveParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_SixParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_SevenParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_EightParams(略)
DECLARE_MULTICAST_DELEGATE_RetVal_NineParams(略)

バインドと実行

シングルキャスト・デリゲートと違い、バインド関数ではなく、追加関数(Add*)を使います。
追加関数によって、呼び出すデリゲートを実行リスト(invocation list)にひとつずつ登録します。
リストに登録したデリゲートは、Broadcast() 関数でまとめて実行します。

以下の関数は実行リストにデリゲートを追加する関数です。
追加関数は FDelegateHandle を戻り値として返し、そのハンドルはデリゲートを削除するときに使います。

Add(デリゲートのインスタンス)
AddStatic(グローバルな C++ のスタティック関数のポインタ, 引数リスト)
AddRaw(オブジェクトのポインタ, メンバ関数のポインタ, 引数リスト)
AddSP(シェアードリファレンス, メンバ関数のポインタ, 引数リスト)
AddUObject(UObject継承クラスのポインタ, メンバ関数のポインタ, 引数リスト)

以下の関数は実行リストに登録したデリゲートをリストから削除するために使います。

特定のデリゲートだけを削除
Remove(FDelegateHandle)

全てのデリゲートを削除
RemoveAll()

以上。

実行リストにデリゲートが登録されている場合、true を返します。
IsBound()

指定したオブジェクトのデリゲートが実行リストに登録されている場合、true を返します。
IsBoundToObject(オブジェクトのポインタ)

実行リストに登録したデリゲートを全て実行
実行リストに登録したデリゲートが呼び出される順番は未定義です。
Broadcast()

使用例
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FSampleDelegate, int);

static void SampleStaticFunction(int a, int b)
{
	UE_LOG(LogTemp, Log, TEXT("called SampleStaticFunction(%d) : %d"), a * b, b);
}

class FSampleRawClass
{
public:
	void OnRawDelegate(int a, int b)
	{
		UE_LOG(LogTemp, Log, TEXT("called FSampleRawClass::OnRawDelegate(%d) : %d"), a * b, b);
	}
};

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USample()
	{
		FDelegateHandle h1 = SampleDelegate.AddLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaA(%d) : %d"), a + b, b); }, 1);
		FDelegateHandle h2 = SampleDelegate.AddLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaB(%d) : %d"), a + b, b); }, 2);

		FDelegateHandle h3 = SampleDelegate.AddUObject(this, &USample::OnDelegateCalled, 3);
		FDelegateHandle h4 = SampleDelegate.AddUObject(this, &USample::OnDelegateCalled, 4);

		FDelegateHandle h5 = SampleDelegate.AddUFunction(this, "OnUFunctionDelegate", 5);
		FDelegateHandle h6 = SampleDelegate.AddUFunction(this, "OnUFunctionDelegate", 6);

		FDelegateHandle h7 = SampleDelegate.AddStatic(SampleStaticFunction, 7);

		auto f = TBaseDelegate<void, int>::CreateLambda([](int a, int b) { UE_LOG(LogTemp, Log, TEXT("called LambdaC(%d) : %d"), a + b, b); }, 8);
		FDelegateHandle h8 = SampleDelegate.Add(f);

		SampleRawClass = MakeShareable(new FSampleRawClass());
		FDelegateHandle h9 = SampleDelegate.AddRaw(SampleRawClass.Get(), &FSampleRawClass::OnRawDelegate, 9);
		FDelegateHandle h10 = SampleDelegate.AddSP(SampleRawClass.ToSharedRef(), &FSampleRawClass::OnRawDelegate, 10);

		SampleDelegate.Remove(h1);
		SampleDelegate.Remove(h4);
		SampleDelegate.Remove(h6);
	}
	void Invoke() const
	{
		if (SampleDelegate.IsBoundToObject(this) == false) { UE_LOG(LogTemp, Log, TEXT("%p is not bound to SampleDelegate"), this); }
		if (SampleDelegate.IsBound() == false) { return; }
		SampleDelegate.Broadcast(100);
	}
private:
	FSampleDelegate SampleDelegate;
	TSharedPtr<FSampleRawClass> SampleRawClass = nullptr;

	void OnUObjectDelegate(int a, int b)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnUObjectDelegate(%d) : %d"), a - b, b);
	}

	UFUNCTION()
	void OnUFunctionDelegate(int a, int b)
	{
		UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate(%d) : %d"), a - b, b);
	}
};

ダイナミック・デリゲート

ダイナミック・デリゲートには、シングルキャストとマルチキャストの両方があります。
ダイナミック・デリゲートはシリアライズされるため、バインドしたデリゲートを関数名で検索できます。
ただし、通常のデリゲートよりも動作が遅くなります。

宣言

ダイナミック・シングルキャスト・デリゲート
DECLARE_DYNAMIC_DELEGATE(デリケート名)
DECLARE_DYNAMIC_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_DYNAMIC_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_DELEGATE_NineParams(略)

DECLARE_DYNAMIC_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_DELEGATE_RetVal_ThreeParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_FourParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_FiveParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_SixParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_SevenParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_EightParams(略)
DECLARE_DYNAMIC_DELEGATE_RetVal_NineParams(略)

ダイナミック・マルチキャスト・デリゲート
DECLARE_DYNAMIC_MULTICAST_DELEGATE(デリケート名)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(デリケート名, 引数1の型)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_NineParams(略)

DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal(戻り値の型, デリケート名)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_OneParam(戻り値の型, デリケート名, 引数1の型)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_TwoParams(戻り値の型, デリケート名, 引数1の型, 略…)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_RetVal_NineParams(略)

バインドと実行

バインド(シングルキャスト)と追加(マルチキャスト)はマクロを使って行います。

ダイナミック・シングルキャスト・デリゲート専用

マクロ
BindDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)

関数
Execute()
ExecuteIfBound()

ダイナミック・マルチキャスト・デリゲート専用

マクロ
AddDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)
RemoveDynamic(UObject継承クラスのポインタ, メンバ関数のポインタ)

補足
AddDynamic() で同じメンバ関数を複数回登録すると、実行時に ensure() に引っ掛かります。
同じメンバ関数を複数回登録することは想定していないようです。
ただ、ensure() を無視してプログラムの実行を継続しても、クラッシュはしませんでした。

関数
Broadcast()

シングルでもマルチでも使える関数
IsBound()

使用例(マルチキャスト)
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSampleDelegate);

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USample()
	{
		SampleDelegate.AddDynamic(this, &USample::OnUFunctionDelegate1);
		SampleDelegate.AddDynamic(this, &USample::OnUFunctionDelegate2);
		SampleDelegate.RemoveDynamic(this, &USample::OnUFunctionDelegate1);
	}
	void Invoke() const
	{
		if (SampleDelegate.IsBound() == false) { return; }
		SampleDelegate.Broadcast();
	}
private:
	FSampleDelegate SampleDelegate;

	UFUNCTION()
	void OnUFunctionDelegate1()
	{
		UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate1()"));
	}

	UFUNCTION()
	void OnUFunctionDelegate2()
	{
		UE_LOG(LogTemp, Log, TEXT("called OnUFunctionDelegate2()"));
	}
};

イベント・デリゲート
宣言

DECLARE_EVENT(イベントを所有するクラス, イベント名)

DECLARE_EVENT_OneParam(イベントを所有するクラス, イベント名,引数1の型)

DECLARE_EVENT_TwoParams(イベントを所有するクラス, イベント名,引数1の型,…)
… NineParams まである

戻り値は指定できません。

DECLARE_DERIVED_EVENT(派生クラス, 親クラス::イベント名, オーバーライドしたイベント名)

バインドと実行

マルチキャスト・デリゲートと同じです。

ドキュメントと実装内容の相違点

公式の説明

イベントは マルチキャスト デリゲート と非常によく似ています。ただし、どのクラスもイベントを結合できる一方で、 イベントをデリゲートするクラスのみがそのイベントの Broadcast、IsBound、Clear 関数を呼び出すことができます。つまり、イベント オブジェクトは、これらのセンシティブ関数のアクセスを外部関数に与えることを心配せずにパブリック インターフェースにエクスポーズすることができます。 イベントのユースケースは、純粋な抽象クラスにコールバックを含み、外部クラスが Broadcast、IsBound、Clear 関数を呼び出さないようにしています。

公式のドキュメントから(引用部分以外も含めて)イベント・デリゲートの特徴をまとめると。

  • 中身はマルチキャスト・デリゲート
  • 実行できるのは宣言したクラスだけ
  • 純粋な抽象クラスでコールバックを定義して使う
  • 戻り値を持ったデリゲートは宣言できない
  • デリゲートを継承するための宣言用マクロがある

ドキュメントによると、宣言したクラスでのみ、デリゲートを実行できる…と書かれているのですが、実際はどこからでも呼べるようになっています。

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 
#pragma once 
#include "CoreMinimal.h" 
#include "Sample.generated.h"

// FSampleBase だけが FSampleEvent を実行できる(はず)
DECLARE_EVENT(FSampleBase, FSampleEvent);

class FSampleBase {};

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USample()
	{
		// バインドはどこでもできる
		SampleEvent.AddLambda([]() { UE_LOG(LogTemp, Log, TEXT("AddLambda()")); });
	}
	void Invoke() const
	{
		// FSampleBase 以外は実行できないはずだが、できてしまう…。
		// ランタイムエラーも出ない。
		SampleEvent.Broadcast();
	}

	// FSampleBase の外部で使用
	FSampleEvent SampleEvent;
};

上記のコードは、イベント・デリゲートのルールに従えば間違っているはずですが、ビルドしても実行しても何もエラーは報告されません。

別のテストコード

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 
#pragma once 
#include "CoreMinimal.h" 
#include "Sample.generated.h"

class IBase
{
public:
	DECLARE_EVENT(IBase, FEvent);
};

class FOther
{
public:
	IBase::FEvent Event;
};

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USample()
	{
		Other = MakeShareable(new FOther());

		// バインドはどこでもできる
		Other->Event.AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("AddLambda")); });
	}
	void Invoke() const
	{
		// これも実行可能。
		Other->Event.Broadcast();
	}
private:
	TSharedPtr<FOther> Other;
};

宣言したのとは違うクラスでイベント・デリゲートのインスタンスを持ち、さらにそのインスタンスを別のクラスから呼び出すコードを試してみましたが、こちらも問題なく実行できます。

実際のところ、Broadcast() はどこでも呼び出せるようです。
さらにまた、別のコードを試しました。

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 
#pragma once 
#include "CoreMinimal.h" 
#include "Sample.generated.h"

class IBase
{
public:
	// 存在しないクラスを指定
	DECLARE_EVENT(hoge123456789XYZ, FEvent);
};

class FOther
{
public:
	// 同じく存在しないクラスを指定
	DECLARE_DERIVED_EVENT(hoge987654321hoge, IBase::FEvent, FOtherEvent);
};

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	USample()
	{
		SampleEvent.AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("AddLambda")); });
	}
	void Invoke() const
	{
		SampleEvent.Broadcast();
	}
	FOther::FOtherEvent SampleEvent;
};

ビルドもできますし、実行もできます。
宣言用マクロの一番目の引数に指定したクラスが存在しているか?はチェックしていないようです。

何が原因?

不可解な動作をしているので、宣言用マクロを定義している部分のソースコードを調べました。

Engine/Runtime/Core/Public/Delegates/Delegate.h に定義があります。

宣言用マクロは以下の関係になっていることが分かりました。

DECLARE_DERIVED_EVENT(イベントのフレンドクラス, イベントクラスの親クラス, イベントクラス);

class イベントクラス : public イベントクラスの親クラス { friend イベントのフレンドクラス; };

イベントのフレンドクラスは、イベントクラスの protected と private メンバーにアクセスできるようになっています。
宣言用マクロに問題はなさそうです。

問題は DECLARE_EVENT() の方でしょうか…。

// DECLARE_EVENT() の中身

#define FUNC_DECLARE_EVENT( OwningType, EventName, ... ) \
	class EventName : public TBaseMulticastDelegate<__VA_ARGS__> \
	{ \
		friend class OwningType; \
	};

イベントクラスは、TBaseMulticastDelegate を継承しています。
問題があるのは、このテンプレートクラスのようです。

Broadcast(), IsBound() などのクラスのメンバ関数/変数を外部から呼び出せないようにするには、 protected か private で宣言する必要があります。
現状、外部から呼び出せるようになっているということは、public で宣言されているということになります。
それを確認します。

TBaseMulticastDelegate は Engine/Runtime/Core/Public/Delegates/DelegateSignatureImpl.inl にあります。

// Broadcast() の宣言を抜粋

public:
	/**
	 * Broadcasts this delegate to all bound objects, except to those that may have expired.
	 *
	 * The constness of this method is a lie, but it allows for broadcasting from const functions.
	 */
	void Broadcast(ParamTypes... Params) const

public で宣言してます…。

Broadcast() を外部から呼べなくすることで、意図せず Broadcast() されることを防ぐことができる…というのが、イベント・デリゲートの特徴として紹介されていますが、現状、イベントクラスを外部に出した時点で Broadcast() し放題です。
だからといって、イベントクラスを外部に出さないようにしてしまうと、どこからでもバインドできる…という特徴と矛盾が生じます。

略

// こういう使い方しかできないようにするつもりだった?
UCLASS()
class USample : public UObject, public FDerived
{
	GENERATED_BODY()
public:
	USample()
	{
		this->AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("Inner Derived Lambda()")); });

		OuterDerived = MakeShareable(new FDerived());
		OuterDerived->AddLambda([](){ UE_LOG(LogTemp, Log, TEXT("Outer Derived Lambda()")); });

		// 内部呼び出しなのでOK
		this->Broadcast();

		// 外部からの呼び出しなのでNG
		OuterDerived->Broadcast();
	}
	TSharedPtr<FDerived> OuterDerived;
};

現状、自前でラッパークラスを作るか、エンジンのソースコードを修正するしかないようです。
ラッパークラスは以下のようなものになります。


// クラス設計の例として記載しているだけですので、戻り値と引数は省略しています。

template <typename OwningType>
class TEventWrapper
{
public:
	AddStatic()
	AddLambda()
	AddWeakLambda()
	AddRaw()
	AddSP()
	AddThreadSaveSP()
	AddUFunction()
	AddUObject()
	Remove()
	Clear()
protected:
	Broadcast()
	IsBound()
	IsBoundToObject()
private:
	friend OwningType;
	イベントクラス MyEvent;
};

結構な大きさのテンプレートクラスになりそうです。
作る手間を考えると、マルチキャスト・デリゲートで間に合わせることになりそうです。

バージョン 4.23 で追加されたデリゲート
ダイナミック・マルチキャスト・スパース

使い方の説明がどこにもなかったため、ソースコードを読みつつ試行錯誤しました。

※これから説明する内容は、Epic Games に確認をとっておらず、また、誰かと議論したわけでもなく、私ひとりの判断によるものなので、勘違いしている可能性があります。

ソースコードは以下のパスにあります。
Engine/Runtime/CoreUObject/Public/UObject/SparseDelegate.h
Engine/Runtime/CoreUObject/Private/UObject/SparseDelegate.cpp

ソースコードに記載されている説明

Sparse delegates can be used for infrequently bound delegates so that the object uses only
1 byte of storage instead of having the full overhead of the delegate invocation list.
The cost to invoke, add, remove, etc. from the delegate is higher than using the delegate
directly and thus the memory savings benefit should be traded off against the frequency
with which you would expect the delegate to be bound.

日本語訳
スパース・デリゲートは頻繁にバインドしないデリゲートに使用することができ、マルチキャスト・デリゲートの実行リスト(invocation list)のように巨大なオーバーヘッドを持つのではなく、1 バイトしか使用しません。
Invoke(), Add*(), Remove*() などのデリゲート用関数を呼び出すコストは、他のデリゲートよりも高いです。
そのため、メモリ節約のメリットはゲリゲートをバインドする頻度とトレード・オフされるべきです。

たまにしか使わない&遅くてもいいから、メモリ節約したい…という用途で使うダイナミック・マルチキャスト・デリゲートです。

宣言用マクロ

DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE( SparseDelegateClass, OwningClass, DelegateName )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam( SparseDelegateClass, OwningClass, DelegateName, Param1Type, Param1Name )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_TwoParams( SparseDelegateClass, OwningClass, DelegateName, Param1Type, Param1Name, Param2Type, Param2Name )
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_ThreeParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_FourParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_FiveParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SevenParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_EightParams(略)
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_NineParams(略)

他の宣言用マクロと異なり、引数の型と引数名を指定する必要があります。
値を返すデリゲートは宣言できません(バインドはできそう)。

OwningClass は UObject を継承している必要があります。

使用例
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Sample.generated.h"

UCLASS()
class USample : public UObject
{
	GENERATED_BODY()
public:
	DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(FSampleSparseDelegate, USample, SampleSparseDelegate, int, a);
	USample()
	{
		SampleSparseDelegate.AddDynamic(this, &USample::SampleFunction);
	}
	void Invoke()
	{
		SampleSparseDelegate.Broadcast(123);
	}
	UFUNCTION()
		void SampleFunction(int a) { UE_LOG(LogTemp, Log, TEXT("SampleFunction(%d)"), a); }
private:
	FSampleSparseDelegate SampleSparseDelegate;
};

参考にした資料

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です