PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

UE4|IUE5 Delegate, EventDispatcherをC++で書く方法

目次

環境

・JetBraings Rider 2022.1.1
・Unreal Engine5.1.1

Unreal Engineで使用可能なDelegateについて

Delegateの種類について
シングルキャスト、マルチキャスト、ダイナミック(動的)、イベント等の種類があります。
公式docも併せて参照されるとより理解が深まるかと思います。
デリゲート | Unreal Engine ドキュメント
Delegates and Lamba Functions in Unreal Engine | Unreal Engine 5.1 Documentation

・シングルキャスト(Singlecast)
 関数を1つだけデリゲートにバインドできます。
マルチキャスト(Multicast)
 複数の関数をバインドしてすべての関数を一度に実行できます。
・ダイナミック(Dynamic)
 シリアライズ可能でリフレクションを使って関数を呼び出すことができます。
通常のデリゲートよりも動作が遅くなります。
・イベント(Event)
 複数の関数をバインドしてすべての関数を一度に実行できます。
マルチキャストに近いですが、任意のクラス内で定義して使います。
イベントをデリゲートするクラスのみがそのイベントの Broadcast、IsBound、Clear 関数を呼び出すことが可能で
外部クラスが Broadcast、IsBound、Clear 関数を呼び出さないようにしています。

デリゲートの宣言について
引数なし~最大引数9個までの登録が可能です
Engineコードでは下記に宣言されています。
(EngineDir)\Engine\Source\Runtime\Core\Public\Delegates\DelegateCombinations.h

// 基本の宣言
DECLARE_DELEGATE(DelegateName);
DECLARE_MULTICAST_DELEGATE(DelegateName);
DECLARE_DYNAMIC_DELEGATE(Delegate);
DECLARE_EVENT(OwningType, EventName);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(DelegateName);

// 戻り値有
DECLARE_DELEGATE_RetVal(ReturnValueType, DelegateName);

// 引数有
DECLARE_DELEGATE_OneParam(DelegateName, ValueType);
DECLARE_DELEGATE_TwoParams(DelegateName, ValueType1, ValueType2)
... Nineまであります。

// 戻り値、引数有
DECLARE_DELEGATE_RetVal_OneParam(ReturnValueType, DelegateName, ValueType)
DECLARE_DELEGATE_RetVal_TwoParam(ReturnValueType, DelegateName, ValueType1, ValueType2)
// 実際に宣言を行う際の使用例
DECLARE_DELEGATE(FOnPostEditChangeOwner);
FOnPostEditChangeOwner OnPostEditChangeOwner;

DECLARE_DELEGATE_RetVal(FString, FOnGetSelectedCurve);
FOnGetSelectedCurve OnGetSelectedCurve;

DECLARE_DELEGATE_OneParam(FOnIsTransitioningChanged, bool);
FOnIsTransitioningChanged OnIsTransitioningChanged;

DECLARE_DELEGATE_RetVal_OneParam(bool, FOnIsAssetAutoReimportAvailable, UObject*);
FOnIsAssetAutoReimportAvailable IsAssetAutoReimportAvailableHandler;

// DECLARE_DYNAMIC_MULTICAST_DELEGATEに関してはBlueprintと連携可能
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FConnectedToSignallingServer);
// Blueprintからアサイン可能にするためにBlueprintAssignableを付与する
UPROPERTY(BlueprintAssignable, Category = "Pixel Streaming Delegates")
FConnectedToSignallingServer OnConnectedToSignallingServer;

// Blueprintからのアサイン、呼び出しを可能にする場合には
// BlueprintAssignable, BlueprintCallableを付与する
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDynamicOnFinalPassRendered, class ACompositingElement*, CompElement, UTexture*, Texture);
UPROPERTY(BlueprintAssignable, BlueprintCallable, DisplayName=OnFinalPassRendered, Category="Composure")
FDynamicOnFinalPassRendered OnFinalPassRendered_BP;

C++Delegateを記述する方法

まずは新規にActorを追加した場合のheaderの記述

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "(ActorName).generated.h"

// 1. delegateの宣言を追加する場合にはGlobalスコープに追加することが多いです。
// ここに追加

