概要
UnrealEngine4でのイベント処理(デリゲート)とその関連処理についてのメモ書きです。タイマー処理についても書いています。
ブループリントでのイベント処理をUnrealC++で実装する場合の検証も行っています。
修正履歴
日付 | 内容 |
---|---|
2019/08/03 | BlueprintImplementableEventについて追記 |
2019/08/04 | タイマー処理について追記 |
2019/10/18 | TFunction<>宣言とラムダ式の引き数型違い時について追記 |
2019/12/09 | タイマーハンドルのIsVaild()についての追記 |
2020/11/27 | Weak参照のラムダ(BindWeakLambda)についての追記 |
環境
Windows10
Visual Studio 2017
UnrealEngine 4.22, 4.25
参考
UnrealC++のデリゲート、イベントについて
Unrealマニュアル-デリゲート
UE公式:TBaseDelegate::BindWeakLambda
【UE4 C++】デリゲートの使い方まとめ
UE4|IUE5 Delegate, EventDispatcherをC++で書く方法
テスト1:SetTimerByEvent
タイマー処理をする SetTimerByEvent を使ってのメソッド呼び出しです。
Unreal C++側実装
Test.cppにて呼び出されるメソッドを実装します。
"TestEvent!" をログに表示されるだけのものです。
UFUNCTION(BlueprintCallable, Category = "Test")
void TestEvent() { UE_LOG(LogTemp, Log, TEXT("TestEvent!")); }
BluePrint側実装
Test.cppを継承したブループリントBP_Test.uassetにて以下の様に実装します。
Timeはとりあえず3.0、ループはonにしてみます。
上記BPをUnrealC++で書くと以下の様になります。
BindUFunction() の第2引数にてデリゲートにバインドするメソッドを文字列指定していますが、Bind系メソッドはラムダ式を使うものなど他にいろいろあるようです。
// コンストラクタ
ATest::ATest(){
FTimerDynamicDelegate _Delegate;
_Delegate.BindUFunction(this, "TestEvent");
UKismetSystemLibrary::K2_SetTimerDelegate(_Delegate, 3.0f, true);
}
結果
3秒ごとに [TestEvent!] がアウトプットログに表示されます。
追記1:タイマー処理の削除
タイマー処理の削除は UKismetSystemLibrary::K2_SetTimerDelegate()
の返り値 FTimerHandle
を保持して以下の様に K2_ClearAndInvalidateTimerHandle
を呼ぶことにより実装できます。
// タイマー処理の削除
UKismetSystemLibrary::K2_ClearAndInvalidateTimerHandle(GetWorld(), TimerHandle);
追記2:C++のみでタイマー処理を行う例
タイマー処理をC++のみで行う場合は、ラムダ式で指定をすると便利です。
// 任意時間後に実行する処理
TFunction<void(void)> _Func = [this]() {
UE_LOG(LogTemp, Log, TEXT("Test!"));
};
FTimerHandle _Handle;
// 1秒後にセット
GetWorld()->GetTimerManager().SetTimer(_Handle, (TFunction<void(void)>&&)_Func, 1.0f, false);
上記のテストコードはFTimerHandle
をローカル変数で使っていますが、メンバ等などで保持&使いまわしをする場合は、Invalidate()
で初期化と IsVaild()
で使用中かどうかの確認をすると良いです。間違って踏みつぶすとタイマー処理が実行されません。
また、引き数がある場合は、FTimerDelegate
を使います。
FTimerHandle _Handle;
FTimerDelegate _TimerDelegate;
// 任意時間後に実行する処理(引数あり)
TFunction<void(float)> _Func = [this](float _Val) {
UE_LOG(LogTemp, Log, TEXT("Test : %f"), _Val);
};
_TimerDelegate.BindLambda((TFunction<void(float)>&&)_Func, 1.0f);
// 2秒後にセット
GetWorld()->GetTimerManager().SetTimer(_Handle, _TimerDelegate, 2.0f, false);
ラムダ式ではなくメソッドを指定する場合はバインドするメソッド名が違うので注意。
// 同一クラスのメソッドTestFuncをデリゲートにバインドする場合
_TimerDelegate.BindUFunction(this, FName("TestFunc"), 1.0f);
また BindWeakLambda
や CreateWeakLambda
など弱参照なオブジェクトをバインドできるメソッドもあります。第1引数が違うので注意。
_TimerDelegate.BindWeakLambda(this, (TFunction<void(float)>&&)_Func, 1.0f);
追記3:コンパイルエラーが出ないケース
TFunction<>
での宣言と実際のラムダ式の引数の型が違う場合、なぜかコンパイルエラーがでません。
下記のコードの場合、1秒後に[Param:100]と出力されると期待されますが、実際は[Param:0]となってしまいます。
TFunction<void(float)> Func = [this](int32 _Param) { // 引数の型が宣言と違う
UE_LOG(LogTemp, Log, TEXT("Param:%d"), _Param);
};
TimerDelegate.BindLambda((TFunction<void(int32)>&&)Func, 100);
GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, 1.0f, false);
テスト2:コンポーネントのヒットイベント
カプセルコンポーネントを持つアクターの OnComponentHit
イベントです。コリジョンを持つコンポーネントのヒットイベント系の処理は全て同じだと思います。
UnrealC++側イベント実装
"OnHit!"を表示されるだけの実装です。
UFUNCTION(BlueprintCallable, Category = "Test")
void OnCompHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
void ATest::OnCompHit(
UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
FVector NormalImpulse,
const FHitResult& Hit
)
{
UE_LOG(LogTemp, Log, TEXT("OnHit!"));
}
BluePrint側実装
テストコードでは引数使っていませんが一応全部つなぎます。
上記をUnrealC++側で書くと
void ATest::PostInitializeComponents(){
CapsuleComponent->OnComponentHit.AddDynamic(this, &ATest::OnCompHit);
}
コンポーネントヒット用デリゲートに追加しています。
結果
カプセルコリジョンヒット時に [OnHit!] がアウトプットログに表示されます。
テスト3:動的マルチキャストデリゲート
Unreal用語的には イベントディスパッチャー
メソッドを2つ登録して呼び出ししてみます。
UnrealC++側処理
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTestDelegate);
UCLASS()
class TEST_API ATest : public APawn
{
GENERATED_BODY()
public:
// 呼び出されるメソッド1
UFUNCTION(BlueprintCallable, Category = "Test")
void TestEvent(){ UE_LOG(LogTemp, Log, TEXT("TestEvent!")); }
// 呼び出されるメソッド2
UFUNCTION(BlueprintCallable, Category = "Test")
void TestEvent2() { UE_LOG(LogTemp, Log, TEXT("TestEvent2!")); }
// デリゲート定義
UPROPERTY(BlueprintAssignable, Category = "Test")
FTestDelegate TestDelegate;
// 呼び出し
UFUNCTION(BlueprintCallable, Category = "Test")
void CallDelegate();
};
void ATest::BeginPlay()
{
Super::BeginPlay();
// 追加
TestDelegate.AddDynamic(this, &ATest::TestEvent);
TestDelegate.AddDynamic(this, &ATest::TestEvent2);
}
void ATest::CallDelegate() {
// 呼び出し
TestDelegate.Broadcast();
}
BluePrint側実装
[z]キーを押して **[CallDelegate]**を呼び出す処理にしてみます。
結果
マルチキャストなので、[TestEvent!] と [TestEvent2] がアウトプットログに表示されます。
追記
引き数が必要な場合は、以下の様に引き数の数に応じて _OneParam
や _TwoParams
を追加するようです。(2つ以上はParam s になることに注意)
// int型の変数名valueの引数1つを取る場合
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTestDelegate, int, value);
呼び出し側は以下の様になります。
// デリゲート呼び出し(int型引き数を1つ)
int InParam1 = 10;
TestDelegate.Broadcast(InParam1);
宣言時に引数の型(int)だけではなく、変数名(value)まで書く必要があるのはブループリント側でのイベントバインド時の表示をするためのようです。
ブループリントに公開しないデリゲート(Dynamicではないデリゲート)の宣言は変数名は不要です。
// int引数1つ取るBP非公開デリゲート
DECLARE_DELEGATE_OneParam(FNoBPEvent, int);
テスト4:イベント
Unreal用語的には 動的デリゲート
これもメソッドを2つ登録して呼び出ししてみます。
UnrealC++側実装
DECLARE_DYNAMIC_DELEGATE(FTestOnEvent);
UCLASS()
class TEST_API ATest : public APawn
{
GENERATED_BODY()
public:
// 呼び出されるメソッド1
UFUNCTION(BlueprintCallable, Category = "Test")
void TestEvent(){ UE_LOG(LogTemp, Log, TEXT("TestEvent!")); }
// 呼び出されるメソッド2
UFUNCTION(BlueprintCallable, Category = "Test")
void TestEvent2() { UE_LOG(LogTemp, Log, TEXT("TestEvent2!")); }
// イベント定義
UPROPERTY(BlueprintReadWrite, Category = "Test")
FTestOnEvent TestOnEvent;
// イベント呼び出し
UFUNCTION(BlueprintCallable, Category = "Test")
void CallOnEvent();
};
void ATest::BeginPlay()
{
Super::BeginPlay();
// 追加
TestOnEvent.BindUFunction(this, "TestEvent");
TestOnEvent.BindUFunction(this, "TestEvent2");
}
void ATest::CallOnEvent() {
// 呼び出し
TestOnEvent.ExecuteIfBound();
}
マルチキャストデリゲートのBroadcast()
と違い、Execute()
やExecuteIfBound()
はブループリント側からは直接は呼び出せないようです。
Blueprint側実装
結果
イベントはシングルキャストなので2つバインドしていますが、**[TestEvent2!]**だけ(最後にバインドした処理だけ)アウトプットログに表示されます。
追記
マルチキャストデリゲートと同様に引き数を取る場合は、宣言時に _OneParam や _TwoParams を追加して、型と変数名を追加していきます。
// 引数2つ、int型のvalue1とfloat型value2を取る場合
DECLARE_DYNAMIC_DELEGATE_TwoParams(FTestOnEvent, int, value1, float, value2);
返値がある場合の定義は _RetVal が後ろにつきます。更に引数が付く場合は _OneParam などを続けます。
// 返値型int、引数なしのデリゲート定義
DECLARE_DYNAMIC_DELEGATE_RetVal(int, FTestDelegate);
// 返値型int, 引数1つ(float型ValName)のデリゲート定義
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(int, FTestDelegate2, float, ValName);
この時、なぜか呼び出しメソッドの ExecuteIfBound() がないので IsBound() で確認してから Execute() で呼び出しをしないとなりません。
勘違いでした。普通にExecuteIfBound()
が使えます。
テスト5:BlueprintImplementableEvent
BlueprintImplementableEventを使うとC++側で宣言を行い実装はBP側で、呼び出しはC++で行うことができます。
内部的にはイベントで行われています。
// 実装はBP側でC++では宣言のみ
UFUNCTION(Category = "Test", BlueprintImplementableEvent, BlueprintCallable)
void TestImpl(float _DeltaTime);
BPが継承関係になっている場合、親クラスの実装も呼ぶことができます。
この方法は実装や流れがシンプルなのでわかりやすいです。
まとめ
用語が とても 紛らわしい。。
C#の delegate も変数を外部からのアクセスを制限するために event キーワードによる呼び出しがあるように、UnrealEngineのイベントディスパッチャーと動的デリゲートもそのような関係にあるのではないかと思います。
あと基本的にブループリントでの都合を重視しているためUnrealC++のみで考えると不要な実装も多いため混乱します。(DECLARE_DYNAMIC_DELEGATE_OneParam
の宣言など)
UnrealC++だけで使用する場合は、(多分)動的デリゲートのほうが処理がかかると思いますので、通常デリゲート(静的デリゲート?)も使い分けたほうがいいと思われます。