PavilionDV7の雑多なやつ

Qiitaから移行しました。UE4に関する記事から興味のあることまで色々書きます。

【UE4】超単純な自作アセットエディタを作った

  • はじめに

    今年に入ってから自作アセットエディタに興味を持ち、色々試行錯誤してきました。そんな中、1つの成果物としてGitHubにて、自作のアセットエディタが出来るまでを段階ごとにまとめたプロジェクトを公開しました。各段階でプラグイン化されており、C++プロジェクトを用意していただければ、Pluginフォルダをプロジェクトフォルダに放り込んで有効化するだけで動作確認できます。

    github.com

    動いている様子はTwitterに載せています。

    特徴的な機能は一切無く、エディタでロジックを構築し他のオブジェクトから呼び出すまでの一連の流れが確認できるだけです。

    この記事では公開したプロジェクトについて、「すこし説明不足だったかもしれない」と感じた部分について書いていきます。 ソースコード全体を載せるということは無いので、コードを読みたい方は是非ダウンロードして読んでいただければと思います。

    モジュール分割

    モジュールについては株式会社ヒストリア さんのブログドキュメントを参照してください。(ここを参照していること前提で以降の内容は書かれています)

    公開したプラグインにはランタイム用モジュール「Tes」とエディタ用モジュール「TesEd」の2つのモジュールが実装されています。ランタイム用モジュールは文字通り「実行時に動作する機能」が実装されており、パッケージングに含まれます。エディタ用モジュールは「エディタ上でのみ動作する機能」が実装され、パッケージングからは除外されます。

    どちらのモジュールに含まれるか?

    大雑把に

    • CoreUObject、Engine、Slate、SlateCoreに属するクラスを継承する場合はランタイムモジュール

    • それ以外のクラス(AssetTools、UnrealEd等)に属するクラスを継承する場合はエディタモジュール

    という指針で振り分けています。

    もし何も継承しないクラスを作成する場合は次のような指針で振り分けます。

    • エディタモジュール内のクラスでのみ参照される場合はエディタモジュール

    • エディタモジュールとランタイムモジュール両方、またはランタイムモジュールのみの場合はランタイムモジュール

    エディタモジュールのクラスをランタイムモジュールのクラスから呼び出した場合

    エディタで動作確認する分には、エディタモジュールのクラスをランタイムモジュール側で呼び出してもエラーは発生しません。ビルドは問題なく通りますし、正しく機能します。問題が発生するのは「パッケージング実行時」です。

    エディタモジュールのクラスを利用するために、ランタイムモジュール側にエディタモジュールに属するモジュール名を記述すると、次のようなエラー文がパッケージング時に表示されます。

  UATHelper: Packaging (Windows (64-bit)):ERROR: Unable to instantiate module 'UnrealEd': Unable to instantiate UnrealEd module for non-editor targets.
  
  UATHelper: Packaging (Windows (64-bit)):(referenced via Target -> Tes.Build.cs -> "エディタモジュールに書くべきモジュール名".Build.cs)

ランタイムモジュール側に「AssetTools」を記述してしまった場合、エラー文は次のようになります。

  UATHelper: Packaging (Windows (64-bit)):ERROR: Unable to instantiate module 'UnrealEd': Unable to instantiate UnrealEd module for non-editor targets.
  
  UATHelper: Packaging (Windows (64-bit)):(referenced via Target -> Tes.Build.cs -> AssetTools.Build.cs)

このようなエラーが表示された場合、「対象となるモジュール名を消す」ことで解決できます。その修正によりランタイムモジュール側で呼び出すことが出来なくなりますが、どうにかして回避する必要があります。

公開したプロジェクトのPart6では、エディタモジュール側に定義した自作ノードに繋げたロジックをランタイムモジュールから呼び出しますが、当然ランタイムモジュールからエディタモジュールのクラスは呼び出せないため、「UK2Node_CustomEventクラスを使ってリフレクションクラスに関数オブジェクトを生成し、ランタイムモジュール側からは関数オブジェクトを通じてロジックを実行させる」という方法を採りました。