UCLASS()
class (ModuleName)_API A(ActorName) : public AActor
{
    GENERATED_BODY()
	
public:
    A(ActorName)();
protected:
    virtual void BeginPlay() override;
public:	
    virtual void Tick(float DeltaTime) override;

    // 2. class内でdelegateの宣言を書くことも可能で
    // Blueprint連携する場合にはこちらに書くことが多いです
    // ここに追加
};

次にdelegateに処理を結合するための関数を見ていきます。
基本的には以下があります。
今回はこの中から一部の使い方を解説します。

BindStatic(); // staticなグローバル関数を結合
BindRaw(); // 生のC++ポインタdelegateを結合
BindLambda(); // ラムダ関数を結合
BindWeakLambda();
BindSP(); // shared pointerベースのメンバ関数を結合
BindThreadSafeSP();
BindUFunction(); // UFUNCTION()のメンバ関数を結合
BindUObject(); // UObject継承のクラスのメンバ関数を結合

Singlecast delegateの場合(DECLARE_DELEGATE)

まずこういった使い方はしないのですが、ctorでbindしてBeginPlayで呼び出しを行っています
BindLambdaを使用したシンプルな実装例です

...

DECLARE_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.BindLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call singlecast delegate."));
        });
    }

protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Execute();
        }
    }
    ...
};

実行するとBindした処理がExecuteで呼ばれていることが確認できます

余談ですがOutput LogのFiltersからErrorsのみやWarningsのみの表示が便利です

Multicast delegateの場合(DECLARE_MULTICAST_DELEGATE)

続いて、同様にMulticast delegateのケースも書いていきます。
以下の例では1つの処理を登録していますが
実際にはMulticast delegateなので複数の処理を登録することができます。
Singlecast delegateとの違いは処理の登録がBindではなく、Addになっている点と
実行時の処理がExecuteではなく、Broadcastになっている点です。

...
DECLARE_MULTICAST_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.AddLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call multicast delegate."));
        });
    }

protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
    ...
};

以下が実行結果になります。
Singlecastのケースとほとんど同じように使用することが出来ます。

Dynamic singlecast delegateの場合(DECLARE_DYNAMIC_DELEGATE)

Dynamic delegateシリアライズ可能でリフレクションを使って関数を呼び出すことができます。
要約するとBindした処理を関数名で検索することが出来ます。
ただし、Dynamic delegateは通常のSinglecast delegateと比較して動作が遅くなるという特徴があります。
また、BindDynamicで登録する関数には必ずUFUNCTIONを付与する必要があります。

...
DECLARE_DYNAMIC_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    // Dynamic delegateにBindする関数にはUFUNCTION()が必須となる
    UFUNCTION()
    void OnCallDynamic()
    {
        UE_LOG(LogTemp, Error, TEXT("Call dynamic singlecast delegate."));
    }

    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.BindDynamic(this, &AExampleDelegateActor::OnCallDynamic);
    }

protected:
    virtual void BeginPlay() override
    {
        // IsBound() でチェックしてExecute() をする場合と同様に、関数がBindされていれば実行する
        ExampleDelegate.ExecuteIfBound();
    }
    ...
};

以下が実行結果になります。

Eventの場合(DECLARE_EVENT)

Eventを使うユースケースがイマイチ掴めておらずなのですが…
公式docにも「イベントは マルチキャスト デリゲート と非常によく似ています」と記載があるように似た機能ではあります。
内部的にはMulticast delegateなのかなと思います。
相違点として、戻り値を指定できない点と実行できるのは宣言したクラスだけであるということなのですが
実際にはどこからでも呼び出しが出来てしまい、イマイチ有用性が理解できておらずです。

...
DECLARE_EVENT(AExampleDelegateActor, FExampleEvent);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
	GENERATED_BODY()

	FExampleEvent ExampleEvent;
public:
    // 別クラスから OnUpdate().AddSP(this, &AnotherClass::Function)のような使い方をするのが良さそう?
    FExampleEvent& OnUpdate()
    {
        return ExampleEvent;
    }

    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleEvent.AddLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call event."));
        });
    }
protected:
    virtual void BeginPlay() override
    {
        if (ExampleEvent.IsBound())
        {
            ExampleEvent.Broadcast();
        }
    }
};

実行結果(Multicast delegateと同様)

