LoginSignup
26
17

More than 1 year has passed since last update.

UE4 イベント処理(デリゲート)についてのメモ

Last updated at Posted at 2019-06-29

概要

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++】デリゲートの使い方まとめ

テスト1:SetTimerByEvent

タイマー処理をする SetTimerByEvent を使ってのメソッド呼び出しです。

Unreal C++側実装

Test.cppにて呼び出されるメソッドを実装します。
"TestEvent!" をログに表示されるだけのものです。

Test.h

    UFUNCTION(BlueprintCallable, Category = "Test")
        void TestEvent() { UE_LOG(LogTemp, Log, TEXT("TestEvent!")); }

BluePrint側実装

Test.cppを継承したブループリントBP_Test.uassetにて以下の様に実装します。
Timeはとりあえず3.0、ループはonにしてみます。

TestEventBP.png

上記BPをUnrealC++で書くと以下の様になります。
BindUFunction() の第2引数にてデリゲートにバインドするメソッドを文字列指定していますが、Bind系メソッドはラムダ式を使うものなど他にいろいろあるようです。

Test.cpp
// コンストラクタ
ATest::ATest(){
    FTimerDynamicDelegate _Delegate;
    _Delegate.BindUFunction(this, "TestEvent");
    UKismetSystemLibrary::K2_SetTimerDelegate(_Delegate, 3.0f, true);
}

結果

3秒ごとに [TestEvent!] がアウトプットログに表示されます。

outputlog.png

追記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()で使用中かどうかの確認をすると良いです。間違って踏みつぶすとタイマー処理が実行されません。:cry:

また、引き数がある場合は、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);

また BindWeakLambdaCreateWeakLambda など弱参照なオブジェクトをバインドできるメソッドもあります。第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!"を表示されるだけの実装です。

ATest.h
    UFUNCTION(BlueprintCallable, Category = "Test")
        void OnCompHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

ATest.cpp
void ATest::OnCompHit(
        UPrimitiveComponent* HitComp,
        AActor* OtherActor,
        UPrimitiveComponent* OtherComp,
        FVector NormalImpulse,
        const FHitResult& Hit
        )
{
    UE_LOG(LogTemp, Log, TEXT("OnHit!"));
}

BluePrint側実装

テストコードでは引数使っていませんが一応全部つなぎます。

OnCompHitBP.png

上記をUnrealC++側で書くと

ATest.cpp
void ATest::PostInitializeComponents(){
    CapsuleComponent->OnComponentHit.AddDynamic(this, &ATest::OnCompHit);
}

コンポーネントヒット用デリゲートに追加しています。

結果

カプセルコリジョンヒット時に [OnHit!] がアウトプットログに表示されます。

テスト3:動的マルチキャストデリゲート

Unreal用語的には イベントディスパッチャー
メソッドを2つ登録して呼び出ししてみます。

UnrealC++側処理

ATest.h
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();

};  
ATest.cpp

void ATest::BeginPlay()
{
    Super::BeginPlay();

    // 追加
    TestDelegate.AddDynamic(this, &ATest::TestEvent);
    TestDelegate.AddDynamic(this, &ATest::TestEvent2);
}

void ATest::CallDelegate() {
    // 呼び出し
    TestDelegate.Broadcast();
}

BluePrint側実装

[z]キーを押して [CallDelegate]を呼び出す処理にしてみます。

call_delegate.png

結果

マルチキャストなので、[TestEvent!][TestEvent2] がアウトプットログに表示されます。

result3.png

追記

引き数が必要な場合は、以下の様に引き数の数に応じて _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)まで書く必要があるのはブループリント側でのイベントバインド時の表示をするためのようです。
delegate_oneparam.png

ブループリントに公開しないデリゲート(Dynamicではないデリゲート)の宣言は変数名は不要です。

ATest.h
// int引数1つ取るBP非公開デリゲート
DECLARE_DELEGATE_OneParam(FNoBPEvent, int);

テスト4:イベント

Unreal用語的には 動的デリゲート
これもメソッドを2つ登録して呼び出ししてみます。

UnrealC++側実装

ATest.h
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();
};
ATest.cpp
void ATest::BeginPlay()
{
    Super::BeginPlay();

    // 追加
    TestOnEvent.BindUFunction(this, "TestEvent");
    TestOnEvent.BindUFunction(this, "TestEvent2");
}

void ATest::CallOnEvent() {
    // 呼び出し
    TestOnEvent.ExecuteIfBound();
}

マルチキャストデリゲートのBroadcast()と違い、Execute()ExecuteIfBound()はブループリント側からは直接は呼び出せないようです。

Blueprint側実装

callonevent.png

結果

イベントはシングルキャストなので2つバインドしていますが、[TestEvent2!]だけ(最後にバインドした処理だけ)アウトプットログに表示されます。

result4.png

追記

マルチキャストデリゲートと同様に引き数を取る場合は、宣言時に _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++で行うことができます。
内部的にはイベントで行われています。

ATest.h
// 実装はBP側でC++では宣言のみ
UFUNCTION(Category = "Test", BlueprintImplementableEvent, BlueprintCallable)
    void TestImpl(float _DeltaTime);

BP側では以下のように処理を実装できます。
ImplementableEvent.png

BPが継承関係になっている場合、親クラスの実装も呼ぶことができます。
ParentImplementableEvent.png

この方法は実装や流れがシンプルなのでわかりやすいです。

まとめ

用語が とても 紛らわしい。。
C#の delegate も変数を外部からのアクセスを制限するために event キーワードによる呼び出しがあるように、UnrealEngineのイベントディスパッチャーと動的デリゲートもそのような関係にあるのではないかと思います。

あと基本的にブループリントでの都合を重視しているためUnrealC++のみで考えると不要な実装も多いため混乱します。(DECLARE_DYNAMIC_DELEGATE_OneParamの宣言など)
UnrealC++だけで使用する場合は、(多分)動的デリゲートのほうが処理がかかると思いますので、通常デリゲート(静的デリゲート?)も使い分けたほうがいいと思われます。:worried:

26
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
17