ぼっちプログラマのメモ

Unreal Engineについて書いたりしてます

【UE5】UPROPERTYのInterfaceフィルタリング指定子まとめ(MustImplement / ObjectMustImplement / AllowedClasses)

はじめに

UE5.8 の Mover プラグインのソースを眺めていたら、見慣れないメタ指定子に遭遇しました。

// Mover/Source/Mover/Public/MoverComponent.h:923
UPROPERTY(EditDefaultsOnly, EditFixedSize, Instanced, Category = Mover, 
meta = (NoResetToDefault, ObjectMustImplement = "/Script/Mover.MovementSettingsInterface"))
TArray<TObjectPtr<UObject>> SharedSettings;

ObjectMustImplement...?MustImplement ならよく見るんですが、ObjectMustImplement は見慣れないですしググっても出てこない… なにやら便利そうなので、Object/Classプロパティのフィルタリングで使う他のmeta指定子と合わせて調査・検証結果をまとめてみました。 (GetAllowedClassesもググっても情報出てこないですし)

検証環境:UE5.8 Preview

本記事の結論

少し長くなるので結論早見表・結論をぺたり

メタ 非Instanced (Asset picker) Instanced (EditInline) Class picker (TSubclassOf)
MustImplement
ObjectMustImplement
AllowedClasses
GetAllowedClasses

状況に応じてフィルタリング処理を変えたい場合はGetAllowedClasses を利用(後述の仕様に注意)。  

固定で良い場合は…

  •  非Instanced な Object: AllowedClasses
  •  Instanced Object:ObjectMustImplement
  •  Class系プロパティ:MustImplement

( AllowedClasses でも実装可能だが、速度面・安全面から MustImplement /ObjectMustImplementを利用した方が無難 )

以降は詳細や調査内容・結果について

MustImplement と ObjectMustImplement の違い

Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h の定義を見ると

/// Used for Subclass and SoftClass properties. 
/// Indicates the selected class must implement a specific interface
MustImplement,

/// Used for object properties.
/// Indicates the selected object must be of a class which implements a specific interface
ObjectMustImplement,

つまり用途自体は以下のように分かれています。

  • MustImplementTSubclassOf<> / TSoftClassPtr<> / FSoftClassPath 等の クラス参照プロパティ に書く
  • ObjectMustImplementTObjectPtr<> / UObject* 等の オブジェクト参照プロパティ に書く

ただ注意点として、Instancedの有無によって挙動が変わるという落とし穴があります。

例えば以下のようにプロパティを用意し、

// MustImplement on Object property
UPROPERTY(EditAnywhere,
          meta = (MustImplement = "/Script/MyModule.Foo"))
TObjectPtr<UObject> MustImplement;

// ObjectMustImplement on Object property
UPROPERTY(EditAnywhere,
          meta = (ObjectMustImplement = "/Script/MyModule.Foo"))
TObjectPtr<UObject> ObjectMustImplement;

// ObjectMustImplement on Instanced property
UPROPERTY(EditAnywhere, Instanced,
          meta = (ObjectMustImplement = "/Script/MyModule.Foo"))
TObjectPtr<UObject> InstancedObjectMustImplement;

インターフェース IFoo を実装した アセット A, 実装していないアセット Bを準備した場合、

候補表示数の結果は以下のようになります

  • MustImplement= A・B両方 ← UIフィルタ無効
  • ObjectMustImplement on Object = A・B両方 ← UIフィルタ無効
  • ObjectMustImplement on Instanced = Aのみ ← フィルタが効く!

そして、MustImplement版 や ObjectMustImplement on Object版に IFoo を持たない アセット Bを設定しようとするとWarningが出て失敗します。つまり、UI上でのフィルタリングは機能していませんが、アセット選択後の内部処理では弾く処理が動作します。

うーん、ややこしい

AllowedClasses と GetAllowedClasses の違い

非Instancedなプロパティにて MustImplementObjectMustImplement は使えませんが、AllowedClassesGetAllowedClasses にインターフェースのクラスを指定することでUI上でのフィルタリングが動作します

// AllowedClassesの場合
UPROPERTY(EditAnywhere, meta = (AllowedClasses = "/Script/MyModule.Foo"))
TObjectPtr<UMyAsset> AllowedClasses;