Dynamic Multicast delegateの場合(DECLARE_DYNAMIC_MULTICAST_DELEGATE)

これは先程紹介したDynamic delegateとMulticast delegateを合わせたものです。
シリアライズ可能で複数Bindが可能なdelegateとなっています。
この後に紹介するBlueprint連携でも使用されるため、使う機会は他より多いと思います。
ただし、負荷的には一番大きなものになりますね。

...
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FExampleDynamicMulticast);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDynamicMulticast ExampleDelegate;
public:
    UFUNCTION()
    void OnDynamiMulticast()
    {
        UE_LOG(LogTemp, Error, TEXT("Call dynamic multicast."));
    }
	
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.AddDynamic(this, &AExampleDelegateActor::OnDynamiMulticast);
    }
protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
};

実行結果は以下です。

Blueprintで呼び出し可能なDelegateC++で記述する方法

ここから本題というか、特に使うケースが多いものになると思います。
基本的には先程紹介したDynamic multicast delegateを使うことで
Blueprint上でEvent Dispatcherを追加したのと同様の処理を組むことができます。

Dynamic Multicast delegateを使用する(Event Dispacher相当)

BlueprintからアクセスするためにはDelegateの宣言をした変数に

UPROPERTY(BlueprintAssignable)

を付与するだけです。

まずはC++側のコードから書いていきます。

...
UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FExampleDynamicMulticast);
    UPROPERTY(BlueprintAssignable)
    FExampleDynamicMulticast ExampleDelegate;

public:
    AExampleDelegateActor();
protected:
    virtual void BeginPlay() override;

    // BlueprintからDelegateを実行するための関数を用意
    UFUNCTION(BlueprintCallable)
    void Invoke()
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
};

上記のC++クラスを継承したBlueprintを用意します。
ここで先程のDelegate変数のExampleDelegateを打つと、Event Dispatcherと同じように扱うことができます。

Delegateに対してCustomEventをBindしてログを出力するようにしました。
Bindしただけでは実行されないため、先ほど作成したInvoke() 関数を使用して実行しています。

実行結果は以下になります。

Singlecast delegateを使用する(実はできる)

DECLARE_DYNAMIC_DELEGATEで宣言したDelegateでBlueprintと連携することも可能です。
この場合は、BlueprintAssignableではなくBlueprintReadWriteの属性を付与します。

...
UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
	GENERATED_BODY()

public:
	AExampleDelegateActor();

protected:
    // BlueprintReadWriteな変数なのでprivateではエラーになる
    DECLARE_DYNAMIC_DELEGATE(FExampleDynamicDelegate);
    UPROPERTY(BlueprintReadWrite)
    FExampleDynamicDelegate ExampleDelegate;

    virtual void BeginPlay() override;

    // BlueprintからDelegateを実行するための関数を用意
    UFUNCTION(BlueprintCallable)
    void Invoke()
    {
        ExampleDelegate.ExecuteIfBound();
    }
};

この場合のBlueprint連携はBindではなく、Setterを使用します。
そのあとにEventを繋ぐだけで処理を追加することが出来ます。
さきほど紹介したEventdispatchersの方式に比べて処理が軽いという利点もあります。
ただし、Singlecastなので複数の処理を登録することはできない点に注意です。

実行結果

まとめ

Delegate自体は複数用意されていて、記述方法も独特なため最初は困惑するかと思いますが
慣れてしまえばそんなに苦戦することもないと思います。
また、よく使われるケースとしてBlueprint連携があるかと思いますが
こちらについては主に使用されるのは上記の2つだと思いますので、覚えることも少なくて済むと思います。
他にも引数がある場合にはC++であれば型名のみでよくて、Blueprint連携の場合には型名と引数名が必要だったりと微妙に違う点もあります。


おまけ
SetTimer系の処理をC++で書く方法については以前に記事にしましたので、こちらを参照ください。
papersloth.hatenablog.com
BlueprintでDelegateを引数で使用する方法(ハック的な手法)も以前に記事にしていますので、こちらも必要に応じて活用ください。
papersloth.hatenablog.com

参考資料
Unreal Engine でのデリゲートとラムダ関数 | Unreal Engine 5.1 ドキュメント
Delegates in UE4, Raw C++, and BP Exposed - UE4: Guidebook