🌟

UE5:Unreal EngineのInterfaceについてまとめた

に公開

はじめに

Unreal Engine 5 の interface機能とくに UInterfaceについてまとめます。

個人的に UInterfaceについて理解が足らないと感じており、いまいちどう書けばいいのか、使いどころや選定基準が曖昧であったため、自己学習および将来の自分のためのメモとしてまとめることとします。さんざん擦られてきたUInterfaceですがイマイチ深く踏み込んだすべての疑問を解決してくれる記事がなかったので調べました。
主に C++ 側での実装についてまとめます。

BP側のinterface機能については 世にある記事をご参照ください。

検証環境

UE 5.5.4 ソースビルド

interfaceについて

C++にはいわゆるinterface と呼称される機能はありません。

本稿では純粋仮想関数を持つclass/struct のことを interface と呼称します。
以下、理解しやすさのためにinterface型には接頭辞Iを付けて宣言することとします。

// いわゆるインターフェース
class ICancelable
{
public:
    virtual void Cancel() = 0;
};

UInterfaceとの差別化のため、本稿ではpure C++で実装されたインターフェースのことをnative interfaceと呼称します。

UInterface の使い方

UInterface とは Unreal C++ における インターフェース機能のことです。

初めに公式ドキュメント UnrealEngineの インターフェース をご一読ください。
UInterfaceは 接頭辞Uで始まる型と接頭辞Iで始まる型を必要とします。
以降、UInterfaceの使い方について述べます。

1: C++ Only UInterface

「C++で 完結する UInterface」 のことを C++ Only UInterfaceと呼称しましょう。
C++ Only UInterfaceは BPで一切使わない C++ で定義・実装・使用・保持するUInterfaceです。UE5でも native interfaceは利用できるのですが、UObjectに実装する場合はUInterfaceを使った方がいいです。BP側には一切情報を露出させないようにしてみます。

C++ Only UInterfacenative interface と使いどころがほぼ同じです。

C++ Only UInterfaceの定義

サンプル事例として Idを提供するinterfaceを挙げましょう。

#include "CoreMinimal.h"

/**
* 例:オレオレId型で識別されしものが実装すべきinterface
* FFooObjectId の実際の型は主旨と無関係だから割愛.
*/
struct FFooObjectId{};

UINTERFACE(meta = (CannotImplementInterfaceInBlueprint))
class MYMODULE_API UFooObjectIdProvider : public UInterface
{
    GENERATED_BODY()
};

class MYMODULE_API IFooObjectIdProvider
{
    GENERATED_BODY()
public:
    virtual FFooObjectId GetFooObjectId() const = 0;
};

★ポイント

  • C++でしか使わないため純粋仮想関数で定義してよい
  • PURE_VIRTUALマクロは不要
  • UINTERFACE() 部分には BlueprintTypeBlueprintableは付与しない
  • C++でしか使わないためCannotImplementInterfaceInBlueprint を付与してBP実装させない

C++ Only UInterfaceの使用

Unreal C++に適合した方法で使用します。

例として、Hitした相手アクターの具象型を知らずにinterfaceを実装しているかで操作する事例です。相手が弾丸なのか壁なのか味方なのかはinterface経由で得てみましょう。

UCLASS()
class AUserActor : public AActor
{
    // HitしたOtherアクターがIdProviderかをチェックする
    // 相手のIdから相手が何者かを知れる
    void OnHit(AActor* Other)
    {
        if(!IsValid(Other)){ return; }

        // 使い方1: Castする
        if(const IFooObjectIdProvider* PureProvider = Cast<IFooObjectIdProvider>(Other))
        {
            const FFooObjectId Id = PureProvider->GetFooObjectId();
            if(Id == 対象なら)
            {
                // IFooObjectIdProvider* は nativeポインタなので 直接TScriptInterfaceに格納できない
                // TScriptInterface::operator=は UObject系を引数に取る
                HitObject = Other;
                // ただしTWeakInterfacePtrは nativeポインタを直接入れられる
                 // TWeakInterfacePtr::operator=は interface型を引数に取る
                HitObjectWeak = PureProvider;
            }
        }

        // 使い方2: TScriptInterfaceコンストラクタ呼び出し
        // TScriptInterface::operator bool による比較があるのでifスコープが使える
        if( TScriptInterface<IFooObjectIdProvider> Provider(Other))
        {
            const FFooObjectId Id = Provider->GetFooObjectId();
            if(Id == 対象なら)
            {
                HitObject = Provider;
                HitObjectWeak = Provider; //TScriptInterfaceからTWeakInterfacePtrへの変換
            }
        }

        // 使い方3: Implements()
        if( Other->Implements<UFooObjectIdProvider>())
        {
            /// 省略
        }

        // 使い方4: ImplementsInterface()
        if( Other->GetClass()->ImplementsInterface(UFooObjectIdProvider::StaticClass()))
        {
            /// 省略
        }
    }

protected:
    UPROPERTY()
    TScriptInterface<IFooObjectIdProvider> HitObject;

    UPROPERTY()
    TWeakInterfacePtr<IFooObjectIdProvider> HitObjectWeak;
};

