※この記事で使用しているUnrealのVersionは5.6.1です。
※この記事のサンプルプロジェクトはありません。 代わりに、作成したプラグインはそのままGitHubにて公開しております。
※この記事はUnreal Engine (UE) Advent Calendar 2025 の14日目の記事になります。 昨日は我らがおかずさん の猫でもざっくり分かる新しいカメラシステム「Gameplay Camera」 でした! まだ実験的機能とのことですが、内容が濃く今後もGameplay Cameraの機能が拡充されていきそうとのことでめちゃめちゃ参考になる記事でした!
アドカレの記事を日々読ませてもらっていますが、どれも良い記事で素晴らしいなと思いつつ、自分もネタを考えて見たところ結局毎度のちょっとしたEditor拡張ネタとなりました。
目新しいような情報でもありませんが、何かの助けになりましたら幸いです。
前置き
皆さんお久しぶり&はじめましてです。 サカナダヨッ>🐟
皆さんは快適なUnreal Engineライフを過ごしておりますでしょうか?
UEFNのVerseが近い内にBlueprintの座を奪ったりするのかなとか思いつつも、Blueprintエンジョイ勢として色々な案件で日々Unreal Engineを触らせてもらい、少しでも快適に開発できたらなといろんな実装をしてきました。
そんなお魚ですが、今回はUnreal Engine標準で提供されている型であるName(FName)/String(FString)についてのエディタ上の拡張話となります。
※前半は読まなくても本文 は理解できますので、手っ取り早く拡張の実装方法等を知りたい場合は本文 から読むことをおすすめします。
Unreal Engineにおけるグローバルな名前の管理方法について考える レベル【★】
少し話はそれますが、ゲームを開発する上で、「プロジェクト全体で一意の名前(ID)を管理したい 」ケースは非常によくあるかなと思います。
例えば、
・アイテムID ・キャラID ・ステート名 ・敵AIの種類 ・イベントID ・フラグID
…など、ゲーム内の種類・分類を整理するための“グローバルID”をどう設計するかは、プロジェクト規模が大きくなるほど重要になってきます。
では、Unreal Engineではどのような実装パターンが考えられるのでしょうか?
色々考えられますが、自分の中では大まかに以下の4つの選択肢があるかなと思っております。
① GameplayTag
“グローバルなID”として Unreal Engine が標準提供している仕組みです。
公式ドキュメント:Gameplay Tag
Project Settings(プロジェクト設定) にある DefaultGameplayTags.ini 等の設定ファイルに定義するだけで気軽に扱え、使い勝手も非常に良いため、手軽かつ強力に ID を管理したい場合は真っ先に候補 になります(迷ったらGameplayTagでも大体なんとかなるらしい)
C++ からも以下マクロを使用して宣言して使うことができます。
//ヘッダーに記載
#pragma once
#include "NativeGameplayTags.h"
// タグの宣言
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Test);
//Cppファイルに記載
// 実体の定義(コメント無しの場合)
UE_DEFINE_GAMEPLAY_TAG(Ability_Test, "Ability.Test");
// 実体の定義(コメントありの場合)
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Ability_Test,"Ability.Test","Test Ability");
// このCpp内だけで使うタグ
UE_DEFINE_GAMEPLAY_TAG_STATIC(Ability_Test_Internal, "Ability.Test.Internal");
↑のマクロのいずれか(EXTERNは定義と合わせて使用)で、C++で扱いやすい形でGameplayTagを運用することができます。 コード上やエディタ上で一元管理されたタグを使用できるので、定義の手間は多少ありますがC++メインの開発であればGameplayTagが扱いやすくなるかなと思います。
FGameplayTag や FGameplayTagContainer 型で変数化でき、 プロパティウィンドウや Blueprint では専用のドロップダウンUIにより、文字入力なしで確実に選択できます。
また、GameplayTagは使用しているアセットをReference Viewerで探し出すことができるのでどこでどんな使われ方をしているのか検知することも容易です。
さらに、Gameplay Ability System(GAS)をはじめ、エンジン内の多くの機能と強く統合されており、 大規模・長期運用のゲームでも採用価値が高くなります。
ただ、便利すぎるが故に何でもかんでもGameplayTagで実装してしまうようなプロジェクトもよくありますが、Tagが多すぎて何の用途で使っているかもわからないし一覧性にかける場合も出てくるのは気をつけたいところです。
その性質上必然的にTagの名前は階層構造を作ってしまいますが、階層が不要なシンプルな名前を作る場合にも過剰な機能かなとは思います。
今年のアドカレにもいくつか参考になりそうな記事があったので、リンクを貼らせてもらいます。
@T_Sumisaki(Tatsuya Sumisaki) さんGameplayTagのすすめ
@ude1932 さん【UE5】GameplayTagをC++上で参照しやすくする為のツールを作ってみた
② DataTable
扱う名前に紐づいたメタ情報ごとまとめて管理したい場合 に最適な方法です。みんなの味方ですね。
C++ やBlueprintで構造体を定義し、それを基にテーブル化するため、マスターデータのように整理して運用 できます。
公式ドキュメント:DataTable
主なメリットとしては、
・テーブルな見た目なので一覧性がいい ・CSV→DataTable変換で外部ツール(Excel / スプレッドシート)と連携しやすい ・EditorUtility などによる自動化との相性が良い ・アセットとして複数 DataTable を切り替える運用も容易 ・DataAssetと組み合わせることで柔軟性をさらに向上可能
あたりかなと思います。
一方で、RowNameはFName型であるため、GameplayTagほど変数・Blueprint 上でのサポートは厚くありません。
ただし、
・FDataTableRowHandle ・FDataTableCategoryHandle
を使えば、指定 DataTable から 自動ドロップダウンが生成 されるので、実務では十分使えるレベルの UI サポートがあります。
また、FDataTableRowHandleを使っていれば、GameplayTagと同じようにDataTableのRowNameの参照をReference Viewerにて表示することができます。
Blueprint では GetDataTableRow ノードのみドロップダウンサポートがありますが、テーブル指定がピン接続されている場合は候補が表示されなくなる点には要注意です。
③ Enum
名前の選択肢がほぼ固定で、実装者側だけで閉じた用途であれば、シンプルに Enum(列挙型) を使う方法があります。
ドキュメント:【BP】Enumeration(列挙型) 、【C++】Enumeration(列挙型)
Blueprint / C++ どちらでも定義でき、Editor上では必ずドロップダウンUIが表示されるため、入力ミスがなく、安全性の高い選択肢です。コード側で厳密に制御したい(Stateとして扱ったりswitchで分岐する等)場合にも有用です。
Blueprint
C++
UENUM(BlueprintType)
enum class ECharacterType: uint8
{
Human,
Monster,
};
プロパティにすると自動でドロップダウン化され、Blueprint グラフ上でもUIが保証されるため、 「固定の要素をプロジェクト側で制御する」用途にはもってこいです。
ただし、以下のような内容は他の選択肢よりは不向きです。
・値の追加・変更はビルドに影響するため、頻繁に編集する場合 ・外部データ(CSV/スプレッドシート)と連携は難しい ・何十・何百と要素がある場合
④ Config(独自定義)
GameplayTagに依存したくない(階層構造は不要、GameplayTagが膨大になりすぎるのを防ぎたい、用途が限定的等)けれど、DataTable等のアセットにも依存をしたいわけではないなどの他の選択肢の中間的な選択肢として、Configファイル(ini)を使った独自定義 を行う方法も選択肢としてはあります。
公式ドキュメント:Configuration Files
UEでは UCLASS(Config) および UPROPERTY(Config) を使うことで、DefaultGame.iniをはじめ自作ini等から値を読み書きする仕組みを簡単に作れます。
UObject派生であればどんなクラスでも公開できるので、たとえばUGameinstanceSubsystem等のライフサイクルが明確で扱いやすいクラスのプロパティをそのまま公開することができます。
//DefaultGame.iniプロパティを公開
UCLASS(Config=Game,defaultconfig)
class UGlobalNameSettings : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
//UPROPERTYで"Config"を付与することで公開される
UPROPERTY(Config, EditAnywhere, Category="NameList")
TArray<FString> MyNameList;
};
また、iniファイルに公開したプロパティはProject Settings(プロジェクト設定)やEditor Preference(エディタの環境設定)の画面へプロパティを公開できます。 →プロジェクトやプラグインのIModuleInterfaceを継承したクラスのStartupModuleにて公開するクラスを登録することで可能です
virtual void StartupModule() override
{
#if WITH_EDITOR
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule)
{
//Project Settingsへの公開(Editor Preferenceなら第1引数はEditor)
SettingsModule->RegisterSettings(
"Project",
"Game",
"GlobalName",
LOCTEXT("SettingName", "GlobalName"),
LOCTEXT("SettingDescription", "GlobalNameの設定"),
GetMutableDefault<UGlobalNameSettings >()
);
}
#endif
}
virtual void ShutdownModule() override
{
#if WITH_EDITOR
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule)
{
SettingsModule->UnregisterSettings(
"Project",
"Game",
"GlobalName"
);
}
#endif
}
※ちなみに特にクラスにこだわりが無ければ、UDeveloperSettingsを継承しておくとStartupModuleでの登録が不要です。
では、どれを使うべきか?
ここまで見てきたように、 名前(ID)管理の方法は用途に応じて大きく変わります。
方法 用途 長所 短所 GameplayTag ・ゲーム全体で共通のIDを扱う ・ GASやシステム連携が多い ・標準機能でサポートが厚い ・ドロップダウンUIが自動生成 ・階層構造で整理しやすい ・GASとの親和性が高い ・階層構造が不要な用途では過剰 ・タグ数が増えると管理が煩雑 DataTable ・名前にメタ情報を紐付けたい ・マスターデータとして扱いたい ・CSV/Excelと連携しやすい ・自動化・外部編集と相性が良い ・DataAsset併用で柔軟性を高められる ・RowName が FName でUIサポート弱め ・BPのドロップダウンは限定的 Enum ・値が固定 ・実装者側で閉じたID管理 ・常にドロップダウンUIで安全 ・入力ミスが原理的に起こらない(コンパイルで判明) ・コード側で扱いやすい(switch等) ・値の追加/変更でコードに影響(ビルドやコンパイルが必要) ・大量項目には向かない ・外部データとの相性が悪い 独自の定義(Config) ・軽量にIDを扱いたい ・アセット依存を避けたい ・特定用途に最適化したい ・独自の UObject 定義のプロパティをそのまま公開できる ・公開したい型・構造を自由に設計でき、柔軟性が高い ・仕組みを自作する必要がある(ほぼC++) ・標準UIサポートは存在しない ・高度な構造化データには向かない
また、上記の情報以外にもプロジェクトの規模、チーム構成、更新頻度等のコンテキストに応じて、 最適解は変わって来るかなと思いますし、場合によっては複数の合せ技なんかも考えられたりします。
長々と前置きを書きましたが、ここで言いたかったこととしては「名前入力のヒューマンエラーをいかに防ぐか、どのような場所で使われているかが容易に確認できること」がとても重要 という点です。
特に Unreal Engine では、GameplayTagやEnumのように安全なUIが用意されている反面、NameやStringを使う入力欄に関しては “生の文字入力” になっているケースが多く存在します。
でも、すべての状況でこういった直入力を使わないと言うのはなかなか難しいところでもあります。
このような場面では、たとえ内部的にID管理が整備されていても、ユーザー入力が「文字入力」である限りヒューマンエラーは必ず発生します。
また、どこにどんな値が入っているかは当然確認しやすいほうが様々な問題を未然に防げる確率が上がります。
運用でカバーしたいところではありますが、少人数で開発環境を整えずに進めている場合や、割と大規模なプロジェクトほど人力に頼ったりする場合が多くその結果手作業な部分も多くなりがちです(大型RPGの開発なんかはかなりの確率でこういう部分が懸念されます)
そういった場面をなるべく防ぐためにもにできる限り実装側でも配慮できればなと個人的には思うので、今回はその一例としてBlueprintのノードピンおよびプロパティの Name / String の取り扱い について深堀り&拡張してみました。
Name / String を安全に扱える機能 ~GetOptions~ レベル【★★☆】
① 変数のGetOptions
FNameやFStringを使ったUPROPERTYな変数には、GetOptionsというとても便利なメタ指定子があります。
これは、任意のFName,FStringプロパティの入力をドロップダウンからの選択方式に変更してくれるものです。
ドロップダウンの候補にはTArrayのFName,FStringを返す関数を使用することができます。
//ドロップダウンで選択できるName変数
UPROPERTY(EditAnywhere, meta = (GetOptions = "GetMyOptions"))
FName SakanaID;
//ドロップダウンの候補を作成するための関数
UFUNCTION()
TArray<FName> GetMyOptions() const;
これはBlueprintで定義した変数にも設定することができ、Blueprint関数を使ってドロップダウン候補を作成できます。
変数を選択し、DetailsパネルにあるAdvanded以下のDrop-down Optionsの項目に配列のNameかStringを返している関数を選ぶことで有効になります。
デフォルトだと、変数の型と配列で返す型はStringとNameそれぞれ揃えてあげないと候補に出てこないです。
が、指定する関数名は直接文字で入力ができるので、Name(String)変数に対してString(Name)の配列でドロップダウンを作成も可能です。
ちなみに、変数自体をArray,Map,Setに変更すると、Drop-down Optionsの項目が消えてしまいますが、単一プロパティのときにDrop-down Optionsに関数を設定しておくと、Array等に変えてもその要素の中のNameやStringにドロップダウンをつけることが可能です。
↓項目はないけどドロップダウン化されているArray変数
今年のアドカレの記事にもこちらの説明をしている記事がありましたので参考にリンクを貼らせてもらいます。
@mike928neko(Mike Neko) さんBPのみで変数をドロップダウンメニューにしてデバッグを便利にする
② 引数のGetOptions
GetOptionsは関数の引数にも使えます。
C++での引数のドロップダウン化は2通りやり方があります。
↓その1 UFUNCTION(meta = (GetOptions = ...))を使ったやり方 ※Getoptionsの名前、使用する関数名、引数の名前をすべて一致させることでドロップダウン化される
UFUNCTION(BlueprintCallable,meta = (GetOptions = "TargetID"))
void DoSomething(FName TargetID) {};
UFUNCTION(BlueprintCallable)
TArray<FName> TargetID() { return TArray<FName>({ TEXT("Aji"),TEXT("Saba"),TEXT("Iwashi") }); };
↓その2 UPARAM(meta = (GetOptions = ...))を使ったやり方 ※こちらは関数名を自由に設定可能
UFUNCTION(BlueprintCallable)
void DoSomething(UPARAM(meta = (GetOptions = "GetMyOptions"))FName TargetID) {};
UFUNCTION(BlueprintCallable)
TArray<FName> GetMyOptions() { return TArray<FName>({ TEXT("Aji"),TEXT("Saba"),TEXT("Iwashi") }); };
現状、標準機能のBlueprintだけでは関数の引数にドロップダウンはつけられなさそうです。
が、少しC++とEditorUtilityを書くことでBlueprint定義の関数引数にもGetOptionsでドロップダウンをつけることができます。
//Blueprint公開用のC++ライブラリ
UCLASS()
class KACND_API UKAEditorUtilityLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
//Blueprint 関数グラフへ Meta をセットする
UFUNCTION(BlueprintCallable)
static void SetBlueprintFunctionMeta(
UEdGraph* FunctionGraph,
const FName MetaKey,
const FString& MetaValue
)
{
if (!FunctionGraph || MetaKey.IsNone())
{
return;
}
// グラフの Function 用メタ構造体(FKismetUserDeclaredFunctionMetadata)を取得
FKismetUserDeclaredFunctionMetadata* MetaData =
FBlueprintEditorUtils::GetGraphFunctionMetaData(FunctionGraph);
if (!MetaData)
{
return;
}
if (MetaValue.IsEmpty())
{
MetaData->RemoveMetaData(MetaKey);
}
else
{
MetaData->SetMetaData(MetaKey, MetaValue);
}
}
};
↑で定義したSetBlueprintFunctionMeta関数を使ってEditorUtilityで少し処理を書きます。
実行結果
一応これでBlueprintノードにもドロップダウンを仕込むことができました。
この設定は保存もされるのでエディタ起動毎とかに実行する必要はありません。ただ、運用するとなるとドロップダウンがどこで設定されているかがUI上にどこにも表示がないので、設定されたmetaを可視化するツールは作る必要はあるかなと思います。
とまあ、GetOptionsについて説明しましたが、正直これだけでも十分開発する上では助かりますが、実際にプロジェクトで使い込んでいくと、GetOptions だけではカバーしきれない部分も見えてきます。
例えば:
・プロパティ/引数ごとに毎回 meta = (GetOptions = “…”) を書く必要がある C++ でも BP でも「この変数/引数だけ別のリストを使いたい」などが増えると、記述がどんどん散らばる・候補リストの中身がコードやBPに散乱しがち プロジェクト全体で共通の「ID一覧」を DataTable / DataAsset などで一元管理したくても、 実際の GetOptions 実装はあちこちに生えていきがち 「同じIDを扱っているのに中身Iがバラバラ」という状態になりやすい・DisplayName や Tooltip などの「人間向けメタ情報」を付けにくい 内部IDは FName で持ちたいが、UI上は別の表示名で出したい 各候補ごとに説明文を出したい…といったことを、素のGetOptionsだけではできない・コンパイル時の検証(Validate)との連携が弱い この引数に、候補に存在しないIDが入っていたらコンパイルエラー/警告にしたりは難しい 特定の関数・特定の変数だけ、もっと厳しめのチェックをかけたい等がある場合も難しい・どこでどの値が使われているかが辿れない 特定の値を使っている場所一覧のようなものは、アクター等のプロパティの場合複数のWorldに依存するので一括で追いづらい
等、安全に扱いやすくプロジェクト全体でID体系を整えて運用するとなると、もう一段上の“仕組み”が欲しいな…なんてことをよく思っておりました。
ので、
そんなプラグインをなんとなく作ってみようと思います 🐟️
Name / Stringを安全に使えるプラグインの作成 レベル【★★★★】
今回作成するプラグインの完成イメージとして、
1.GetOptionsのようにドロップダウンで入力でき 、表示をカスタマイズする ドロップダウンの値に対応した表示名や補足(Tips)を設定できるように。
2.C++コード不要&Blueprintのみですべての拡張可能(もちろんC++の受け口もあり) →ドロップダウン候補や適用範囲はEditorUtilityなBlueprint(C++)やDataTableで設定可能。 (ブログ説明ではDataTable版は省略)
3.Blueprintのコンパイル時に設定したName/Stringのプロパティの値が規則に違反してたらWarningやErrorを出す →BlueprintCompilerExtensionを使ってコンパイル時のValidate規則に任意の処理を追加可能。
4.Reference Viewerにて設定した値がどこで使われているかを追いかけることができる →プロパティを持つ親のアセットが保存されるときにSearchableNameを付与。
5.すでに開発が進んでいるプロジェクトでも容易に運用できる設計 →プラグイン独自の構造体を使用するやり方ではなく、既存のNameやStringのプロパティ入力方式をカスタマイズする。
みたいにしていきます。
※以降ソースコード説明を書きますが、全文書くと膨大なのでほとんどヘッダーのみ記載しています。あくまで実装の流れを書いているだけでブログのまま実装はできません。全ソースを覗きたい方はこちら を御覧ください。
Step0.準備
まずは新規プラグインを作成します。
PluginsウィンドウからAddボタンにて新規プラグインを追加します。
今回のテンプレートはBlank(一番シンプルな構成)を指定しています。
名前はKACustomNameDropdownとしてあります。
作成したら、upluginとBuild.csを編集します。
今回はEditor専用のモジュールなので、ModuleのTyepをEditorにしてあります。
KACustomNameDropdown.uplugin
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "KACustomNameDropdown",
"Description": "Blueprint関数の引数や変数にあるNameやStringに任意のドロップダウンを提供するプラグイン",
"Category": "EditorTools",
"CreatedBy": "Kinnaji",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "https://kinnaji.com/contact/",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "KACustomNameDropdown",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}
Build.csでは、今回使うモジュールのIncludeをしておきます。
BlueprintGraphやPropertyEditor周りをメインに使うので、↓のような構成になっております。
KACustomNameDropdown.Build.cs
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class KACustomGraphPin : ModuleRules
{
public KACustomGraphPin(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
//このあたりで使うものをInclude
PrivateDependencyModuleNames.AddRange(
new string[]
{
"UnrealEd",
"CoreUObject",
"Engine",
"InputCore",
"Slate",
"SlateCore",
"GraphEditor",
"BlueprintGraph",
"Kismet",
"PropertyEditor"
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
Step1.見た目
まずは見た目となるクラスを作成していきます。
Unrealでは、Editor上のUIの見た目を構成するのはSlateと呼ばれるクラスを継承して作ることができます。
今回はプロパティエディタ上の見た目を構成するクラスとBlueprintのピン上の見た目を構成するクラスの2つを作成します。
プロパティの方はシンプルにSCompoundWidgetを継承、Blueprintのピンに関してはSGraphPinという専用のクラスがあるのでそちらを継承して作成します。
Blueprint上のピンの見た目クラスの定義(SKACND_GraphPinDropdown.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "SGraphPin.h"
/**
* GraphPin用のドロップダウン
*
* ・FName などの値を、検索付きのドロップダウンとして選択できる GraphPin 用Widget。
* ・ピンの接続状態を毎フレーム監視し、接続中は通常のピン表示、
* 非接続時はドロップダウン(コンボボタン+検索+リスト)として振る舞う想定。
*/
class SKACND_GraphPinDropdown : public SGraphPin
{
public:
SLATE_BEGIN_ARGS(SKACND_GraphPinDropdown) {}
SLATE_END_ARGS()
/**
* ピンの構築
*
* @param InArgs Slate用の引数(現状は空)。
* @param InGraphPinObj 対象となる UEdGraphPin。
* @param InNameOptions 実際にピンへ設定する値(文字列)の一覧。
* @param InDisplayNameList UI上で表示するラベルの一覧(InNameOptions と同数 or 空)。
* @param InToolTipsList 各候補に対応するツールチップの一覧(InNameOptions と同数 or 空)。
*
* 内部配列の初期化や各種Widgetの構築を行う。
*/
void Construct(
const FArguments& InArgs,
UEdGraphPin* InGraphPinObj,
TArray<FString> InNameOptions,
TArray<FText> InDisplayNameList,
TArray<FText> InToolTipsList);
/**
* 毎フレームピンを監視して、状態に応じてドロップダウン表示状態を更新
*
* @param AllottedGeometry このWidgetに割り当てられたジオメトリ情報。
* @param InCurrentTime 現在時刻。
* @param InDeltaTime 前フレームからの経過時間。
*
* ・ピンが接続された/外れたタイミングを検知し、
* 標準ピン表示とドロップダウン表示の切り替えや再描画要求などを行う想定。
* ・bWasConnected と実際の接続状態を比較して変化を検出する。
*/
virtual void Tick(
const FGeometry& AllottedGeometry,
const double InCurrentTime,
const float InDeltaTime) override;
protected:
/**
* UI生成
*
* @return ピンのデフォルト値部分に表示するWidget。
*/
virtual TSharedRef<SWidget> GetDefaultValueWidget() override;
/**
* コンボボックスの中のUI生成
*
* @return コンボボタンを押したときに表示されるドロップダウンメニューWidget。
*/
TSharedRef<SWidget> CreateComboDropdown();
/**
* 検索ボックス内テキスト更新時の処理
*
* @param InText 検索ボックスに入力されたテキスト。
*/
void OnSearchTextChanged(const FText& InText);
/**
* コンボボックスの要素UI生成
*
* @param Item 表示対象の要素(Value文字列)。
* @param OwnerTable 親テーブル(SListView)。
* @return 1行分の STableRow。
*/
TSharedRef<ITableRow> GenerateRow(
TSharedPtr<FString> Item,
const TSharedRef<STableViewBase>& OwnerTable);
/**
* ComboBox表示用
*
* @param InItem 選択中の FName 候補。
* @return コンボボタン上に表示するWidget。
*/
TSharedRef<SWidget> MakeNameItemWidget(TSharedPtr<FName> InItem) const;
/**
* 選択中のNameのテキストを取得
*
* @return 現在のピン値に対応する文字列を FText として返す。
*/
FText GetSelectedNameText() const;
/**
* コンボボックスの要素選択時の処理
*
* @param NewSelection 新しく選択された要素(Value文字列)。
* @param SelectInfo 選択がユーザー操作か、プログラム操作かなどの情報。
*
* ・選択された Value をピンの DefaultValue に書き込み、
* グラフへ変更通知を出した上でコンボドロップダウンを閉じる。
*/
void OnItemSelected(TSharedPtr<FString> NewSelection, ESelectInfo::Type SelectInfo);
/**
* ドロップダウンUIの表示更新
*
* ・ピン値の変更や接続状態の変化をグラフに通知する。
*/
void NotifyGraphChanged() const;
/**
* 検索ボックスにキーボードフォーカスを当てる
*
* @return アクティブタイマーの継続/停止状態を返す。
*
* ・ドロップダウンを開いた直後に SearchBox へキーボードフォーカスを移したい場合に使用する。
* ・コンボボタン展開直後はフォーカスが当たっていないことが多いため、
* アクティブタイマーで1フレーム遅らせてフォーカスを与える。
*/
EActiveTimerReturnType FocusSearchBox(double, float);
/*
* 現在のピン値に対応する DisplayName / Tooltip を取得
*
* @param OutDisplayName 現在値に対応する表示名。
* @param OutToolTip 現在値に対応するツールチップ。
* @return 見つかった場合 true、見つからない場合 false。
*/
bool GetCurrentEntryTexts(FText& OutDisplayName, FText& OutToolTip) const;
/**
* インデックスからDisplayName&Tooltip取得
*
* @param Index NameOptions / DisplayNameList / ToolTipsList のインデックス。
* @param OutDisplayName 対応する表示名。
* @param OutToolTip 対応するツールチップ。
* @return 有効なインデックスであれば true、範囲外なら false。
*/
bool GetEntryTextsByIndex(int32 Index, FText& OutDisplayName, FText& OutToolTip) const;
/**
* Itemからインデックスを求める
*
* @param Item NameOptions / FilteredList に含まれる要素(Value文字列)。
* @return 見つかった場合はインデックス、見つからなければ INDEX_NONE。
*/
int32 FindIndexByItem(const TSharedPtr<FString>& Item) const;
/**
* ボタン右側に表示するDisplayNameを返す
*
* @return 現在のピン値に対応する DisplayName。なければ空テキスト。
*/
FText GetCurrentDisplayNameText() const;
/**
* ボタンのTooltipsText
*
* @return 現在のピン値に対応するツールチップ。なければ空テキスト。
*/
FText GetCurrentToolTipText() const;
/**
* DisplayNameの表示状態取得
*
* @return DisplayName を表示するかどうか(Visible / Collapsed)。
*/
EVisibility GetDisplayNameVisibility() const;
/**
* 前のフレームでピンが接続されていたかどうか
*
* ・Tick 内で接続状態の変化を検知するためのフラグ。
* ・「接続状態が変わった瞬間」にだけ UI 更新処理を行いたい場合に利用する。
*/
bool bWasConnected;
/**
* ドロップダウンのリスト
*
* NameOptions : 実際にピンへ書き込む Value の一覧。
* FilteredList : 検索テキストにマッチした NameOptions のサブセット。
* DisplayNameList : 各 Value に対応する表示名。
* ToolTipsList : 各 Value に対応するツールチップ。
*
* ・NameOptions / DisplayNameList / ToolTipsList は同じインデックスで対応させる。
*/
TArray<TSharedPtr<FString>> NameOptions;
TArray<TSharedPtr<FString>> FilteredList;
TArray<TSharedPtr<FText>> DisplayNameList;
TArray<TSharedPtr<FText>> ToolTipsList;
/**
* 各種UIのSlate
*
* SearchBox : フィルタ用の検索テキストボックス。
* FilteredListView : 検索結果を表示する SListView。
* ComboButton : ドロップダウンを開閉するボタン。
*/
TSharedPtr<SSearchBox> SearchBox;
TSharedPtr<SListView<TSharedPtr<FString>>> FilteredListView;
TSharedPtr<SComboButton> ComboButton;
/**
* 検索テキスト
*/
FString SearchText;
};
プロパティ編集画面の見た目クラスの定義(SKACND_PropertyDropdown.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class IPropertyHandle;
/**
* プロパティ用のドロップダウン
*
* ・DetailCustomization 等から利用する、検索付きのドロップダウンWidget。
* ・外部から渡された候補(NameOptions / DisplayNameOptions / ToolTipOptions)の中から
* 1つを選択し、PropertyHandle で指定されたプロパティに Value を書き込む。
*/
class SKACND_PropertyDropdown : public SCompoundWidget
{
public:
/**
* Slate引数定義
*
* - PropertyHandle : バインド先となるプロパティハンドル。
* - NameOptions : プロパティへ設定する生の値(Value)一覧。
* - DisplayNameOptions : エディタ上に表示するラベル一覧。NameOptions と同数 or 空。
* - ToolTipOptions : 各候補に対応するツールチップ。NameOptions と同数 or 空。
*/
SLATE_BEGIN_ARGS(SKACND_PropertyDropdown) {}
SLATE_ARGUMENT(TSharedPtr<IPropertyHandle>, PropertyHandle)
SLATE_ARGUMENT(TArray<FString>, NameOptions)
SLATE_ARGUMENT(TArray<FText>, DisplayNameOptions) // NameOptionsと同数 or 空
SLATE_ARGUMENT(TArray<FText>, ToolTipOptions) // NameOptionsと同数 or 空
SLATE_END_ARGS()
/**
* ウィジェット構築
*
* @param InArgs Slate引数。PropertyHandle や候補配列が渡される。
*
* 引数から内部状態を初期化し、ルートWidget(コンボボタン+現在値表示)を組み立てる。
*/
void Construct(const FArguments& InArgs);
private:
/**
* バインド対象プロパティ
*
* ・選択された Value(NameOptions の要素)をこのプロパティに書き込む。
* ・現在のプロパティ値を取得して表示用テキストを更新する際にも利用する。
*/
TSharedPtr<IPropertyHandle> PropertyHandle;
/**
* 生の候補配列
*
* NameOptions : プロパティに直接入る値(Value)の一覧。
* DisplayNameOptions : UI上に表示するラベル。空の場合は NameOptions をそのまま表示する想定。
* ToolTipOptions : 各候補の説明テキスト。空の場合は特にツールチップ無し。
*/
TArray<FString> NameOptions;
TArray<FText> DisplayNameOptions;
TArray<FText> ToolTipOptions;
/**
* SListView 用に NameOptions を TSharedPtr<FString> でラップしたリスト。
*/
TArray<TSharedPtr<FString>> NameStringList;
/**
* コンボボタン本体
*/
TSharedPtr<SComboButton> ComboButton;
/**
* 候補一覧
*/
TSharedPtr<SListView<TSharedPtr<FString>>> ListView;
/**
* 検索文字列
*/
FString SearchText;
/**
* フィルタ済みリスト
*/
TArray<TSharedPtr<FString>> FilteredList;
private:
/**
* 全体のルートWidget(ComboButton + 表示部分)を構築する。
*
* @return ルートとなる SWidget。
*/
TSharedRef<SWidget> BuildRootWidget();
/**
* ドロップダウンメニューの中身を構築する。
*
* @return コンボボタン展開時に表示するメニューWidget。
*/
TSharedRef<SWidget> BuildDropdownMenu();
/*
* ListView Row生成
*
* @param Item 表示対象の要素(Value文字列)。
* @param OwnerTable 親テーブル(SListView)。
* @return 1行分の STableRow。
*/
TSharedRef<ITableRow> GenerateRow(TSharedPtr<FString> Item, const TSharedRef<STableViewBase>& OwnerTable);
/**
* 選択変更時
*
* @param NewSelection 新しく選択された要素(Value文字列)。
* @param SelectInfo 選択がユーザー操作かプログラムかなどの情報。
*/
void OnItemSelected(TSharedPtr<FString> NewSelection, ESelectInfo::Type SelectInfo);
/**
* 検索入力時
*
* @param InText 検索ボックスに入力されたテキスト。
*/
void OnSearchTextChanged(const FText& InText);
/**
* 現在のプロパティ値を文字列で取得
*
* @return プロパティに設定されている Value を FString として返す。
*/
FString GetCurrentValueString() const;
/**
* 現在値から DisplayName / Tooltip を取得
*
* @param OutDisplayName 現在値に対応する表示名。
* @param OutTooltip 現在値に対応するツールチップ。
* @return 対応エントリが見つかった場合 true、見つからない場合 false。
*/
bool GetCurrentEntryTexts(FText& OutDisplayName, FText& OutTooltip) const;
/*
* 下に表示する DisplayName(人間に見せるラベル)
*
* @return 現在の Value に対応する表示名。見つからない場合は空、または Value をそのまま返す想定。
*/
FText GetDisplayNameText() const;
/**
* DisplayName 表示の Visibility
*
* @return DisplayName を表示するかどうか(Visible / Collapsed)。
*/
EVisibility GetDisplayNameVisibility() const;
/**
* 現在の Value 表示用テキスト
*
* @return 現在のプロパティ値(Value)を FText に変換したもの。
*/
FText GetValueText() const;
/**
* ツールチップテキスト
*
* @return 現在の Value に対応するツールチップ。なければ空テキスト。
*/
FText GetTooltipText() const;
};
次に、各種Slateクラスを登録するためのクラスを定義します。
プロパティエディタに関しては、IPropertyTypeCustomizationというクラスを継承することで独自のプロパティUIを作成できます。
Blueprintのグラフピンに関しては、FGraphPanelPinFactoryというクラスを継承することでグラフピンを独自のピンUIを作成できます。
独自プロパティエディタ作成用クラス定義(KACND_NamePropertyCustomization.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
class UKACND_DropdownData;
class IPropertyHandle;
/**
* FName用のプロパティカスタマイズ
* クラス+プロパティ名を使ってDropdownProviderを検索し、
* SKACND_PropertyDropdownで描画する。
*/
class FKACND_NamePropertyCustomization : public IPropertyTypeCustomization
{
public:
/**
* このカスタマイズクラスのインスタンスを生成するためのFactory関数。
*/
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
/**
* プロパティ行のヘッダ部(ラベル+値部分)のウィジェットをカスタマイズする。
*
* @param PropertyHandle 対象プロパティを表すハンドル。
* @param HeaderRow ヘッダ行(ラベルと値の行)に対してウィジェットを追加するためのビルダー。
* @param CustomizationUtils カスタマイズ処理用のユーティリティ群(検索やリフレッシュ時に使用可能)。
*/
virtual void CustomizeHeader(
TSharedRef<IPropertyHandle> PropertyHandle,
FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& CustomizationUtils) override;
/**
* 子プロパティのカスタマイズ用。
*
* 単体のプロパティを対象としているため、
* 現状では子プロパティを持たず、特に何も行わない。
* ※将来的に構造体など複合型に拡張する場合は、ここで子要素のレイアウトを定義する。
*/
virtual void CustomizeChildren(
TSharedRef<IPropertyHandle> PropertyHandle,
IDetailChildrenBuilder& ChildBuilder,
IPropertyTypeCustomizationUtils& CustomizationUtils) override
{
}
private:
/**
* 対象プロパティに紐づくドロップダウン用の表示データを取得する。
*
* @param PropertyHandle 対象プロパティのハンドル。
* @param OutNameList 実際にプロパティへセットする内部値(FName化される文字列)の一覧。
* @param OutDisplayNameList エディタ上のドロップダウンに表示するラベル用テキスト一覧。
* @param OutToolTipList 各項目に対応するツールチップテキスト一覧。
* @return true を返した場合は、Out~ に有効なリストが詰められており、
* false の場合はドロップダウンとして描画するデータが見つからなかったことを意味する。
*/
bool GetDropdownListsForProperty(
TSharedRef<IPropertyHandle> PropertyHandle,
TArray<FString>& OutNameList,
TArray<FText>& OutDisplayNameList,
TArray<FText>& OutToolTipList) const;
/**
* このカスタマイズで使用する DropdownData アセットを取得する。
*
* @return 利用可能な UKACND_DropdownData へのポインタ。
* 見つからなかった場合や無効な場合は nullptr を返す。
*/
UKACND_DropdownData* GetDropdownData() const;
};
独自Blueprintピン作成用クラス定義(KACND_GraphPanelPinFactory.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "EdGraphUtilities.h"
/**
* 独自のGraphPinを作成するためのFactoryクラス
*
* ・FEdGraphUtilities::RegisterVisualPinFactory で登録しておくことで、
* 対象となるピン(FName 等)に対してカスタム SGraphPin(SKACND_GraphPinDropdown)を差し替える。
* ・どのピンに対してカスタムピンを返すかの判定ロジックは CreatePin 内で実装する。
*/
class FKACND_GraphPanelPinFactory : public FGraphPanelPinFactory
{
public:
/**
* GraphPinを作成
*
* @param InPin 視覚化対象の UEdGraphPin。
* @return カスタムピンを生成する場合はそのインスタンス、対象外なら nullptr。
*
* ・InPin の PinType / 所属ノード / 所属グラフ などをチェックし、
* KACustomNameDropdown の対象となるピンかどうかを判定する。
* ・対象であれば SKACND_GraphPinDropdown などの SGraphPin 派生クラスを生成して返す。
* ・nullptr を返した場合、そのピンはデフォルトの描画(標準の SGraphPin)が使用される。
*/
virtual TSharedPtr<SGraphPin> CreatePin(UEdGraphPin* InPin) const override;
};
次に、それぞれの見た目をUnreal Editorが使用してくれるように登録する処理を記載します。
作成したプラグインのモジュールクラスのStartupModuleとShutdownModuleをオーバーライドし中身を記載します。
プラグインのモジュールクラス(KACustomNameDropdown.h/cpp)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "KACND_GraphPanelPinFactory.h"
#include "Modules/ModuleManager.h"
#include "UObject/ObjectSaveContext.h"
/**
* プラグイン用モジュール
*/
class FKACustomNameDropdownModule : public IModuleInterface
{
public:
/**
* モジュール起動時の処理
*
* ・エディタ起動/ゲーム起動時に一度だけ呼ばれる。
*/
virtual void StartupModule() override;
/**
* モジュール終了時の処理
*
* ・エディタ終了時など、モジュールがアンロードされるタイミングで呼ばれる。
*/
virtual void ShutdownModule() override;
/**
* ドロップダウンUIを生成するためのファクトリー
*/
TSharedPtr<FKACND_GraphPanelPinFactory> GraphPinFactory;
};
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACustomNameDropdown.h"
#include "KACND_NamePropertyCustomization.h"
#include "PropertyEditorModule.h"
#include "ISettingsModule.h"
#include "KACND_GraphPanelPinFactory.h"
#define LOCTEXT_NAMESPACE "FKACustomNameDropdownModule"
void FKACustomNameDropdownModule::StartupModule()
{
//GraphPinFactoryを登録
{
GraphPinFactory = MakeShareable(new FKACND_GraphPanelPinFactory());
FEdGraphUtilities::RegisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//プロパティ編集画面でドロップダウン設定されたNameProperty/StringPropertyの見た目を変えるためのCustomizationを登録する
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout(
"NameProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.RegisterCustomPropertyTypeLayout(
"StrProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}
}
void FKACustomNameDropdownModule::ShutdownModule()
{
//GraphPinFactoryを登録解除
if (GraphPinFactory.IsValid())
{
FEdGraphUtilities::UnregisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//NamePropertyのCustomizationの登録解除
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("NameProperty");
PropertyModule.UnregisterCustomPropertyTypeLayout("StrProperty");
PropertyModule.NotifyCustomizationModuleChanged();
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FKACustomNameDropdownModule, KACustomNameDropdown)
これでひとまず見た目を作成することができるようになりました。
↓作成したクラスを使った見た目(プロパティエディタ)
↓作成したクラスを使った見た目(Blueprintピン)
実際の値をドロップダウン化した上、表示名を下に追加しました。また、ドロップダウンの候補や表示されているUIにカーソルを合わせることで設定したTipsが出るようにもなっています。
Step2.ドロップダウンの設定
見た目はできたので、今度は任意のプロパティ/ピンに対して自分で定義したドロップダウンの候補を設定できるようにします。
この設定はDataAssetを用いて設定項目を1箇所で管理できるような設計を目指します。また、登録できるものは独自のEditorUtilityなC++/Blueprintなクラスでの拡張も行えるものにしていきます。
まずは、DataAssetに登録するEditorUtilityな設定用のクラスを定義します。後ほど説明する内容ですが、このクラスではドロップダウン設定の他にもコンパイル時のValidation設定やSearchableName付与の設定等もできるようにしておきます。
ドロップダウンの候補値を提供するクラス(KACND_DropdownProvider.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "KACND_DropdownProvider.generated.h"
/**
* ドロップダウンの候補値を提供するクラス
* KACND_DropdownDataへ任意に登録可能
*
* ※UKACND_SettingsのbEnableCustomPinValidationがtrueな場合エディタ起動時にロードされるので
* GetNameListでAsset等の参照を行うと場合によってはエディタの初期ロードが遅くなるため適度に最適化を推奨
*
* もしくはコンパイルサポート不要ならUKACND_SettingsのbEnableCustomPinValidationはOffにするのを推奨
*/
UCLASS(Abstract, Blueprintable, EditInlineNew)
class KACUSTOMNAMEDROPDOWN_API UKACND_DropdownProviderBase : public UObject
{
GENERATED_BODY()
public:
/**
* ドロップダウン化するピンの名前
* ※空の場合はピンへの適用なし
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TargetName")
TArray<FName> TargetPinNameList;
/**
* ドロップダウン化する変数の名前
* ※空の場合は変数への適用なし
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "TargetName")
TArray<FName> TargetVariableNameList;
/**
* ドロップダウン化する関数のピンまたは変数を持つ関数名
* ※空の場合は関数名でのフィルタはなし
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Filter")
TArray<FName> TargetFunctionNameList;
/**
* ドロップダウン化する関数のピンまたは変数を持つ関数のクラス
* ※空の場合は定義クラスでのフィルタはなし
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Filter")
TArray<FSoftClassPath> DefinitionClassList;
/**
* CompileでValidateするか?
* ※ProjectSettings にある "EnableCustomPinValidation" が有効でないと機能しません
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Validate")
bool bEnableValidateCompile = true;
/**
* Validate時のCompileContext("Warning" or "Error" or "Note")
*
* ・OnValidateGraphPinValue が false を返した場合に、
* BlueprintCompileExtension 側でどのレベルのメッセージを出すかを指定する。
*/
UPROPERTY(Category = "Validate", EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bEnableValidateCompile", EditConditionHides))
EKACND_BlueprintCompileContext CompileContext = EKACND_BlueprintCompileContext::Warning;
/**
* Serialize時のSearchableNameを付与する際Validateを行うか?
* ※ProjectSettings にある "bEnableInjectSearchableName" が有効でないと機能しません
*
* ・true の場合、Serialize 時(SearchableName 差し込み時)に
* OnValidateGraphPinValue / OnValidateVariableValue による Validate を行う。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Validate")
bool bEnableSearchableNameValidate = true;
/**
* Serialize時のValidateでMessageLogに出すMessageType
*
* ・Serialize(保存)時に不正値が見つかった場合、
* MessageLog にどの種別(Warning / Error / Info)で出力するかを指定。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Validate", meta = (EditCondition = "bEnableSearchableNameValidate", EditConditionHides))
EKACND_MessageLogType SearchableNameValidateMessageType = EKACND_MessageLogType::Warning;
/**
* MessageLogに出力された際にMessageWindowを開くか?
*
* ・true の場合、Serialize 時の Validate でメッセージが出力された際に
* 自動的に MessageLog ウィンドウを開く。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Validate", meta = (EditCondition = "bEnableSearchableNameValidate", EditConditionHides))
bool bOpenMessageWindow = true;
/**
* ドロップダウンの候補を取得
* DisplayNameやTooltipsを設定可能
* ここで返した値がそのままドロップダウン候補になる
*
* ※この処理内で他のAssetのロードを行うと ensure に引っかかる可能性があります
* (特にエディタ起動直後や Compile 中など、Unsafe なタイミングで呼ばれ得るため)。
*
* @param DropdownList 候補要素を詰める配列(Value / DisplayName / ToolTip)。
*/
UFUNCTION(Category = "DropdownProvider", BlueprintNativeEvent, BlueprintCallable)
void GetNameList(TArray<FKACND_DropdownInfo>& DropdownList) const;
virtual void GetNameList_Implementation(TArray<FKACND_DropdownInfo>& DropdownList) const;
/**
* BlueprintCompile時やアセット保存時にValidateする (GraphPin用)
*
* ※この処理内で他のAssetのロードを行うと ensure に引っかかる可能性があります
*
* @param OwnerBlueprint 対象の Blueprint。
* @param PinDefaultValue ピンのデフォルト値(文字列)。
* @param PinName ピン名。
* @param FunctionName ピンが属している関数名。
* @param bValidateSuccess Validateの結果。
* @param bSaveSearchableName SearchableNameに現在の値を保存するか?
* @param OutMessage エラーまたは警告内容。
*/
UFUNCTION(Category = "DropdownProvider", BlueprintNativeEvent, BlueprintCallable)
void OnValidateGraphPinValue(
const UBlueprint* OwnerBlueprint,
const FString& PinDefaultValue,
const FName& PinName,
const FName& FunctionName,
bool& bValidateSuccess,
bool& bSaveSearchableName,
FText& OutMessage
) const;
virtual void OnValidateGraphPinValue_Implementation(
const UBlueprint* OwnerBlueprint,
const FString& PinDefaultValue,
const FName& PinName,
const FName& FunctionName,
bool& bValidateSuccess,
bool& bSaveSearchableName,
FText& OutMessage
) const;
/**
* アセット保存時等のSerialize時にValidateする (Variable用)
*
* ※この処理内で他のAssetのロードを行うと ensure に引っかかる可能性があります
*
* @param OwnerObject 対象オブジェクト(変数を持っているアセットなど)。
* @param VariableValue 変数に設定されている値(文字列)。
* @param VariableName 変数名。
* @param bValidateSuccess Validateの結果。
* @param bSaveSearchableName SearchableNameに現在の値を保存するか?
* @param OutMessage エラーまたは警告内容。
*/
UFUNCTION(Category = "DropdownProvider", BlueprintNativeEvent, BlueprintCallable)
void OnValidateVariableValue(
const UObject* OwnerObject,
const FString& VariableValue,
const FName& VariableName,
bool& bValidateSuccess,
bool& bSaveSearchableName,
FText& OutMessage
) const;
virtual void OnValidateVariableValue_Implementation(
const UObject* OwnerObject,
const FString& VariableValue,
const FName& VariableName,
bool& bValidateSuccess,
bool& bSaveSearchableName,
FText& OutMessage
) const;
private:
};
次に、KACND_DropdownProviderを登録することができるDataAssetクラスを作成します。
ドロップダウンの定義を格納するクラス定義(KACND_DropdownData.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "KACND_DropdownProvider.h"
#include "KACND_DropdownData.generated.h"
/**
* 各種ドロップダウン作成名
*
* ・「どの名前に対してマッチさせるか」を示す区別用 Enum。
*
* ForFirstMatchingProvider の Type 引数に渡して、ターゲットとなる名前の種別を指定する。
*/
UENUM()
enum EKACND_DropdownNameType
{
//関数ピン名に対してマッチさせる
Pin,
// Blueprint 変数名に対してマッチさせる
Variable,
};
/**
* 各種ドロップダウン設定を格納するクラス
*/
UCLASS(BlueprintType)
class KACUSTOMNAMEDROPDOWN_API UKACND_DropdownData : public UDataAsset
{
GENERATED_BODY()
public:
/**
* ドロップダウンの候補設定
* ※BP等で NameList を作成する場合にオーバーライドする用
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "DropdownSettings", Instanced, NoClear)
TArray<TObjectPtr<UKACND_DropdownProviderBase>> DropdownProviderList;
public:
/*
* 最初にマッチした Provider でラムダ実行可能な template 関数
*
* @tparam FuncType Provider を受け取って処理を行う Callable(ラムダ等)。
* @param TargetName ピン名 or 変数名。
* @param FunctionClass 対象となる関数を持つクラス(null 可)。
* @param FunctionName 対象となる関数名。
* @param Type TargetName の種類(Pin / Variable)。
* @param InFunc マッチした Provider に対して実行する処理。
*
* 使い方イメージ:
* ForFirstMatchingProvider(
* PinName,
* FunctionClass,
* FunctionName,
* EKACND_DropdownNameType::Pin,
* [](UKACND_DropdownProviderBase* Prov)
* {
* TArray<FKACND_DropdownInfo> List;
* Prov->GetNameList(List);
* // ここで List を使って UI を構築する etc...
* });
*/
void ForFirstMatchingProvider(
const FName& TargetName,
UClass* FunctionClass,
const FName& FunctionName,
EKACND_DropdownNameType Type,
TFunction<void(UKACND_DropdownProviderBase*)> InFunc);
};
このDataAssetクラスを元に、エディタ上でDataAssetを作成するとUKACND_DropdownProviderを継承したクラスを自由に設定できるようになります。
テクニックとしては、EditInlineNewなUCLASSをInstancedなオブジェクトのUPROPERTY変数に設定することで、変数には自動で設定用オブジェクトのインスタンスが作成され、独自の設定を行えるようになります。
これを最初に作成したSlateクラスで、ここで指定したピンや変数の名前だったらドロップダウンUIを展開するようにすることで設定した情報をそのままピンや変数上に表示が可能です。
ここまでできたので、試しにBlueprintで定義したドロップダウンを使って見ます。
↓EditorUtilityBlueprintとしてUKACND_DropdownProviderを継承。
↓GetNameListをオーバーライドして、ドロップダウンの要素を作成。TargetNamePinListやTargetVariableNameListに任意の名前を指定。 ※BlueprintNativeEventなのでBlueprintでもC++でも処理をかけます。
↓結果
Step3.コンパイル時のValidation
基礎の設計はできたので、次はValidation(設定された値を検証して、ルール違反をしていた場合に通知したりすること)を拡充していきます。
Blueprintには様々な便利拡張がありますが、今回はUBlueprintCompilerExtensionを使ってBlueprintコンパイル時にドロップダウン候補外の値がピンに入っている場合に通知を出すようにしてみます。
まず、UBlueprintCompilerExtensionを継承したクラスを作成します。
Blueprintコンパイルの拡張クラス定義(KACND_BlueprintCompilerExtension.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "BlueprintCompilerExtension.h"
#include "KACND_BlueprintCompilerExtension.generated.h"
/**
* Blueprint コンパイル時に
* 関数ノードの Name/String ピンを検証する拡張
*/
UCLASS()
class KACUSTOMNAMEDROPDOWN_API UKACND_BlueprintCompilerExtension : public UBlueprintCompilerExtension
{
GENERATED_BODY()
protected:
/**
* Blueprint コンパイル完了後の処理
*
* @param CompilationContext コンパイルコンテキスト(Blueprint やメッセージログなどを参照可能)。
* @param Data コンパイル結果データ。
*/
virtual void ProcessBlueprintCompiled(
const FKismetCompilerContext& CompilationContext,
const FBlueprintCompiledData& Data
) override;
/**
* ノード(K2Node_CallFunction or K2Node_VariableSet)の Name/String ピン検証
*
* @param CompilationContext コンパイルコンテキスト。
*
* ・Blueprint 内のすべてのグラフを走査し、
* K2Node_CallFunction or K2Node_VariableSet ノードを対象に Name / String 型ピンを抽出する。
* ・各ピンについて、UKACND_DropdownData / Provider 設定に基づいて
* 「ドロップダウン候補として妥当な値が入っているか」をチェックする。
* ・不正値が見つかった場合は CompileContext(Warning / Error / Note)に応じて
* CompilationContext.MessageLog にメッセージを追加する。
*/
void ValidateNodes(const FKismetCompilerContext& CompilationContext) const;
};
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACND_BlueprintCompilerExtension.h"
#include "KACND_DropdownProvider.h"
#include "KACND_Utility.h"
#include "EdGraph/EdGraphPin.h"
#include "Engine/Blueprint.h"
#define LOCTEXT_NAMESPACE "KACustomNameDropdown"
void UKACND_BlueprintCompilerExtension::ProcessBlueprintCompiled(
const FKismetCompilerContext& CompilationContext,
const FBlueprintCompiledData& Data
)
{
Super::ProcessBlueprintCompiled(CompilationContext, Data);
const UBlueprint* Blueprint = CompilationContext.Blueprint;
if (!Blueprint)
{
return;
}
// 関数ノードの引数ピン検証
ValidateNodes(CompilationContext);
}
void UKACND_BlueprintCompilerExtension::ValidateNodes(
const FKismetCompilerContext& CompilationContext
) const
{
TArray<FName> OutNames;
//Compile失敗時のメッセージやCompileContextの設定
auto CompileFunc = [&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov, UEdGraphPin* Pin)
{
if (!Prov || !Prov->bEnableValidateCompile)
{
return;
}
switch (Prov->CompileContext)
{
case EKACND_BlueprintCompileContext::Error:
CompilationContext.MessageLog.Error(
*MessageText.ToString(),
Pin
);
break;
case EKACND_BlueprintCompileContext::Warning:
CompilationContext.MessageLog.Warning(
*MessageText.ToString(),
Pin
);
break;
case EKACND_BlueprintCompileContext::Note:
CompilationContext.MessageLog.Note(
*MessageText.ToString(),
Pin
);
break;
default:
break;
}
};
//Blueprintのインスタンスから、ノードとして使用されているドロップダウンピンを探し出して
//リスト化したりValidationに引っかかった場合に実行できるラムダ関数を処理できるヘルパ関数
//ブログでは説明を省略(実際の処理はGitHubを見てください)
KACND::Util::CollectNamesFromBlueprint(CompilationContext.Blueprint, OutNames, CompileFunc);
}
#undef LOCTEXT_NAMESPACE
作成したBlueprintCompilerExtensionクラスは、プラグインモジュールのStartupModule関数にて登録を行います。
プラグインのモジュールクラス(KACustomNameDropdown.h/cpp)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACustomNameDropdown.h"
#include "KACND_NamePropertyCustomization.h"
#include "PropertyEditorModule.h"
#include "ISettingsModule.h"
#include "KACND_GraphPanelPinFactory.h"
#include "KACND_BlueprintCompilerExtension.h"
#include "BlueprintCompilationManager.h"
#define LOCTEXT_NAMESPACE "FKACustomNameDropdownModule"
void FKACustomNameDropdownModule::StartupModule()
{
//GraphPinFactoryを登録
{
GraphPinFactory = MakeShareable(new FKACND_GraphPanelPinFactory());
FEdGraphUtilities::RegisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//プロパティ編集画面でドロップダウン設定されたNameProperty/StringPropertyの見た目を変えるためのCustomizationを登録する
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout(
"NameProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.RegisterCustomPropertyTypeLayout(
"StrProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}
// Blueprintコンパイル時に独自の拡張ルールを登録
if(UBlueprint::StaticClass())
{
if (GetMutableDefault<UKACND_BlueprintCompilerExtension>())
{
FBlueprintCompilationManager::RegisterCompilerExtension(
UBlueprint::StaticClass(),
GetMutableDefault<UKACND_BlueprintCompilerExtension>()
);
}
}
}
void FKACustomNameDropdownModule::ShutdownModule()
{
//GraphPinFactoryを登録解除
if (GraphPinFactory.IsValid())
{
FEdGraphUtilities::UnregisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//NamePropertyのCustomizationの登録解除
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("NameProperty");
PropertyModule.UnregisterCustomPropertyTypeLayout("StrProperty");
PropertyModule.NotifyCustomizationModuleChanged();
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FKACustomNameDropdownModule, KACustomNameDropdown)
これで、コンパイルに対して独自の挙動やメッセージを仕込むことができるようになりました。
試しにドロップダウン外の候補の場合にコンパイルエラーにして独自のメッセージを出してみます。
Blueprintで返した結果がBlueprintで使われて…若干面白いですね。
ワンポイントですが、アセットのロード時(削除中のルーティング等)にBlueprintImplementableEvent(BlueprintNativeEvent)は呼んではいけないので、FUObjectThreadContext::Get().IsRoutingPostLoadがTrueの場合は安全を考慮してスキップするようにしています。
void UKACND_BlueprintCompilerExtension::ProcessBlueprintCompiled(
const FKismetCompilerContext& CompilationContext,
const FBlueprintCompiledData& Data
)
{
Super::ProcessBlueprintCompiled(CompilationContext, Data);
const UBlueprint* Blueprint = CompilationContext.Blueprint;
if (!Blueprint)
{
return;
}
//BlueprintNativeEventを読んではいけないタイミング
//アセットのルーティングによるロード中の場合
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
{
return;
}
// 関数ノードの引数ピン検証
ValidateNodes(CompilationContext);
}
ちなみに、このコンパイルがどれだけの負荷がかかっているかもチェックしてみました。
コンパイルエラーを出すノードを500ほど用意し、ValidateをONとOFFで検証。
Validate:OFFのとき 大体230ms前後(5回検証)
Validate:ONのとき 大体250ms前後(5回検証)
コンパイル時間はONにすると少し増えてますが、これぐらいなら許容かなと思います。 (設定する項目が増えるとどんどん増えていきますのでこのあたりは必要に応じてONOFFを調整かなと思います)
ここまで来るとプラグインとして拡張した意義が少しずつですが生まれてきました。
Step4.SearchableNameによるReference Viewerへの参照表示
さて、文字列を使っているとどこで使われているかを判別したくなると思いますが、冒頭でGameplayTagやDataTableの項目で説明しましたReference Viewerへの参照表示機能を、任意のオブジェクトが持つFNameプロパティに対しても設定できるようにします。
この機能はSearchableNameというメタデータを、アセットがロードやセーブされるタイミングで呼び出されるSerialize関数で設定することで実装できます。 →AssetRegistry等から高速で検索できるように、アセットそのものに依存しない設計となっているため、アセットとなるObject本体が参照を持つのではなく、ファイルそのものとのやり取りのようなイメージで作られています。
故にSearchableNameを仕込めるタイミングが限られていたりするので外から制御するような実装を行うために少しトリッキーな実装をします。
まず、アセットへのSeachableNameを付与するためのクラスを作成します。
SeachableNameを付与するためのクラスの定義(KACND_SearchableNameInjector.h/cpp)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Logging/TokenizedMessage.h"
#include "KACND_SearchableNameInjector.generated.h"
/**
* ドロップダウンで使用している値が ReferenceViewer で確認できるようにするための SearchableNames 差し込み用クラス
*
* ・パッケージにぶら下げておき、Serialize タイミングで SearchableName を注入する役割を持つ UObject。
* ・Blueprint やその他アセット内で使われているドロップダウンの値を走査し、
* それらを SearchableName としてアセットに紐づけることで、
* ReferenceViewer 上から「この値がどこで使われているか」を追えるようにする。
*/
UCLASS()
class KACUSTOMNAMEDROPDOWN_API UKACND_SearchableNameInjector : public UObject
{
GENERATED_BODY()
public:
/**
* Serialize
*
* @param Ar アーカイブ。
*/
virtual void Serialize(FArchive& Ar) override;
/**
* 使用されているドロップダウンの値を元に、ReferenceViewerで表示される情報を差し込む
*
* @param Ar アーカイブ。
*
* ・内部で TargetAsset を走査し、収集した値を SearchableName として書き込む。
*/
void InjectSearchableNames(FArchive& Ar);
/**
* 任意のオブジェクトに対して SearchableName を注入する静的ヘルパー
*
* @param Ar アーカイブ。
* @param InObject 対象となる UObject(Blueprint や ExternalActor など)。
*
* ・一時的な Injector を介さずに、「このオブジェクトに対してだけ SearchableName を注入したい」
* 場面で呼び出せるユーティリティ(ExternalActor等で有用)
*/
static void InjectSearchableNames(FArchive& Ar, UObject* InObject);
/**
* Editor 専用オブジェクトであることを示す
*
* @return 常に true(パッケージに SearchableName 用メタ情報だけを追加する Editor 専用クラス)。
*/
virtual bool IsEditorOnly() const override;
/**
* 保存対象
*
* ・SearchableName を注入する実際のアセット。
* ・Blueprint や ExternalActor など、パッケージ内のルートオブジェクトを想定。
*/
TWeakObjectPtr<UObject> TargetAsset;
};
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACND_SearchableNameInjector.h"
#include "KACND_Utility.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "K2Node_CallFunction.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_VariableSet.h"
#include "K2Node_Variable.h"
#include "Logging/MessageLog.h"
#include "Misc/UObjectToken.h"
#define LOCTEXT_NAMESPACE "KACustomNameDropdown"
void UKACND_SearchableNameInjector::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
//エディタの保存
const bool bIsEditorPackageSave =
Ar.IsSaving() &&
!Ar.IsTransacting() &&
Ar.IsPersistent() &&
Ar.IsObjectReferenceCollector() &&
(!Ar.IsIgnoringOuterRef() ||
!Ar.ShouldSkipBulkData()) &&
!(Ar.GetPortFlags() & PPF_Duplicate);
//パッケージ時の保存
const bool bIsCookSave =
Ar.IsSaving() &&
Ar.IsCooking();
if (!bIsEditorPackageSave && !bIsCookSave)
{
return;
}
//SearchableNameを書き込む
InjectSearchableNames(Ar);
}
void UKACND_SearchableNameInjector::InjectSearchableNames(FArchive& Ar)
{
UKACND_SearchableNameInjector::InjectSearchableNames(Ar, TargetAsset.Get());
}
void UKACND_SearchableNameInjector::InjectSearchableNames(FArchive& Ar, UObject* InObject)
{
UKACND_Settings* Settings = GetMutableDefault<UKACND_Settings>();
//設定オブジェクト確認&データアセットのロード状態確認
if (!Settings || !Settings->IsLoadedDropdownData())
{
return;
}
//データアセット確認
UKACND_DropdownData* DA = Settings->GetDropdownData();
if (!DA)
{
return;
}
//MessageLogウィンドウにメッセージを追加する(bOpenMessageWindowがTrueならWindowを開く)
auto MsgFunc = [&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov)
{
FMessageLog log = FMessageLog("KACustomNameDropdown");
switch (Prov->SearchableNameValidateMessageType)
{
case EKACND_MessageLogType::Warning:
log
.Warning()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
case EKACND_MessageLogType::Error:
log
.Error()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
case EKACND_MessageLogType::Info:
log
.Info()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
default:
break;
}
if (Prov->bOpenMessageWindow)
{
log.Open();
}
};
auto MsgFuncBP = [&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov,UEdGraphPin* Pin)
{
FMessageLog log = FMessageLog("KACustomNameDropdown");
switch (Prov->SearchableNameValidateMessageType)
{
case EKACND_MessageLogType::Warning:
log
.Warning()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
case EKACND_MessageLogType::Error:
log
.Error()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
case EKACND_MessageLogType::Info:
log
.Info()
->AddToken(FUObjectToken::Create(TargetObj))
->AddToken(FTextToken::Create(MessageText));
break;
default:
break;
}
if (Prov->bOpenMessageWindow)
{
log.Open();
}
};
TArray<FName> SearchableNames;
//アセットからSearchableNameのリストを収集
if (UObject* Asset = InObject)
{
TArray<FName> CollectResult;
KACND::Util::CollectNamesFromAsset(Asset, SearchableNames, MsgFunc);
//Blueprintアセットなら、ノード引数にある値を収集
if (UBlueprint* Blueprint = Cast<UBlueprint>(Asset))
{
KACND::Util::CollectNamesFromBlueprint(Blueprint, SearchableNames, MsgFuncBP);
}
}
//SearchableNameを付与
for (const FName& Name : SearchableNames)
{
if (!Name.IsNone())
{
Ar.MarkSearchableName(DA, Name);
}
}
}
bool UKACND_SearchableNameInjector::IsEditorOnly() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
Injectorはアセットが保存される前に、そのアセットの中にドロップダウンによって設定された値が変数やBlueprintノードピンにあるかを検証し、設定されている場合はそのアセットを親としてアタッチし、保存プロセスを一緒にたどります。そしてSerializeがInjectorにも実行され、その段階でSearchableNameを付与します。
なので、アセット保存前のデリゲートにアクセスするためにモジュールクラスにデリゲートのバインドを追加してInjectorの付与処理を書きます。
プラグインのモジュールクラス(KACustomNameDropdown.h/cpp)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "KACND_GraphPanelPinFactory.h"
#include "Modules/ModuleManager.h"
#include "UObject/ObjectSaveContext.h"
/**
* プラグイン用モジュール
*
* ・名前ドロップダウン機能全体を束ねるモジュールクラス。
* ・起動時に GraphPinFactory の登録や、アセットロード/保存時のフックを仕込み、
* SearchableNameInjector の自動付与・クリーンアップを行う。
*/
class FKACustomNameDropdownModule : public IModuleInterface
{
public:
/**
* モジュール起動時の処理
*
* ・エディタ起動/ゲーム起動時に一度だけ呼ばれる。
* ・以下のような初期化処理を行う想定:
* - GraphPinFactory の生成および FEdGraphUtilities への登録
* - 保存前通知(OnObjectPreSave)へのバインド
*/
virtual void StartupModule() override;
/**
* モジュール終了時の処理
*
* ・エディタ終了時など、モジュールがアンロードされるタイミングで呼ばれる。
* ・StartupModule で登録した各種デリゲートやファクトリを解除する。
*/
virtual void ShutdownModule() override;
/**
* SearchableNameを付与できるようにアセットセーブ前にInjectorを付与する
*
* @param Obj 保存対象の UObject。
* @param PreSaveContext 保存コンテキスト(クック時かどうかなどの判定に使用)。
*/
void OnObjectPreSave(UObject* Obj, FObjectPreSaveContext PreSaveContext);
};
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACustomNameDropdown.h"
#include "KACND_NamePropertyCustomization.h"
#include "PropertyEditorModule.h"
#include "ISettingsModule.h"
#include "KACND_GraphPanelPinFactory.h"
#include "KACND_BlueprintCompilerExtension.h"
#include "KACND_InjectorComponent.h"
#include "BlueprintCompilationManager.h"
#define LOCTEXT_NAMESPACE "FKACustomNameDropdownModule"
void FKACustomNameDropdownModule::StartupModule()
{
// Injector操作用にAssetセーブ直前のデリゲートをBind
FCoreUObjectDelegates::OnObjectPreSave.AddRaw(this, &FKACustomNameDropdownModule::OnObjectPreSave);
//GraphPinFactoryを登録
{
GraphPinFactory = MakeShareable(new FKACND_GraphPanelPinFactory());
FEdGraphUtilities::RegisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//プロパティ編集画面でドロップダウン設定されたNameProperty/StringPropertyの見た目を変えるためのCustomizationを登録する
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout(
"NameProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.RegisterCustomPropertyTypeLayout(
"StrProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}
// Blueprintコンパイル時に独自の拡張ルールを登録
if(UBlueprint::StaticClass())
{
if (GetMutableDefault<UKACND_BlueprintCompilerExtension>())
{
FBlueprintCompilationManager::RegisterCompilerExtension(
UBlueprint::StaticClass(),
GetMutableDefault<UKACND_BlueprintCompilerExtension>()
);
}
}
}
void FKACustomNameDropdownModule::ShutdownModule()
{
//デリゲート登録解除
FCoreUObjectDelegates::OnObjectPreSave.RemoveAll(this);
//GraphPinFactoryを登録解除
if (GraphPinFactory.IsValid())
{
FEdGraphUtilities::UnregisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//NamePropertyのCustomizationの登録解除
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("NameProperty");
PropertyModule.UnregisterCustomPropertyTypeLayout("StrProperty");
PropertyModule.NotifyCustomizationModuleChanged();
}
}
void FKACustomNameDropdownModule::OnObjectPreSave(UObject* Obj, FObjectPreSaveContext PreSaveContext)
{
// アセットかどうかをチェック
if (!Obj || !Obj->IsAsset() || Obj->IsPackageExternal() || Obj->IsA<UKACND_SearchableNameInjector>())
{
return;
}
UPackage* Package = Cast<UPackage>(Obj->GetOutermost());
if (!Package)
{
return;
}
if (!PreSaveContext.IsCooking())
{
TArray<FName> SearchableNames;
TArray<TSharedRef<FTokenizedMessage>> Messages;
bool bShouldOpenLog = false;
UObject* TargetAsset = nullptr;
AActor* TargetActor = nullptr;
if (Obj->IsPackageExternal())
{
//ブログでは説明が長くなるので未対応
}
else
{
アセットの実態となるオブジェクトを取得
FString PackagePath = Package->GetPathName();
FString AssetPath;
AssetPath.Append(PackagePath);
AssetPath.Append(TEXT("."));
FString LeftS, RightS;
PackagePath.Split(TEXT("/"), &LeftS, &RightS, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
AssetPath.Append(RightS);
if (UObject* AssetObj = FSoftObjectPath(AssetPath).ResolveObject())
{
KACND::Util::CollectNamesFromAsset(Obj, SearchableNames,
[&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov)
{
return;
});
if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
{
KACND::Util::CollectNamesFromBlueprint(Blueprint, SearchableNames,
[&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov,UEdGraphPin* Pin)
{
return;
});
}
TargetAsset = AssetObj;
}
}
//SearchablenameがあるならInjectorをAssetに差し込む
if (TargetAsset && (SearchableNames.Num() > 0 || Messages.Num() > 0))
{
{
UKACND_SearchableNameInjector* Injector =
FindObject<UKACND_SearchableNameInjector>(Package, TEXT("KACNDSearchableNameInjector"));
if (!Injector)
{
Injector = NewObject<UKACND_SearchableNameInjector>(
Package,
TEXT("KACNDSearchableNameInjector"),
RF_Public | RF_Standalone
);
}
if (Injector)
{
Injector->TargetAsset = TargetAsset;
}
}
}
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FKACustomNameDropdownModule, KACustomNameDropdown)
Packageに対して保存可能なオブジェクトとしてぶら下げるという若干無理やり感な実装ではありますが、ひとまずこれで挙動を見てみます。
GameplayTagやDataTableと同じようにReference Viewerに参照を表示できました。
ついでに保存時のイベントを使用しているので、Validateを行えるようにし検証結果によってMessageLogウィンドウに任意のメッセージを表示できるようにもしました。
これならいちいちValidatorを書かなくても自動で値のチェックをしてくれるのでとても便利ですね。
ちなみに、SearchableNameで任意のプロパティがどこで使われているかをすべて網羅して確認するには少し工夫することでエディタ機能のみで一覧化できます。
①ドロップダウン用のデータアセットをReference Viewerで表示。
②左側にある参照を全選択
③右クリック→Re-Center Graphを押す
③ピン/変数の名前やその値を検索ボックスに入力
④使われている値一覧が表示される
さらに特定の参照を選択することで、その参照が使われているアセット一覧を表示することができます。
一覧化した状態で参照を全選択状態でコピーを行うと、アセットのパスをテキストとして一覧でコピーできるので、ドキュメントなどでリスト化する際も使えて便利かなと思います。
Step5.プロジェクトで導入しやすい形に
概ね入れたい機能は入れられたので、あとはプラグインとしての体裁を整えていくだけです。
DataAssetを任意のものを指定できるようにしたり、ValidationやSearchableNameの入れ込みをONOFFできるようにするためにConfigに公開するためのクラスを定義します。
また、Editorの起動時にDataAssetをロードしておく必要がありますが、その際EngineのInitが終わっていないとAssetのロードでLinkerErrorやWarningが出てしまうので、DataAssetのロードはFCoreDelegates::OnPostEngineInitのタイミングでロードします。
コンフィグ用クラス(KACND_Settings.h)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "KACND_DropdownData.h"
#include "KACND_Settings.generated.h"
/**
* KACustomNameDropdownプラグインの設定用Object
* ProjectSettings内で編集可能
*
* ・Editor 設定(Engine/Editor 設定ではなく、プロジェクトごとの Editor 設定)として保存される UObject。
* ・ドロップダウン全体の挙動を制御するフラグや、設定用 DataAsset の参照を保持する。
* ・`DefaultEditor.ini`のconfig に保存される。
*/
UCLASS(Blueprintable, config = Editor, defaultconfig)
class KACUSTOMNAMEDROPDOWN_API UKACND_Settings : public UObject
{
GENERATED_BODY()
public:
/**
* コンストラクタ
*
* ・DropdownDataPathにデフォルトアセットを設定
*/
UKACND_Settings();
/**
* 各種ドロップダウン設定を格納するデータアセット
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, config, Category = "DropdownSettings", NoClear, meta = (MetaClass = "/Script/KACustomNameDropdown.KACND_DropdownData"))
FSoftObjectPath DropdownDataPath;
/**
* BlueprintCompile時にカスタムなコンパイルを行うようにする
*
* ON にすると:
* ・ドロップダウンに存在しない値がピンや変数に設定されている場合、
* Warning または Error としてコンパイルメッセージを出す。
* ・`UKACND_DropdownProviderBase::OnValidateGraphPinValue` をオーバーライドすることで、
* プロジェクト固有の Validate ロジックも追加可能。
*
* OFF の場合:
* ・カスタム検証は行わず、標準のコンパイル挙動のみとなる。
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, config, Category = "DropdownSettings")
bool bEnableCustomPinValidation = true;
/**
* ドロップダウンが設定されたBlueprint保存時にSearchableNameを付与するようにする
*
* ON にすると:
* ・ドロップダウンの選択値を SearchableName としてパッケージに付与し、
* ReferenceViewer 上で「この要素がどこで使われているか」を可視化できるようになる。
*
* 注意:
* ・このフラグの変更はエディタの再起動が必要(モジュールの初期化タイミングで処理を仕込む想定)。
* ・On → Off に変更しても、既に保存済みの Blueprint については再セーブするまで
* SearchableName 情報が残り続ける。
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, config, Category = "DropdownSettings", meta = (ConfigRestartRequired = true))
bool bEnableInjectSearchableName = true;
/**
* WorldPartition の ExternalActor も SearchableName 注入の対象にするかどうか
*
* ・true の場合、ExternalActor の保存時にも Injector / InjectorComponent を用いて
* SearchableName を付与する。
* ・false の場合、従来どおりレベル Blueprint など非 ExternalActor のみを対象とする。
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, config, Category = "DropdownSettings", meta = (ConfigRestartRequired = true, EditCondition = "bEnableInjectSearchableName", EditConditionHides))
bool bSupportExternalActorsForSearchableName = true;
/**
* ExternalActor 用 InjectorComponent の自動破棄フラグ
*
* ・true の場合、Serialize 結果として有効なエントリが無くなった ExternalActor からは
* InjectorComponent を自動で取り除き、アクターをクリーンな状態に保つ。
* ・false の場合、一度付与された InjectorComponent は手動でクリーンアップするまで残る。
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, config, Category = "DropdownSettings", meta = (ConfigRestartRequired = true, EditCondition = "bSupportExternalActorsForSearchableName", EditConditionHides))
bool bAutoDestroyInjectorComponent = false;
/**
* データアセットをロードする
*
* @return DropdownDataPath で指定された UKACND_DropdownData。
*/
UFUNCTION(BlueprintCallable, Category = "DropdownSettings")
UKACND_DropdownData* LoadDropdownData();
/**
* ロード済のデータアセットを取得する
*
* @return DropdownDataPath で指定された UKACND_DropdownData。
*/
UFUNCTION(BlueprintCallable, Category = "DropdownSettings")
UKACND_DropdownData* GetLoadedDropdownData();
/**
* データアセットがロード済みかどうかをチェック
*
* @return DropdownDataPath が有効で、かつ既にロード済みであれば true。
*
* ・GetDropdownData() の前に呼び出すことで、「今ロード中かどうか」を確認したり、
* ロード済みのインスタンスを再利用するかどうかの判定用。
*/
UFUNCTION(BlueprintCallable, Category = "DropdownSettings")
bool IsLoadedDropdownData();
/**
* プロパティ変更時に、DropdownDataPathが変更された場合そのDataAssetをロードしWeakObjectPtrで保持しておく
*
* @param PropertyChangedEvent 変更のあったプロパティの情報を取得可能
*/
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
private:
/**
* ロードされたDataAssetの参照
* ※Serialize時にSoftObjectPathの参照解決ができない場合の回避用に参照を保持
*/
TWeakObjectPtr<UObject> BufferDataAsset;
};
冒頭で説明したように、コンフィグ用に定義したクラスをStartupModuleで登録します。
プラグインのモジュールクラス(KACustomNameDropdown.h/cpp)
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#pragma once
#include "KACND_GraphPanelPinFactory.h"
#include "Modules/ModuleManager.h"
#include "UObject/ObjectSaveContext.h"
/**
* プラグイン用モジュール
*
* ・名前ドロップダウン機能全体を束ねるモジュールクラス。
* ・起動時に GraphPinFactory の登録や、アセットロード/保存時のフックを仕込み、
* SearchableNameInjector の自動付与・クリーンアップを行う。
*/
class FKACustomNameDropdownModule : public IModuleInterface
{
public:
/**
* モジュール起動時の処理
*
* ・エディタ起動/ゲーム起動時に一度だけ呼ばれる。
* ・以下のような初期化処理を行う想定:
* - GraphPinFactory の生成および FEdGraphUtilities への登録
* - 保存前通知(OnObjectPreSave)へのバインド
* - エンジン初期化完了(PostEngineInit)へのバインド
*/
virtual void StartupModule() override;
/**
* モジュール終了時の処理
*
* ・エディタ終了時など、モジュールがアンロードされるタイミングで呼ばれる。
* ・StartupModule で登録した各種デリゲートやファクトリを解除する。
*/
virtual void ShutdownModule() override;
/**
* エンジン初期化完了コールバック
*
* ・KACND_DropdownDataデータをロードする
* ※Engine初期化後にロードしないと予期せぬensureを踏むため、ロードタイミングとしてPostEngineInitを使用
*/
void PostEngineInit();
/**
* SearchableNameを付与できるようにアセットセーブ前にInjectorを付与する
*
* @param Obj 保存対象の UObject。
* @param PreSaveContext 保存コンテキスト(クック時かどうかなどの判定に使用)。
*/
void OnObjectPreSave(UObject* Obj, FObjectPreSaveContext PreSaveContext);
};
/***************************************************
* Copyright 2018 - 2025 Kinnaji.All right reserved.
****************************************************/
#include "KACustomNameDropdown.h"
#include "KACND_NamePropertyCustomization.h"
#include "PropertyEditorModule.h"
#include "ISettingsModule.h"
#include "KACND_GraphPanelPinFactory.h"
#include "KACND_BlueprintCompilerExtension.h"
#include "KACND_InjectorComponent.h"
#include "BlueprintCompilationManager.h"
#define LOCTEXT_NAMESPACE "FKACustomNameDropdownModule"
void FKACustomNameDropdownModule::StartupModule()
{
//データアセットロード用にPostEngineInitをBind
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FKACustomNameDropdownModule::PostEngineInit);
// Injector操作用にAssetセーブ直前のデリゲートをBind
FCoreUObjectDelegates::OnObjectPreSave.AddRaw(this, &FKACustomNameDropdownModule::OnObjectPreSave);
//ProjectSettingsにプロパティを公開
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule != nullptr)
{
if (auto* Setting = GetMutableDefault<UKACND_Settings>())
{
SettingsModule->RegisterSettings(
"Project",
"Plugins",
"KACustomNameDropdown",
LOCTEXT("KACND_SettingsName", "KACustomNameDropdownSettings"),
LOCTEXT("KACND_SettingsNameDescription", "Make a customized function pulldown"),
Setting
);
}
}
//GraphPinFactoryを登録
{
GraphPinFactory = MakeShareable(new FKACND_GraphPanelPinFactory());
FEdGraphUtilities::RegisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//プロパティ編集画面でドロップダウン設定されたNameProperty/StringPropertyの見た目を変えるためのCustomizationを登録する
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout(
"NameProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.RegisterCustomPropertyTypeLayout(
"StrProperty",
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FKACND_NamePropertyCustomization::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}
// Blueprintコンパイル時に独自の拡張ルールを登録
if(UBlueprint::StaticClass())
{
if (GetMutableDefault<UKACND_BlueprintCompilerExtension>())
{
FBlueprintCompilationManager::RegisterCompilerExtension(
UBlueprint::StaticClass(),
GetMutableDefault<UKACND_BlueprintCompilerExtension>()
);
}
}
}
void FKACustomNameDropdownModule::ShutdownModule()
{
//デリゲート登録解除
FCoreUObjectDelegates::OnObjectPreSave.RemoveAll(this);
//ProjectSettings公開の登録解除
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule != nullptr)
{
SettingsModule->UnregisterSettings(
"Project",
"Plugins",
"KACustomNameDropdown"
);
}
//GraphPinFactoryを登録解除
if (GraphPinFactory.IsValid())
{
FEdGraphUtilities::UnregisterVisualPinFactory(GraphPinFactory.ToSharedRef());
}
//NamePropertyのCustomizationの登録解除
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("NameProperty");
PropertyModule.UnregisterCustomPropertyTypeLayout("StrProperty");
PropertyModule.NotifyCustomizationModuleChanged();
}
}
void FKACustomNameDropdownModule::PostEngineInit()
{
if (auto* Setting = GetMutableDefault<UKACND_Settings>())
{
//コンパイル時にデータアセットがロードされると新たなコンパイルを生むため、あらかじめロードしておく
// データアセットのロードタイミングはEngineのInit時あたりが安全なので、ここでロードする
Setting->LoadDropdownData();
}
}
void FKACustomNameDropdownModule::OnObjectPreSave(UObject* Obj, FObjectPreSaveContext PreSaveContext)
{
// アセットかどうかをチェック
if (!Obj || !Obj->IsAsset() || Obj->IsPackageExternal() || Obj->IsA<UKACND_SearchableNameInjector>())
{
return;
}
UPackage* Package = Cast<UPackage>(Obj->GetOutermost());
if (!Package)
{
return;
}
if (!PreSaveContext.IsCooking())
{
TArray<FName> SearchableNames;
TArray<TSharedRef<FTokenizedMessage>> Messages;
bool bShouldOpenLog = false;
UObject* TargetAsset = nullptr;
AActor* TargetActor = nullptr;
if (Obj->IsPackageExternal())
{
//ブログでは説明が長くなるので未対応
}
else
{
アセットの実態となるオブジェクトを取得
FString PackagePath = Package->GetPathName();
FString AssetPath;
AssetPath.Append(PackagePath);
AssetPath.Append(TEXT("."));
FString LeftS, RightS;
PackagePath.Split(TEXT("/"), &LeftS, &RightS, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
AssetPath.Append(RightS);
if (UObject* AssetObj = FSoftObjectPath(AssetPath).ResolveObject())
{
KACND::Util::CollectNamesFromAsset(Obj, SearchableNames,
[&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov)
{
return;
});
if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
{
KACND::Util::CollectNamesFromBlueprint(Blueprint, SearchableNames,
[&](UObject* TargetObj, FText& MessageText, UKACND_DropdownProviderBase* Prov,UEdGraphPin* Pin)
{
return;
});
}
TargetAsset = AssetObj;
}
}
//SearchablenameがあるならInjectorをAssetに差し込む
if (TargetAsset && (SearchableNames.Num() > 0 || Messages.Num() > 0))
{
{
UKACND_SearchableNameInjector* Injector =
FindObject<UKACND_SearchableNameInjector>(Package, TEXT("KACNDSearchableNameInjector"));
if (!Injector)
{
Injector = NewObject<UKACND_SearchableNameInjector>(
Package,
TEXT("KACNDSearchableNameInjector"),
RF_Public | RF_Standalone
);
}
if (Injector)
{
Injector->TargetAsset = TargetAsset;
}
}
}
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FKACustomNameDropdownModule, KACustomNameDropdown)
これで、ProjectSettingsにプラグインの設定を公開できました。
Step6.使ってみる
これで大体サクッと入れられそうな機能は入れられたんじゃないかなと思いますので、ここでプラグインの運用についてサンプルケースを紹介してみたいと思います。
① ゲーム進行で使うフラグの管理
ストーリーのあるゲームでは、ストーリーの進行状況に応じた状態を記録するために「フラグ」と呼ばれる値の束を扱うことがよくあります。
フラグは実装的には型(boolやint等)や初期値、その他使う側がわかりやすいように説明文等を設定できるようにしたり、手軽に追加・編集を行えるようにDataTableで実装されることがよくあるのかなと思います。
また、Scenario用やGimmick用等複数に分けて管理することもあるんじゃないかなと思います。
そんな場合においては今回のプラグインが役に立つのではないかなと思います。
UKACND_DropdownProviderBaseを継承し、DataTableからドロップダウンの値やメタ情報の要素をGetNamelist関数をオーバーライドして作成します。 ターゲットのPin名と変数名はそれぞれFlagIDとしています
これで、BPやC++で変数や関数ピンでFlagIDと名前をつけるだけで自動的にドロップダウンが作成され、設定された値はメタ情報とともに表示されます。また、保存時にSearchableNameが付与されるのでどこで使用されているかがわかるようになります。
② ActorのTags
アクタークラスには、汎用的にメタデータを仕込む場所としてTagsというプロパティが設けられています。このTagsはNameの配列なので、このプラグインの機能でドロップダウン化することができます。
流石に全アクター共通でドロップダウンを作成するのは運用しづらいので、特定のクラスのアクターのみにフィルタします。
これをDataAssetに登録することで、BP_SampleActorのTagsプロパティをドロップダウン化できました。
こんな感じで、いろんな使い方ができるかなと思います。
まとめ
長い記事になってしまいましたが、概ね書きたいことはかけたかなと思います。
ブログでは書ききれていない機能等も含め、最終的なプラグインの構成は以下のようになっています。
このブログで紹介したプラグインの完成品は、GitHub にて公開しておりますので中身が気になる方はご自由にダウンロード・改変等してください。
※一応試験運用しているプロジェクトもありますが、ブログ用にサクッと作ったものなので問題等あれば教えてもらえると助かります。マッテルヨ>🐟️
このプラグインのいいところは、既存の実装を全く変更することなく今プロジェクト使っているプロパティをドロップダウン化できちゃいます。
似た実装で、GameplayTagやDataTableRowHandleのような構造体を使って独自のカスタマイズを実装するような方法もありますが、独自に作るとなるとどうしても構造体依存で実装されていって毎回ひと手間加わってしまったりしますが、今回の設計であればそのようなクセはまったくなくUnreal Engine標準の文字機構をそのまま使用できます。
また、エディタオンリーなものしかないので、パッケージに影響を与えないのも良い点かなと思います。
ただ、多少無理やりな部分があったり、エンジン内部の都合でしかたない実装があるので、このあたりは今後Unreal Engineのアップデートで改善できるようになってたらいいな…と思ってます。
最後に
Unreal Engineは日々進化をし続けており、未来ではこんなつまらないことにいちいち悩むようなことはなくなっているかもしれません。
とはいえ、人間である以上何かしらミスをしたりしてしまうことはほぼ確実にありますので、なるべくは快適な開発ができるに越したことはないかなと思います。
開発者が安心して開発ができるような気配りは、心にゆとりを持てるようにもなるのでより良いUnreal Engineライフを送るためにも様々な配慮ができると良いかなと個人的には思っております。
些末な記事で恐縮ですが、みなさんの開発で少しでも役に立ちましたら嬉しい限りです。
明日は@UnPySide さんの「World PartitionでLevelInstance+DataLayerを使ってみた 」です。ワクワクですね!
毎度些末な記事で恐縮ですが、以上となります。
※この記事で使用しているUnrealのVersionは5.6.1です。
※この記事のサンプルプロジェクトはありません。 代わりに、作成したプラグインはそのままGitHubにて公開しております。