エディタモジュールからランタイムモジュールへのアクセスは可能ですから、ロジック実行に必要なデータをランタイムモジュール側のクラスに保存し実行することで、ランタイムモジュール側からは一切エディタモジュール側のクラスを呼ぶこと無くロジックを実行することが出来ます。

空っぽのUTesBlueprint

UTesBlueprintクラスは空っぽですが「アセット生成時のメニューでデフォルトのUBlueprintと重複しないようにするため」にそのままにしています。

UTesBlueprintを使用している箇所を全て「UBlueprint」に置き換えた時、アセット生成時メニューは次のように変化します。

f:id:PavilionDV7:20210504141036p:plain

同じ「TesBlueprint」が並んでいますが、実際は上がプラグインで定義した「UTesInstance」を親クラスとするブループリントが表示されます。それに対し下は、通常のブループリントを生成するように親クラスを選択するダイアログが表示されます。

このような動作をする原因は次の2つの関数を見ることで何となく理解いただけると思います。

  /* Factory.cpp */
  uint32 UFactory::GetMenuCategories() const
  {
    FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
      // SupportedClassを取得.
    UClass* LocalSupportedClass = GetSupportedClass();
  
    if (LocalSupportedClass)
    {
          // SupportedClassが持つアセットカテゴリ番号を取得.
        TWeakPtr<IAssetTypeActions> AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(LocalSupportedClass);
        if (AssetTypeActions.IsValid())
        {
            return AssetTypeActions.Pin()->GetCategories();
        }
    }
  
    // Factories whose classes do not have asset type actions fall in the misc category
    return EAssetTypeCategories::Misc;
  }
  
  /* NewAssetContextMenu.cpp; */
  
  // 引数にはTesEdModule.cppで登録した自作アセットカテゴリの番号が渡される.
  // AssetTools.RegisterAdvancedAssetCategory で検索するべし.
  TArray<FFactoryItem> FindFactoriesInCategory(EAssetTypeCategories::Type AssetTypeCategory)
  {
    TArray<FFactoryItem> FactoriesInThisCategory;
  
    static const FName NAME_AssetTools = "AssetTools";
    const IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>(NAME_AssetTools).Get();
    TArray<UFactory*> Factories = AssetTools.GetNewAssetFactories();
    for (UFactory* Factory : Factories)
    {
          // FactoryクラスのSupportedClassが持つアセットカテゴリ番号を取得.
        uint32 FactoryCategories = Factory->GetMenuCategories();
          /*
          * 積で判定するので「UBlueprint」をSupportedClassとする
          * BlueprintアセットタイプアクションとTesBlueprintアセットタイプアクションが
          * 真となることがわかる.
          */
        if (FactoryCategories & AssetTypeCategory)
        {
            FactoriesInThisCategory.Emplace(Factory, Factory->GetDisplayName());
        }
    }
  
    return FactoriesInThisCategory;
  }
  

モードによるエディタレイアウト

エディタレイアウトを定義する方法は以下のような2パターンあります。

  • パターン1:FAssetEditorToolkitを継承するクラスの初期化関数内で定義する方法(EQSエディタ、SoundCueエディタ等)
  • パターン2:FApplicationModeを継承したクラスのコンストラクタ内で定義する方法(Anim Blueprintエディタ、Behavior Treeエディタ等)

パターン2を採用するエディタの特徴として「エディタモード切替をサポートする」ことが挙げられます。

Behavior Treeエディタを開くと右上にBlackboardエディタとBehavior Treeエディタの切り替えボタンがあります。ボタンを押すことで動作が各エディタ固有のものに代わるとともにエディタレイアウトが変更されますが、これはそれぞれFBehaviorTreeEditorApplicationModeとFBlackboardEditorApplicationModeをセットすることで、切り替えを実現しています。

f:id:PavilionDV7:20210506211932p:plain

今回のプロジェクトではエディタ切り替えをサポートするパターン2の方法で実装しています。パターン2の方法は登場するクラスや処理が増えてしまいますが、今回においてはそれによってクラス関係の把握が難しくなることはないはずですし、UE4に登場する主なエディタはパターン2の実装がほとんどなので、土地勘を養う面でも有効だろうということで実装しました。