使い方1は Cast<T>およびnullチェック方式です。Cast<T>IsValid()==falseであるときやキャストできないときはnullptrが返りますのでnullチェックで十分なのです。この方式は対象がT型を実装しておりかつそれを使いたいときに便利です。

使い方2はTScriptInterface に格納してみる方式です。コンストラクタでUObjectを代入すると、そのUObjectT型を実装しているかチェックします。T型を実装している場合、有効なTScriptInterfaceを構築します。実装していない場合、TScriptInterfacenullptrとして振る舞います。この方式は対象がT型を実装しているときにUPROPERTY()としてkeepしたいときに便利です。

使い方3は使い方4のシンタックスシュガーです。template関数であるためconstexprなコンパイル時チェックが走るという点で後述の使い方4よりも優れています。

使い方4は型情報で判定する方式です。この方式は対象UObjectT型を実装していることに意味があるときに使います。型タグで判断するような事例で便利です。

C++ Only UInterfaceの保持

interface*を保持するにはTScriptInterfaceに格納します。
弱参照で保持するには TWeakInterfacePtrに格納します。

UFooObjectIdProviderUObject派生型なのですが、IFooObjectIdProvidernative classですからこのままではUPROPERTY()として扱えません。そこで TScriptInterfaceに格納する必要があるのです。

UCLASS()
class UFooObjectManager : public UObject
{
    GENERATED_BODY()

public:
    void RegisterObject(UObject* Object)
    {
        TScriptInterface<IFooObjectIdProvider> Instance(Object);
        check(Instance); // interfaceを実装していないものを登録してはいけない!

        Objects.Add(Instance->GetFooObjectId(), Instance);

        // TScriptInterface -> TWeakInterfacePtr変換はassign operatorで簡単に行える
        ManagedObjectWeak = Instance;
    }

    void UnregisterObject(const FFooObjectId& Id)
    {
        // 略
    }

private:
    // Interface 型をkeepするときは 
    // UPROPERTY() TScriptInterfaceにすること
    // GCのReachable判定で到達可能にしてくれるのでGC回収を妨げる
    UPROPERTY()
    TScriptInterface<IFooObjectIdProvider> ManagedObject;

    // GC回収を妨げたくないときは TWeakInterfacePtrで弱参照に
    // UPROPERTY() は付与できない
    TWeakInterfacePtr<IFooObjectIdProvider> ManagedObjectWeak;

    // TScriptInterfaceはコンテナ型に格納してもよい
    UPROPERTY()
    TMap<FFooObjectId, TScriptInterface<IFooObjectIdProvider>> Objects;
};

ラムダキャプチャするときは 基本的にTWeakInterfacePtrを使って弱参照でキャプチャしましょう。TWeakInterfacePtr はコンストラクタかoperator=を使います。引数にはInterface*を与えます。

void UFooObjectManager::FooAsync()
{
    // 1. コンストラクタ を使う oneliner
    DoAsync([Weak = TWeakInterfacePtr(ManagedObject.GetInterface())]()
    {
        if(IFooObjectIdProvider* Provider = Weak.Get()){...}
    });

    // 2. operator= を使う
    TWeakInterfacePtr<IFooObjectIdProvider> Weak2 = ManagedObject.GetInterface();
    DoAsync([Weak2]()
    {
        if(IFooObjectIdProvider* P = Weak2.Get()){...}
    });
}
  • TObjectPtr<U> ←→ TScriptInterface<T>
  • TWeakObjectPtr<U> ←→ TWeakInterfacePtr<T>

TScriptInterface は内部にUObject*をもっていますので、UPROPERTY()であるかぎりGCを妨げることができる優れものです。

C++ Only UInterfaceの実装

interfaceの実装は2番目以降に継承します。
UInterfaceを実装せしものは UObject派生型でなければなりません。

UObject派生型であればAActorでもActorComponentでもなんでも構いません。

UCLASS()
class AConcreteImplActor : public AActor // 継承の1番目は必ずUObject派生型
    , public IFooObjectIdProvider   // interface継承は2番手以降でないとダメ
{
public:
    // IFooObjectIdProvider interface begin
    virtual FFooObjectId GetFooObjectId() const override { return Id; }
    // IFooObjectIdProvider interface end

private:
    // 実装は好きにしていい
    // 例としてエディタで設定するものとする
    UPROPERTY(EditAnywhere)
    FFooObjectId Id;
};

2番目以降にinterfaceを継承せねばならないのはおそらくシリアライズ処理の関係です。
thisポインタをUObject*にc-styleキャストしたりするのでメモリレイアウト上UObjectが先に来ないと困るからと思われます。

2: BP Callable UInterface

便宜上、「C++で実装してBPから使うだけのUInterface」のことをBP Callable UInterfaceと呼称しましょう。BPから関数を呼び出すだけでBP実装はしないinterfaceです。

BP側は具象型に依存する必要がなくなるため、C++側での実装変更が容易になります。全BPの親クラス差し替えやリコンパイルは大変なので、BP側は具象型ではなくinterface依存にしておくと将来楽になれるはずです。
BPコンパイル時間も速くなるようです。

