ぼっちプログラマのメモ

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

【UE5】Instanced Object におけるプロパティ表示を少し強引にフィルタリングしてみる

はじめに

Instanced Object という機能を使うことで実装を分割・併用することができ、柔軟かつ変更に強い設計を構築することができます。

UE5/UE4 C++で変数にクラス(Class)のインスタンス(Instance)をプロパティ指定子(Property Specifiers)を使って指定する(UPROPERTY(Instanced)、UCLASS(EditInlineNew)) 凛(kagring)のUE5/UE4とゲーム制作と雑記ブログ
エディタのUXを向上させるUnreal C++との付き合い方 | ドクセル

特に個人的に好きなのは、C++で基盤となる仕組みを作っておけば、BPでも拡張・量産できることです。下記のように書いておけば、

UCLASS(Blueprintable, EditInlineNew, CollapseCategories)
class MYPROJECT_API UMyCommand : public UObject
{
    GENERATED_BODY()

    UMyCommand();

public:
    UFUNCTION(BlueprintNativeEvent)
    void Run();
    virtual void Run_Implementation() PURE_VIRTUAL();

};

BP側で派生クラスを作成することで、

専用のActorクラス や DataAsset などで Instanced Object による処理のつけ外しを行うことができます。便利!

ただ少し困ったことに、BP側で追加したプロパティが全て Instanced Object の設定部分で表示されてしまいます。プロパティのInstance Editable設定は残念ながら無視されます…

使用するプロパティを全てC++側で実装したり、C++側の CollapseCategories を削除 & BP側で Hide Categories を設定すれば解決するのですが…色々面倒でInstanced Object の良さが減ってしまいます。

なにか良い方法がないか調べていたのですが見つからず…最終的にはInstanced Objectの表示カスタマイズにより強引に対応してみた、というのがこの記事の内容です。C++多めです。

Instanced Objectの表示カスタマイズ

IPropertyTypeCustomizationというUE4からある機能を使うと、特定のプロパティに対して詳細パネルにおける表示内容をカスタマイズすることができます。

Customizing Details & Property Type panel - tutorial | Unreal Engine Community Wiki

ゲーム開発を助けるエディタ拡張とプラグイン化について | ドクセル
[UE4 エディタ拡張] 詳細パネルで UStruct のプロパティカスタマイズ #C++ - Qiita
【UE4】独自アセット実装マニュアル(後編) #初心者 - Qiita
[UE5] 構造体のヘッダーにカスタム文字列を表示する #C++ - Qiita

様々な記事にて解説されているので詳細は割愛するとして、ひとまずこの機能を使って「特定の条件を満たすプロパティは詳細パネルで表示しない」とカスタマイズすれば何とかなりそうです。

では、まずはIPropertyTypeCustomizationを使いつつ、Instanced Objectをそのまま出す所までやってみます。

Instanced Object用PropertyTypeCustomizationを導入

  • Instanced Objectの表示部分の実装
class FInstancedObjectCustomization : public IPropertyTypeCustomization
{
public:
    static TSharedRef<IPropertyTypeCustomization> MakeInstance()
    {
        return MakeShareable(new FInstancedObjectCustomization);
    }

    virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;
    virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;
};
void FInstancedObjectCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle,
    FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    // clang-format off
    HeaderRow
        // 
        .NameContent()
        [
            PropertyHandle->CreatePropertyNameWidget()
        ]
        .ValueContent()
        .MinDesiredWidth( 250.f )
        [
            PropertyHandle->CreatePropertyValueWidget( false )
        ];
    // clang-format on
}

void FInstancedObjectCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle,
    IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    // Instanced Object自身のPropertyHandleを取得
    if(TSharedPtr<IPropertyHandle> InstancedObjectHandle = PropertyHandle->GetChildHandle(0))
    {
        // Instanced Objectが持つPropertyHandleを取得 -> 表示
        uint32 NumChildren = 0;
        InstancedObjectHandle->GetNumChildren( NumChildren );
        for ( uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex )
        {
            const TSharedRef<IPropertyHandle> ChildHandle = InstancedObjectHandle->GetChildHandle( ChildIndex ).ToSharedRef();
            ChildBuilder.AddProperty(ChildHandle);
        }
    }
}
  • Editorモジュールで↑の処理を登録
class FMyEditorModule : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
void FMyEditorModule::StartupModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    PropertyModule.RegisterCustomPropertyTypeLayout(
        UMyCommand::StaticClass()->GetFName(),
        FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FInstancedObjectCustomization::MakeInstance));
    
    PropertyModule.NotifyCustomizationModuleChanged();
}