// GetAllowedClassesの場合
UPROPERTY(EditAnywhere, meta = (GetAllowedClasses = "GetReturnInterfaceUClass"))
TObjectPtr<UMyAsset> GetAllowedClasses;

UFUNCTION()
TArray<UClass*> GetReturnInterfaceUClass()
{
    return { UFoo::StaticClass() }; 
}

設定がシンプルなAllowedClassesと比べるとGetAllowedClassesは少し準備が大変ですが、動的にフィルタリング条件を変えることができるので色々と便利です。 (今回はInterfaceのクラスを渡していますが、通常のクラスでもフィルタリング可能です)

なお、TSubclassOf や Instanced版でもAllowedClassesGetAllowedClassesは動作しますが、GetAllowedClassesで指定する関数の実装に注意する必要があります。具体的には上述の実装では機能しないため、下記のように実装する必要があります。

TArray<UClass*> UMPV_PickerHostDA::GetExpandedFooImplementers()
{
    // 全クラスに対してUFooインターフェースを持つか否かをチェック
    TArray<UClass*> Result;
    for (TObjectIterator<UClass> It; It; ++It)
    {
        if (It->ImplementsInterface(UFoo::StaticClass()))
        {
            Result.Add(*It);
        }
    }
    return Result;
}

どうやら非Instanced版やAllowedClassesではこの処理が自動的に行われるのですが、それ以外の場合は自前で行う必要があるようです。 (PropertyCustomizationHelpers::GetClassesFromMetadataString参照)

うーん、ややこしい

結局何を使えばいいのさ?

ここで改めて対応表を見てみます。

メタ 非Instanced (Asset picker) Instanced (EditInline) Class picker (TSubclassOf)
MustImplement
ObjectMustImplement
AllowedClasses
GetAllowedClasses

こう並べると全パターンに対応している AllowedClasses でいいのでは?となりますが…AllowedClassesを使う場合、上述の通り 全Classに対してインターフェースを持つか否かのチェックが走ります。小規模なプロジェクトなら問題にならないかもですが、一定以上の規模になってくるとピッカーUIを開くたびに少し待たされる・引っ掛かりを感じるなどの問題が起きる…かもしれません。また、ObjectMustImplementに関してはUE5で追加された指定子ということもあり、シリアライズ時のチェックに使われたりなどデータの整合性を考慮した作りになっています。

動的にフィルタリング条件を変えたい場合や、複数のインターフェースを対象としたい場合は (Get)AllowedClassesを使わざるをえないのですが、それ以外だとMustImplementObjectMustImplement を使った方が安牌かなぁ…と思ってます。

おまけ:DisallowedClasses、GetDisallowedClasses

AllowedClasses , GetAllowedClasses と対となるDisallowedClasses ,GetDisallowedClasses というメタ指定子もあります。どちらも選択対象外とするクラスを指定するためのもので、例えばFAnimNode_SequencePlayerでは以下のように設定することで AnimMontage は選択対象外にしたりしてます。

UPROPERTY(EditAnywhere, Category = Settings, 
meta = (PinHiddenByDefault, DisallowedClasses="/Script/Engine.AnimMontage"))
TObjectPtr<UAnimSequenceBase> Sequence = nullptr;

おまけ:TScriptInterface

ここまで色々メタを書いてきましたが、 TScriptInterface を使えばメタ無しでほぼ同じことができます。

UPROPERTY(EditAnywhere)
TScriptInterface<IFoo> ScriptInterface;

ただし TScriptInterface は Instancedに対応してなかったり、UObjectとIInterfaceをペアで持つ関係で取り扱いが少し特殊だったりするので注意が必要です。 zenn.dev

さいごに

開発が進むにつれてクラス・アセットの数はドンドン増えていくので、こうしたフィルタリング処理はフローやルールを整えておくのがオススメです!設定する側も迷わなくていいですし、不正なデータが設定されることを防げるのはとてもいいことです!ガンガンやっていきましょう! (便利なメタ指定子がいつの間にか増えてたりするので、それらが纏まってる ObjectMacros.h を定期的に見ないとですね… )

おしまい