BP Callable UInterfaceの定義

サンプル事例としてUI用のinterfaceを定義します。
キャラクターのステータス表示UIやBP上でステータスごとの分岐に使う想定です。

#include "CoreMinimal.h"

UINTERFACE(BlueprintType, meta=(CannotImplementInterfaceInBlueprint))
class UBuffStatusProvider : public UInterface
{
    GENERATED_BODY()
};

class IBuffStatusProvider
{
    GENERATED_BODY()
public:

    /** 毒状態ならばtrueを返す
    * UIに毒アイコンを出すために使用してよい
    */
    UFUNCTION(BlueprintCallable)
    virtual bool IsPoison() const = 0;
};

★ポイント

  • UINTERFACE(BlueprintType, meta=(CannotImplementInterfaceInBlueprint))なclass
  • NotBlueprintableは使わない
  • UFUNCTION(BlueprintCallable) な純粋仮想関数

UINTERFACE(BlueprintType, meta=(CannotImplementInterfaceInBlueprint))でBPでの使用・変数・Castできるけど、interfaceを実装したりoverrideできないことを明言しています。

BlueprintTypeはこのインターフェースがBP上で変数として保持できるようにするため付与しています。BP変数はTScriptInterface<T>として保持されるようです。

CannotImplementInterfaceInBlueprint でこのインターフェースの実装をBPでできなくします。BPの ClassSettings > Implement interface欄のリストに表示されません。今回は C++で実装してBPからは関数呼び出しだけをできるように制限したい事例ですので適切です。

NotBlueprintableでもBP実装は防げるのですが、CastやBP変数への保持ができなくなります。それは使い勝手が悪いのでCannotImplementInterfaceInBlueprintの方がよいです。

BPに公開したい関数にUFUNCTION(BlueprintCallable) を付与します。
純粋仮想関数をBP上からメッセージ経由で適切に呼び出せるようになります。

BP Callable UInterfaceのC++での使用

C++上は前回と全く同じです。Castして使ってください。

if( IBuffStatusProvider* Provider = Cast<IBuffStatusProvider>(Object))
{
    bool bIsPosion = Provider->IsPoison();
}

BP Callable UInterfaceのBPでの使用

このUInterfaceでやりたいことです。

BP上はTargetに対して IsPoison(Message) ノード か IsPoisonノードを使います。

  • お手紙アイコンがついていない方がInterfaceへのCall Functionノード
  • お手紙アイコンがついているノードがInterface Messageノード

です。

Interface MessageTargetがこのinterfaceを実装していれば関数を呼び出し正しい値を返します。実装していなければ内部実行せずに戻り値型のdefaultバリューを返し次の実行ピンへ進みます。詳しくは詳解をご参照ください。

インターフェースを実装しているかは Does Object Implement Interfaceノードでチェックできます。Castノードを使ってもいいです。

BP Callable UInterfaceのC++での保持

C++ Only UInterfaceと同じです。

BP Callable UInterfaceのBPでの保持

普通にBP変数でInterface型を指定するだけです。

BP Callable UInterfaceのC++での保持

C++ Only UInterfaceと同じです。

BP Callable UInterfaceのBPでの保持

CannotImplementInterfaceInBlueprintを付与して明示的にできないようにしているのでできません。想定通りです。一応確認しました。

比較のために実装可能なIBuffStatusProvider2を定義しました。

UINTERFACE(BlueprintType)
class UBuffStatusProvider2 : public UInterface{GENERATED_BODY()};
class IBuffStatusProvider2{ GENERATED_BODY()
public:
    UFUNCTION() virtual bool IsPoison2() const = 0;
};

IBuffStatusProvider はリストビューに表示されず、IBuffStatusProvider2が表示されていますね。CannotImplementInterfaceInBlueprintが付与されているのでIBuffStatusProvider実装できないのは正しい望んだ挙動です。一方、IBuffStatusProvider2CannotImplementInterfaceInBlueprintNotBlueprintableも付与されていないので実装できてしまいますね。


3: BP Implementable UInterface

C++側で呼び出しを担いBPがinterfaceを実装するパターンです。
便器上、C++で使用してBPで実装するUInterfaceのことをBP Implementable UInterfaceと呼称します。

サンプル事例としてダメージリアクションをBP上で実装します。ダメージリアクション呼び出し自体はC++上で実装される複雑なダメージ制御内で行われるものとします。BP側ではやられモーションやIK調整などC++から与えられた文脈に応じて制御するものとします。

BP Implementable UInterfaceの定義

#include "CoreMinimal.h"

UINTERFACE(BlueprintType, Blueprintable)
class UDamageReaction : public UInterface
{
    GENERATED_BODY()
};

class IDamageReaction
{
    GENERATED_BODY()
public:

    /** BPで実装する関数 */
    UFUNCTION(BlueprintImplementableEvent)
    void PlayReaction(FContext& Context);

    //virtual修飾は付けない
    //UFUNCTION(BlueprintImplementableEvent)
    //virtual void PlayReaction(FContext& Context);
}