void FMyEditorModule::ShutdownModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    PropertyModule.UnregisterCustomPropertyTypeLayout(
        UMyCommand::StaticClass()->GetFName());
}
  • InstancedObjectを扱うDataAsset
UCLASS(Blueprintable)
class MYPROJECT_API UMyCommandDataAsset : public UDataAsset
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Instanced)
    UMyCommand* Command;
};

これで見た目では全くわかりませんが、カスタム前の状況を再現することができました(たぶん)。


あとはフィルタリング処理をFInstancedObjectCustomization::CustomizeChildrenに入れて、外部から設定してほしいプロパティのみを表示するようにします。

Instanced Objectの詳細パネル表示をフィルタリングしてみる

用途などによってフィルタリングの基準は変わってくると思いますが、今回は2パターン実装してみました

Instance Editableフラグが無効の場合は表示しない

BPをメインで触っている人にも分かりやすいのがこれかなぁと思います。「公開したいプロパティは目玉を開ける!」、シンプル is ベスト!

コードも比較的シンプルで、CPF_DisableEditOnInstance タグを持っているか否かを判定すればOKです。

void FInstancedObjectCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle,
    IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
...
            const TSharedRef<IPropertyHandle> ChildHandle = InstancedObjectHandle->GetChildHandle( ChildIndex ).ToSharedRef();
            FProperty* ChildProperty = ChildHandle->GetProperty();

            // Instance Editableフラグが無効の場合は表示しない
            if ( ChildProperty->HasAnyPropertyFlags( CPF_DisableEditOnInstance ) )
            {
                continue;
            }
            
            ChildBuilder.AddProperty(ChildHandle);
        }
    }
}

なお、プロパティの各公開設定ごとの結果は以下の通り。C++側は DefaulyOnly だと InstanceEditableフラグが無効状態と覚えておけば良さそうです!

  • EditAnywhere:表示
  • EditInstanceOnly:表示
  • EditDefaultOnly:非表示
  • VisibleInstanceOnly:表示
  • VisibleDefaultOnly:非表示
  • InstanceEditableフラグ 有効(BP変数):表示
  • InstanceEditableフラグ 無効(BP変数):非表示

HideCategoriesに設定したカテゴリーの場合は表示しない

Instance Editableと比べると少し奥まった所にある設定ですが、ワークフローに組み込みやすい条件かなと思います。

UCLASS(Blueprintable, EditInlineNew, CollapseCategories, **HideCategories=(Hide)**)
class BUILD0504_API UMyCommand : public UObject

実装は FEditorCategoryUtils::GetClassHideCategories を使えば楽ちんです(僕はこの関数に気づくまでは頭を抱えてました)

void FInstancedObjectCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle,
    IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    // InstancedObject 
    if(TSharedPtr<IPropertyHandle> InstancedObjectHandle = PropertyHandle->GetChildHandle(0))
    {
      // HideCategoriesを取得
        TArray<FString> HideCategories;
        FEditorCategoryUtils::GetClassHideCategories(InstancedObjectHandle->GetOuterBaseClass(), HideCategories);
        
        // Instanced Object が持つPropertyを全て表示
        uint32 NumChildren = 0;
        InstancedObjectHandle->GetNumChildren( NumChildren );
        for ( uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex )
        {
            const TSharedRef<IPropertyHandle> ChildHandle = InstancedObjectHandle->GetChildHandle( ChildIndex ).ToSharedRef();
            FProperty* ChildProperty = ChildHandle->GetProperty();

            // プロパティのカテゴリがHideCategoriesに含まれていなければ表示
            bool bFound = false;
            for (auto& HideCategory : HideCategories)
            {
                if( ChildHandle.Get().GetDefaultCategoryName().ToString().Contains(HideCategory) )
                {
                    bFound = true;
                    break;
                }
            }
            if(!bFound)
            {
                ChildBuilder.AddProperty(ChildHandle);
            }
        }
    }
}

この2つで何とかならない場合は、プロパティに独自にmeta情報を追加し、その有無で判定していくのがいいかと思います。

さいごに

当初は詳細パネルのカスタマイズとなると少し身構えましたが、 プロパティの表示のONOFF程度でしたらSlateに触れることなくサクッと対応できて一安心でした!

ちょっとしたことですが、編集が楽になったり、ヒューマンエラーを回避したりと良いことづくめなので、少しずつ詰め重ねていきたいですね!

おしまい