Blueprintの動的バインディングに関して

はじめに

Blueprintのではマルチキャストデリゲートに関して、イベントをバインドする事が出来ます。

コチラの設計思想はとても大好きなのですが、バインドする対象が多くなってくると視認性が非常に落ちるという欠点が存在しています。

Actor、UserWidgetなどの一部のクラスでは動的バインディング?(正式名称が分からないので誰か教えて下さい)を扱う事が出来ます。

この機能は、詳細タブからクリックする事で自動的にイベントが作成されて、自動的にバインディングまで行ってくれるという素晴らしい機能です。

視認性が非常に優れているのもメリットだと思います。 本記事では、動的バインディングに関して、詳細を調べてきましたので、そのまとめ記事となります。

Actorの動的バインディングに関して

AActor::ExecuteConstruction関数でバインディング処理を呼んでいるようです。


bool AActor::ExecuteConstruction(const FTransform& Transform, const FRotationConversionCache* TransformRotationCache, const FComponentInstanceDataCache* InstanceDataCache, bool bIsDefaultTransform)
{
    // 省略

    // この処理でバインディングを行っているようです。
    // Bind any delegates on components
    UBlueprintGeneratedClass::BindDynamicDelegates(GetClass(), this); // We have a BP stack, we must have a UBlueprintGeneratedClass...

大雑把ですが、処理が呼ばれる順番は下記の表になります。

順番 処理 詳細
1 AActor::UserConstructionScript コンストラクションスクリプト
2 UBlueprintGeneratedClass::BindDynamicDelegates 動的バインディング
3 AActor::BeginPlay 開始処理

その為、コンストラクションスクリプトか、スポーン時タイミングで、動的バインディングに使われるオブジェクトの変数が初期化されていれば正しく動作します。

スポーン時に変数を初期化するか コンストラクションスクリプトで変数を初期化する必要があります。

自作の型で動的バインディングの設定する方法

動的バインディングをする為の条件

詳細タブに動的バインディングのUIが表示される為の条件を調べてみました。

詳細タブのイベント関連のコードはFBlueprintDetails::AddEventsCategory関数で行っているようです。

UBlueprint::AllowsDynamicBinding の可否で判定している模様

void FBlueprintDetails::AddEventsCategory(IDetailLayoutBuilder& DetailBuilder, FName PropertyName, UClass* PropertyClass)
{
    UBlueprint* BlueprintObj = GetBlueprintObj();
    check(BlueprintObj);

    // ここで、チェックをしてUIの表示できるかのチェックを行っています。
    // Check for Ed Graph vars that can generate events
    if ( PropertyClass && BlueprintObj->AllowsDynamicBinding() )
    {

UBlueprint::AllowsDynamicBindingでは、アクターかどうかで区別しているようです。

その為、UBlueprintを置き換える事が出来ればUIを表示できる事が分かります。

独自型専用の、UBlueprint派生の型を用意する事で解決するようです。

bool UBlueprint::AllowsDynamicBinding() const
{
    return FBlueprintEditorUtils::IsActorBased(this);
}

UBlueprintの置き換え

独自Blueprintを実装しているUserWidget関連のコードを見てみます。

UWidgetBlueprint では、実装しているモジュールはEditorの物になります。

独自Blueprintで特殊な事をしない限りは、エディターモジュールで実装して問題無いようです。

今回の記事ではエディターモジュールでの実装とします。

実装例のコードを下記に示します。

ヘッダーファイル

UCLASS(BlueprintType)
class UHogeBlueprint : public UBlueprint
{
    GENERATED_BODY()

public:

    /** @return true if the blueprint supports event binding for multicast delegates */
    virtual bool AllowsDynamicBinding() const;
};

// Blueprintコンパイラ
class FHogeBlueprintCompiler : public IBlueprintCompiler
{

public:
    FHogeBlueprintCompiler() {}

    bool CanCompile(const UBlueprint* Blueprint) override;
    void PreCompile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions) override {}
    void Compile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results) override {}
    void PostCompile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions) override {}
    bool GetBlueprintTypesForClass(UClass* ParentClass, UClass*& OutBlueprintClass, UClass*& OutBlueprintGeneratedClass) const override;

};

ソースファイル

/** @return true if the blueprint supports event binding for multicast delegates */
bool UHogeBlueprint::AllowsDynamicBinding() const
{
    // 動的バインディングを常にするのでtrue
    return true;
}

bool FHogeBlueprintCompiler::CanCompile(const UBlueprint* Blueprint)
{
    return Cast<UHogeBlueprint>(Blueprint) != nullptr;
}

// この関数で、独自型と独自Blueprintクラスを紐づける必要があります。
bool FHogeBlueprintCompiler::GetBlueprintTypesForClass(UClass* ParentClass, UClass*& OutBlueprintClass, UClass*& OutBlueprintGeneratedClass) const
{
    if (ParentClass == UHogeScript::StaticClass() || ParentClass->IsChildOf(UHogeScript::StaticClass()))
    {
        OutBlueprintClass = UHogeBlueprint::StaticClass();
        OutBlueprintGeneratedClass = UBlueprintGeneratedClass::StaticClass();
        return true;
    }
    return false;
}

モジュールソースコード

class FSimpleBindingExampleEditorModule : public IModuleInterface
{
public:
    /** IModuleInterface implementation */
    void StartupModule() override
    {
        // ここでコンパイラに登録を行う
        IKismetCompilerInterface& KismetCompilerModule = FModuleManager::LoadModuleChecked<IKismetCompilerInterface>("KismetCompiler");
        KismetCompilerModule.GetCompilers().Add(&HogeBlueprintCompiler);
    }
    void ShutdownModule() override
    {
        // ここで登録解除を行う
        if (IKismetCompilerInterface* KismetCompilerModule = FModuleManager::GetModulePtr<IKismetCompilerInterface>("KismetCompiler"))
        {
            KismetCompilerModule->GetCompilers().Remove(&HogeBlueprintCompiler);
        }
    }
private:
    FHogeBlueprintCompiler HogeBlueprintCompiler;
};

ここまで実装すると、詳細タブにイベントの項目が表示されるようになります。

独自型の動的バインディングの設定方法

UBlueprintGeneratedClass::BindDynamicDelegatesを使う事で、動的バインディングを行う事が出来ます。

UBlueprintGeneratedClass::UnbindDynamicDelegates で解除をする事も出来ますが... 残念ながらエディター専用の処理となっていますので基本的に、一度バインドしたら解除せずに削除するのが方針だと思われます。

マルチキャストデリゲートから呼ばれないようにする為には、GCでUOBjectが削除されるかMarkAsGarbageを呼ぶ必要があります。

終了時には必ず呼ぶようにしましょう。

void UHogeScript::Initialize()
{
    // Bind any delegates on components            
    UBlueprintGeneratedClass::BindDynamicDelegates(GetClass(), this); // We have a BP stack, we must have a UBlueprintGeneratedClass...
}

void UHogeScript::Finalize()
{
    // 終了時にMarkAsGarbageを呼ぶ必要があります。
    // 呼ばないと、GCで消えるまで、Broadcastで登録したイベントが呼ばれてしまいます。
    MarkAsGarbage();
}

サンプルコード

github.com

本記事のサンプルコードを用意しました。