★ポイント:

  • UINTERFACE(BlueprintType, Blueprintable) な class
  • UFUNCTION(BlueprintImplementableEvent) な非仮想関数

BPで実装するのでBlueprintableが必須です。

BlueprintTypeはお好みでどうぞ。BPから呼び出せる関数が一切ないためBlueprintTypeはなくてもいい気がしますが==ノードやIsValidで判定したいときはBlueprintTypeが必要になるでしょう。

BPで実装する関数は、UFUNCTION(BlueprintImplementableEvent) を付与します。

BlueprintCallable を付与するかしないかは設計次第です。今回の事例では C++に呼び出し責務をすべて任せてBP側に実装責務を負わせる設計にしますのでBlueprintCallableを付与しません。

この場合 virtualにしない

大変ややこしいですが、BlueprintImplementableEventならば virtual関数にはしません。
これはvirtualにするとC++でoverrideできちゃうからと推察されます。誤ってBlueprintImplementableEventな関数にvirtualをつけるとUHTが怒ってくれるので間違えることはありません。

BP Implementable UInterfaceのC++での使用

C++ で呼び出すときはUHTによって自動生成されるstatic関数を使用します。
この関数のことをThunk関数と呼びます。BP側で実装された関数を呼び出すためにはUHTにより自動生成された特別な関数を呼び出す必要があるのです。

Thunk関数は Execute_***で始まる関数です。
今回はIDamageReaction::Execute_PlayReaction()となります。

使用例
void DamageLogic::ResolveDamage(AActor* Offence, AActor* Defence)
{
    //複雑なダメージロジック...
    FContext Context;
    Context.Offence = Offence;
    Context.Damage = 123;

    ...中略...

    // 対象が被ダメージリアクションを実装せしものなら呼び出す
    if(Defence->Implements<UDamageReaction>())
    {
        IDamageReaction::Execute_PlayReaction(Defence, Context);
    }
}

Thunk関数経由での呼び出しになるため、Cast<T>は使いません。型情報から判断するためにImplements<T>()でチェックします。

このようにinterfaceにすることで、C++側は対象がキャラクターのようなリアクションを取るものなのか、壁や床のようにリアクションを取らないものなのかを気にせずに実装できました。
(interfaceではなくDelegateを使ったイベントストリームにする作戦もありっちゃありだと思います。設計次第)

BP Implementable UInterfaceのBPでの使用

意図的に BlueprintCallableを外してできなくしているのでできません。

BP Implementable UInterfaceのC++の保持

C++ Only UInterfaceと同様。

BP Implementable UInterfaceのBPの保持

普通に BP 変数にするだけ。

BP Implementable UInterfaceのC++実装

C++ Only UInterfaceと同様。

ただし、virtual関数を持たないのでなんもoverrideできません。

BP Implementable UInterfaceのBP実装

BP側で実装して overrideします。

  1. ClassSettings を押す
  2. Implemented Interfaceの欄の Addボタンを押す
  3. リストビューからインターフェース型を選択する

これで Interfaceを実装できました。次は関数をoverrideします。
詳しい操作は以下のリンク先をご参照ください。

https://papersloth.hatenablog.com/entry/2018/09/19/195013#BlueprintImplementableEvent

overrideできました。これにて、C++から適切なタイミングで呼び出されるはずです。

4: BP Native UInterface

便宜上、C++で実装・使用してBPで実装・使用・overrideする全部入りUInterfaceのことをBP Native UInterfaceと呼称します。(いい名前が思いつかない)

全部入りです。ここまでくるとベースクラスでいいじゃん感がでてきます。とはいえ、ダイアモンド継承の危険を減らしたり、任意のUObjectinterfaceを実装できるという点では優れています。

BP Native UInterfaceのC++定義

C++で定義することで、C++でもBPでも 使用・overrideできるようにします。

サンプル事例としてC++/BP連携の塊であるインタラクション機能を挙げます。
典型例としてレベル上の宝箱を「開ける」というインタラクトを想定します。
他にもスイッチを「押す」、NPCに「話す」といった用途にも使えます。

#include "CoreMinimal.h"

UINTERFACE(BlueprintType, Blueprintable)
class UInteractable : public UInterface
{
    GENERATED_BODY()
};

/**
 * インタラクト用インターフェース
 * - CanInteract: インタラクト可否
 * - GetInteractionText: UI 表示テキスト
 * - OnInteract: 実際のインタラクト処理
 */
class IInteractable
{
    GENERATED_BODY()
public:
    /** インタラクト可否を返す */
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    bool CanInteract(AActor* Interactor) const;

    /** UI に出すテキスト */
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    FText GetInteractionText() const;

    /** 実際のインタラクト処理(開く、拾う、話しかけるなど) */
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    void OnInteract(AActor* Interactor);

    // UFUNCTIONに virtualは付けない
    // virtual bool CanInteract(AActor* Interactor) const;
    // virtual FText GetInteractionText() const;
    // virtual void OnInteract(AActor* Interactor);

    // デフォルト実装する場合は_Implementationを付ける
    // virtual bool CanInteract_Implementation(AActor* Interactor) const;
    // virtual FText GetInteractionText_Implementation() const;
    // virtual void OnInteract_Implementation(AActor* Interactor);
}