ExpandNode

UEdGraphNode、UK2Node並びにそれらを継承したノードクラスは「ノードのビジュアルを提供するだけ」のものであり、実際の振る舞いを実装するわけではありません。ノード独自の振る舞いを実装するにはUK2Nodeが持つ「ExpandNode関数」をオーバーライドする必要があります。

ExpandNode関数は文字通り「ノードを展開」する部分で、ブループリントエディタのコンパイルボタン押下時に呼び出されます。

Part6で実装したUTesBPGraphK2Node_TesBeginのExpandNode時のイメージをソース(UTesBPGraphK2Node_TesBegin::ExpandNode)と共に貼ってみます。

  • 展開前

f:id:PavilionDV7:20210506210647p:plain

  • カスタムイベントノードの生成

f:id:PavilionDV7:20210506210651p:plain

    UK2Node_CustomEvent* EntryEventNode = CompilerContext.SpawnIntermediateEventNode<UK2Node_CustomEvent>(this, nullptr, SourceGraph);
    EntryEventNode->bInternalEvent = true;
    EntryEventNode->CustomFunctionName = FName(TEXT("TesBegin"));
    EntryEventNode->AllocateDefaultPins();
  • カスタムイベントとTesBeginノードの以降のノードとの接続

f:id:PavilionDV7:20210506210654p:plain

     const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema();
        UEdGraphPin* EntryNodeOutPin = Schema->FindExecutionPin(*EntryEventNode, EGPD_Output);
        UEdGraphPin* ExecLogicInPin = Schema->FindExecutionPin(*FirstLogicNode, EGPD_Input);
        EntryNodeOutPin->MakeLinkTo(ExecLogicInPin);
    
  • TesBeginノードの接続を解除

f:id:PavilionDV7:20210506210644p:plain

BreakAllNodeLinks();

以上でノードの展開が完了します。あとは生成したカスタムイベントノードをランタイム側から呼び出してあげれば、PrintStringノードが実行され画面上に「Hello」という文字が表示されます。

f:id:PavilionDV7:20210506210841p:plain

エディタに記述した処理がなぜ呼び出せるのか

ExpandNode関数でスポーンしたUK2Node_CustomEventはFKismetCompilerContextにあるPrecompileFunction関数で、セットされた情報をもとにUFunctionオブジェクトを生成します。生成されたUFunctionオブジェクトはBP_TesAssetのリフレクションデータを表すクラス(UBlueprintGeneratedClass)にAddFunctionToFunctionMap関数を使って保存されます。

AddFunctionToFunctionMap関数は呼び出したUClassが持つFuncMapプロパティ(FNameとUFunction*で構成されるTMap)に保存します。これによってエディタ側に生成されたCustomEventをランタイム側でリフレクションデータを通じてProcessEvent関数を使って呼び出すことによって、TesBeginノードから連なる処理を呼び出すことが可能となります。

おわりに

もっと深くエンジンに潜って、エディタ側とランタイム側とのやりとりを知りたい方やブループリントのコンパイルプロセスの様子を見たい方は、是非UE4エディタのデバッグシンボルを導入し、いろいろな所にブレークポイントを置いてステップ実行することをおすすめします。

コンパイルプロセスの出発点としてはUTesBPGraphK2Node_TesBegin::ExpandNodeがおすすめです。

今回の単純なアセットエディタを作るにあたってUE4 Marketplaceにある「Logic Driver Pro」を参考にしました。もしご自身でアセットエディタを作る際には参考にすることをおすすめします。

参考資料

深入Unreal蓝图开发:理解蓝图技术架构 深入Unreal蓝图开发:自定义蓝图节点(上) 深入Unreal蓝图开发:自定义蓝图节点(中) 深入Unreal蓝图开发:自定义蓝图节点(下)

UObject の任意関数の検索と実行

UE4 UnrealC++リフレクションについてのメモ

[UE4]複数のConditionをもつBranchノードを作る

Asset Editor - Development of asset editor in Unreal Engine.

Blueprint Compiler Internals II

Blutilityでノード置換ツールを作ってみた~関数ノード編~