★ポイント:

  • UINTERFACE(BlueprintType, Blueprintable) なclass
  • UFUNCTION(BlueprintNativeEvent, BlueprintCallable) な関数

C++で使用/実装してBPでも使用/overrideするということから属性がたくさん付与されます。

BlueprintTypeにして変数として保持できるようにします。

Blueprintableにしてインターフェースを実装できるようにします。

BlueprintNativeEventはデフォルト実装を持たせつつもBPでも実装できるようにするために必要です。インタラクション処理はC++実装とBP実装の両方が必要になりがちなのでこれを採用します。

BlueprintCallableはBP側の任意のタイミングで関数を呼び出したいことがあるためつけておきます。

ややこしいですが、BlueprintNativeEventはvirtualを付けません。

BP Native UInterfaceのC++での使用

BlueprintImplementableEventと同様に、Execute_付のThunk関数を使用する必要があります。

使用例
void APlayerCharacter::TryInteract(AActor* Other)
{
    if(Other->Implements<UInteractable>())
    {
        if(IInteractable::Execute_CanInteract(Other, this))
        {
            // ここにInteractor側のインタラクト処理を実装
            Interactor_DoInteract();

            // Interacable側のインタラクトされた時の処理呼び出し
            IInteractable::Execute_OnInteract(Other, this);
        }
    }
}

BP Native UInterfaceのBPでの使用

普通の関数ノードつなぐだけ

BP Native UInterfaceのC++での保持

上記と同様

BP Native UInterfaceのBPでの保持

BP変数にもつだけ

BP Native UInterfaceのC++実装

C++での実装はデフォルト実装となります。自分でデフォルト実装を実装することが可能です。
実装するには_Implementation付きの同名関数を実装します。interface型にデフォルト実装するには、virtual修飾つきで実装するとよいでしょう。C++派生型でさらにoverrideできた方が都合がよいからです。

.h
class IInteractable
{
    ......

    // interfaceメソッドの定義
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    bool CanInteract(AActor* Interactor) const;

    // デフォルト実装の定義
    virtual bool CanInteract_Implementation(AActor* Interactor) const;
}
.cpp
bool IInteractable::CanInteract_Implementation(AActor* Interactor) const
{
    //ここにC++デフォルト実装を書く
    return IsValid(Interactor);
}

さて、次は具象型にC++実装してみましょう。宝箱を例として挙げます。全部実装するとわかりにくいので一部のみ実装して説明します。

TreasureBox.h

/**C++実装による宝箱アクターの基底クラス
 * BP側でいろんな宝箱を実装する際はこのクラスから派生すること*/
UCLASS()
class ATreasureBoxBase : public AActor, public IInteractable
{
    virtual bool CanInteract_Implementation(AActor* Interactor) const override;
}

/** 鍵付き宝箱アクターの基底クラス
 * BP側で鍵付き宝箱を実装する際はこのクラスから派生すること
 * デコレータパターン */
UCLASS()
class ALockedTreasureBox : public ATreasureBoxBase
{
    virtual bool CanInteract_Implementation(AActor* Interactor) const override;
};

TreasureBox.cpp
bool ATreasureBoxBase::CanInteract_Implementation(AActor* Interactor) const
{
    // デフォルト実装 (相手が有効である)
    if(!Super::CanInteract_Implementation(Interactor)){return false;}

    if(!IsValid(this)) { return false; } // 宝箱自身が有効である
    if(宝箱がすでに空いている) {return false; }

    // 事例1: 鍵なし宝箱
    // 相手がIInteractorならばOK!
    // ACharacterやBP_Playerが実装しているはず
    if(Interactor->Implements<UInteractor>())
    {
        return true;
    }

    return false;
}

bool ALockedTreasureBox::CanInteract_Implementation(AActor* Interactor) const
{
    if(!Super::CanInteract_Implementation(Interactor)){ return false; }

    // 事例2: 鍵付き宝箱
    // この宝箱の指定のカギを所有していたらOK!
    if(IInventoryOwner* Inventory = Cast<IInventoryOwner>(Interactor))
    {
        return Inventory->HasItem(TEXT("Item.TreasureBoxKey.00"));
    }
}

こんな感じです。
デフォルト実装では共通実装して、派生宝箱クラスではちゃんとロジックを実装しています。
いわゆるデコレーターパターンがささった事例です。宝箱の条件なんかは試行錯誤の余地がない決まり切った仕様でしょうからC++で書いても問題ないでしょう。BP側でノードで書くと結構ながーくなっちゃいますし。

BP Native UInterfaceのBP実装

上記のようにC++側で基盤実装されたうえで、さらにBP側でなんやかやoverride実装したいとします。

BP_TreasureBox_Animatable は豪華アニメ付き宝箱とします。この宝箱に対して CanInteractを実装してみましょう。登場演出アニメーション中は開けられないこととします。
宝箱ごときのアニメ状態は超単純であり、いちいちC++でもつとだるいのでBP変数で持つことにましょう。

他にも開封アニメやSEを再生したいため、OnInteractも実装するのが良さげです。

overrideのやり方はBP Implementable UInterfaceと同じです。実際の実装は主旨に外れるので割愛します。本事例を通して、C++で実装しつつBPでも実装したいという具体的な例が分かったかと思います。

詳解

ようやく本題です。

仮想デストラクタがなくても大丈夫

仮想関数を持ちしclass/structは仮想デストラクタが必須です。
しかしながら、本稿のサンプルコードでは一切仮想デストラクタを記述していません。

大丈夫なんでしょうか?大丈夫です。UClass/UInterfaceにおいては 仮想デストラクタは記述不要です。 GENERATED_BODY()によってuser-providedな仮想デストラクタがちゃんと自動実装されるからです。別に自前実装しても問題ありません。自前実装したときは自動実装されません。しっかりしてますね。

user-provideduser-declaredの違いで困ることはないと思いますが、いちおう言及しておきます。

https://qiita.com/yumetodo/items/424cc4d15de4edad436a

BlueprintImplementableEvent 詳解

Thunk関数が分からなすぎるので生成されるコードを覗いてみましょう。

再掲
#include "CoreMinimal.h"

UINTERFACE(BlueprintType, Blueprintable)
class UDamageReaction : public UInterface
{
    GENERATED_BODY()
};

class IDamageReaction
{
    GENERATED_BODY()
public:

    /** BPで実装する関数 */
    UFUNCTION(BlueprintImplementableEvent)
    void PlayReaction(FContext& Context);
};

UHTにより自動生成されたコードがGENERATED_BODY部分に差し込まれた結果概ね以下のコードとなります。 主旨に不要な部分は省略しています。

generated.h
class IDamageReaction
{
protected:
    // 自動生成された仮想デストラクタ
    virtual ~IDamageReaction() {}
public:
    // ブループリントVMへ呼び出す Thunk関数 
    static void Execute_PlayReaction(UObject* O, FContext& Context);

public:

    /** BPで実装する関数 */
    UFUNCTION(BlueprintImplementableEvent)
    void PlayReaction(FContext& Context);
};

なるほど。ちゃんと仮想デストラクタが生成されており、継承しても安全そうです。次はgen.cpp側です。

gen.cpp
// デフォルト実装を使うとcheckで止まるようになっている
void IDamageReaction::PlayReaction(FContext& Context)
{
    check(0 && "Do not directly call Event functions in Interfaces. Call Execute_PlayReaction instead.");
}

// Thunk関数の実装
// BP側に実装があればそれを呼び出し、なければ呼び出さない
void IDamageReaction::Execute_PlayReaction(UObject* O, FContext& Context)
{
    check(O != NULL);
    check(O->GetClass()->ImplementsInterface(UDamageReaction::StaticClass()));
    UFunction* const Func = O->FindFunction(TEXT("PlayReaction"));
    if (Func)
    {
        DamageReaction_Parms Parms{.Context=Context}; // 引数を包むためのラッパ構造体
        O->ProcessEvent(Func, &Parms); //☆ ここで BP実装を実行
        Context = Params.Context; //BPで書き換えられた情報を書き戻す
    }
    else
    {
        // BP実装がなければ何もしない
        // C++側では実装をもたないやつだから
    }
}

謎であったExecute_PlayReaction関数が生成されているのが確認できます。
Blueprint上で実装された関数を実行するためのラッパー関数が生成されています。FindFunctionでBP上で実装されているはずの関数を関数名から探し出して、あれば実行しています。

UFUNCTION(BlueprintImplementableEvent)はどうしてvirtual修飾しないのか、どうしてExecute_付きの関数を呼び出さなくてはならないのかが判明しました。

BlueprintNativeEvent 詳解

同様に BlueprintNativeEventも見ていきましょう。
説明用ミニマム版。

最小API
UINTERFACE(BlueprintType, Blueprintable)
class UInteractable : public UInterface
{
    GENERATED_BODY()
};

class IInteractable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    bool CanInteract(AActor* Interactor) const;
};

UHTにより自動生成されたコードがGENERATED_BDOY部分に差し込まれた結果概ね以下のコードとなります。 主旨に不要な部分は省略しています。

generated.h
class IInteractable
{
public:
    // デフォルト実装が自動生成される
    virtual bool CanInteract_Implementation(AActor* Interactor) const { return false; }; 
protected:
    // 仮想デストラクタが自動生成される
    virtual ~IInteractable() {}

public:
    // Thunk関数が自動生成される
    static bool Execute_CanInteract(const UObject* O, AActor* Interactor); 
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interactable")
    bool CanInteract(AActor* Interactor) const;
};

Thunk関数はこんな感じです。

gen.cpp
bool IInteractable::Execute_CanInteract(const UObject* O, AActor* Interactor)
{
    check(O != NULL);
    check(O->GetClass()->ImplementsInterface(UInteractable::StaticClass()));
    
    // BP実装があればそれを優先的に呼び出す
    UFunction* const Func = O->FindFunction(TEXT("CanInteract"));
    if (Func)
    {
        Interactable_Parms Parms{.Interactor=Interactor};
        const_cast<UObject*>(O)->ProcessEvent(Func, &Parms);
    }
    // BP実装がなければC++側のnative実装を呼び出す
    // virtual関数なのでC++側でoverrideされていればそれが呼び出される
    else if (auto I = (const IInteractable*)(O->GetNativeInterfaceAddress(UInteractable::StaticClass())))
    {
        Parms.ReturnValue = I->CanInteract_Implementation(Interactor);
    }
    return Parms.ReturnValue;
}

BlueprintNativeEventの場合はThunk関数内で、BP実装がなければC++実装をコールするようになっています。CanInteract_Implementationvirtual関数なのでALockedTreasureBoxの場合でもちゃんとALockedTreasureBox::CanInteract_Implementationが呼ばれます。

BlueprintNativeEventBlueprintImplementableEventの違いはC++実装へフォールバックするかしないかであることがわかりました。ドキュメントの通りでした。

Interface Message vs Call Function

BlueprintCallableなくせにBlueprintから全然TScriptInterface経由でcallできなかったので、カッとなって調査しました。結論から言えば、Interface Message なる存在を知らなかったが故の勘違いでした。

まずは画像をご覧ください。

前提となるコードはこちらです。

再掲

class IBuffStatusProvider
{
    UFUNCTION(BlueprintCallable)
    virtual bool IsPoison() const = 0;
}

class IBuffStatusProvider2{
public:
    UFUNCTION()
    virtual bool IsPoison2() const = 0;
};

UCLASS(Blueprintable)
class AStatusActor : public AActor,
    public IBuffStatusProvider,
    public IBuffStatusProvider2
{
	GENERATED_BODY()
public:
	UFUNCTION(BlueprintCallable)
	virtual bool IsPoison() const override { return bPoison; } 

	UFUNCTION(BlueprintCallable)
	virtual bool IsPoison2() const override { return bPoison; }
};

selfは AStatusActorです。
画像を見ると同じ関数なのにTargetが異なるやつがありますね。一体これはなんでしょうか?

  • 上段ノードは Class > IBuffStatusProvider > IsPoison(Message)
  • 中段ノードは Class > IBuffStatusProvider > IsPoison
  • 下段ノードは Call Function > IsPosion

どれを使えばいいんでしょうか?何が異なるのでしょうか?調べました。

Call Function

Call Function とはTarget型TUFUNCTIONを呼び出すBPノードで、その実体はUK2Node_CallFunctionノードです。BPコンパイル時に ノードが指すUFucntionへの関数呼び出しが内部的に登録されます。いわゆるメンバー関数呼び出しであり、最終的にT::Funcが呼ばれます。

よって中段と下段の違いはUFunctionに何が入っているかの違いです。調べたところ、

  • 中段ノード: IBuffStatusProvider::IsPoisonUFunction
  • 下段ノード: AStatusActor::IsPoisonUFunction

が格納されていました。なるほど、つまりoverrideしたメンバー関数を普通に呼び出すのかSuper型へキャストして親の関数を直接呼び出しちゃうのか、という違いですね。
この場合AStatusActorIsPoisonoverrideしているため、下段のAStatusActor::IsPoisonを呼び出すのが正解っぽそうです。

よって、原則として自身が具象型を知っているなら具象型の関数を呼んだらええ、ということがわかりました。

  • BlueprintCallable な具象型のAPIは BPからCall Function(関数呼び出し)で呼び出す
  • BlueprintCallableinterface API はBPから Interface Message経由で呼び出す

しかし待ってください、IBuffStatusProvider::IsPoison()=0は純粋関数です。少し怪しいですね、どこか勘違いしているようです。

UFUNCTIONのおさらい

ここでまず UFunctionとは何なのかをさらーと触れておきます。
UFunctionは 関数ポインタを持つclassです。他にもいっぱい機能がありますがようは関数オブジェクト、ファンクタみたいなやつとでも理解しておきます。

疑似コード
using FNativeFuncPtr = void(*)(UObject* Context, FFrame& TheStack, void* const RESULT_PARAM);

class UFunction : UStruct
{
    // C++実装されたUFUNCTION()への関数ポインタ
    FNativeFuncPtr Func;
}

UFUNCTION(BlueprintaCallable)で修飾されたメンバ関数に対してはUHTにより以下のグルーコード(Thunk関数?)が生成されます。

疑似コード
void IBuffStatusProvider::execIsPoison(UObject* Context, FFrame& TheStack, void* const RESULT_PARAM)
{
    // thisポインタ経由でoverrideされた仮想関数を呼び出すイメージ
    RESULT_PARAM.Result = this->IsPosion();
}

そして、この execIsPoison関数アドレスが名前とともに静的テーブルに保持されますので、UClassへ登録します。

疑似コード
static const TTuple<FName, FNativeFuncPtr> Funcs[]
{
    { "IsPoison", &IBuffStatusProvider::execIsPoison},
};

// ここでU付きのクラスが登場したぞ!
FNativeFunctionRegistrar::RegisterFunction(UBuffStateProvider::StaticClass(), Funcs, 1);

関数テーブルを保持するUClassを作るためにU付のクラスを実装してたんですねー。

ここまで来たらあとは UFunction::Funcに関数アドレスをセットするだけですね。

疑似コード
UFunction* Func = NewObject<UFunction>(...);
Func->Func = Funcs[0].Pointer; //&IBuffStatusProvider::execIsPoison

IBuffStatusProvider::execIsPoison()を指すUFunctionがどこかに保持されました。
同様に、AStatusActor::execIsPoison()を指すUFunctionがどこかに保持されました。

UFunctionとは様々なシグネチャの関数をグルーコードでラップすることにより統一的な関数ポインタにまとめて保持する関数オブジェクト、と言えることが分かりました。

Call Function は Call UFunction

CallFunctionノードとは 関数名からUFunctionを探しそれを実行するノードでした。

よって中段と下段の違いはUFunctionに何が入っているかの違いです。調べたところ、

  • 中段ノード: IBuffStatusProvider::IsPoisonUFunction
  • 下段ノード: AStatusActor::IsPoisonUFunction

先ほどの説明をより正確に書き下しましょう。

  • 中段ノード: IBuffStatusProvider::execIsPoisonを指すUFunction
  • 下段ノード: AStatusActor::execIsPosonを指すUFunction

実際にbreakポイントを張ってUFunction::Funcのアドレスを見ると, Hogehoge.dll!IBuffStatusProvider::execIsPoison(UObject..略..)という関数アドレスが格納されていました。

thisポインタ経由で IsPoison()が呼び出されるならば vtable上の正しい実装へのアドレスが入っているはずなので純粋関数でも大丈夫そうです。納得。

Interface Message

先述の通り、Interface Message とはBPノードの右上にお手紙マークがついているノードです。(お手紙マークはmessageという意味だったのか......)
このノードはCall Functionとほぼ同じですが、インターフェースを実装しているかチェックします。

interface Message の直接の実体は UK2Node_Messageです。BPコンパイル時に内部的にCastToInterfaceノードおよびFunctionCallノードに変換されます。ノードのTargetがinterfaceを実装していなければデフォルト値を返します。
Thunk関数と似たような処理をBlueprintVM上で行っているという訳です。

ということはわざわざ自前でDoes Object Implement Interfaceノードを使わずとも、呼び出すだけなら Messageノード使っても良さそうですね。

まとめ

  • C++のみ → 純粋仮想関数でOK. CannotImplementInterfaceInBlueprint
  • BPからメッセージ呼び出し -> BlueprintCallable, BlueprintType, CannotImplementInterfaceInBlueprint
  • BPから関数を呼び出す -> BlueprintCallable, BlueprintType, Blueprintable
  • BPでoverride -> BlueprintImplementableEventExecute_呼び出し
  • BPで実装 -> BlueprintNativeEventExecute_呼び出し
  • 全部入り -> BlueprintTypeBlueprintable, BlueprintCallableBlueprintNativeEventExecute_呼び出し

共通ルール

  • BlueprintImplemetableEventvirtualつけない
  • BlueprintNativeEvent_Implementationvirtualで実装する
  • BlueprintImplemetableEventBlueprintNativeEventExecute_を使って呼びだす

見えないThunk関数に気を付ける。間違った使い方をするとcheckで止まる。DO_CHECKマクロを外していると止まらず気が付けない。

UInterface とはinterfaceのように振る舞う型でありinterfaceそのものではない。C++の仮想関数を適切に呼び出せるように拡張しつつ、UHTUObjectの仕組みを利用してリフレクション・GC・BP連携などをサポートしたインターフェース機構である。

付録: UInterface 設計方針

個人的にinterfaceはデフォルト実装やデータを持つべきではないと考えております。データを持つならBaseクラスでいいじゃん、と思っているからです。ただし、そうせざるを得ないときもあります。interfaceとはこうあるべき論は本稿の主旨ではないので言及しません。

ぽえむ

本稿を読んだことでUInterfaceを使いたくなるかもしれませんが、なんでもかんでもUInterfaceにすればいいというものではありません。細やかにコンポーネント指向になっているならばinterfaceを使わずともいいケースもあるはずです。

interfaceがクラス間を疎結合にしているというのはある種正しいのですが、より正確な実態はinterfaceに依存を集約しているだけです。interfaceによる「依存性の逆転」ではなくinterfaceへの「依存性の抽出」という表現がより正確なところではないでしょうか?
広く依存されるinterfaceは依存性が抽出されまくった濃厚でドッピオなコーヒーのようなものなので、ひとたび名前やAPIを変更しようとすると大変な苦労に見舞われます。事実上不可能なためIInteractable2IMyObjectExみたいな次期interfaceが生えたりします。

interfaceに高度に抽象化されているが故に、この文脈でこのポインタにはどの具象クラスが入っているからわからん、デバッグが大変だーということもあります。コードを読む側も実装がどこにあるか探すのが大変なことがあります。

シグネチャを統一したいだけならばconcepttemplate関数を使えば代替できたりします。interfaceはツール群の一つとして捉えてください。UInterfaceを使うときは具体的に解決したい課題を見据えて導入するのが良さそうだと思いました。

参考資料

GitHubで編集を提案

Discussion