※↓はUE4向けです。UE5版書きました
久しぶりの投稿です。リモートワーク環境が整うまで暇が出来たのでエディタ環境で使える小ネタを一つ
文字列からUClassの取得したい
エディタ環境で自動配置や、定期的な処理を走らせる時に文字列からUClassが欲しい場合があります。
例えば、外部からインポートしてきたCSVにクラス名と位置情報をはじめとした情報があり、それを基にエディタワールド上に自動配置したい時などです。
UE4とリフレクション
UE4ではリフレクションが採用されているので、文字列からクラスを取ってきたり関数を呼び出したりプロパティにアクセスしたりといった事がRTTiを使わずに実現できます。これがBPとcppを繋ぐキモの部分であったりします。
※ゲームにおけるリフレクションについて、UE抜きでの説明は大前氏のMagicReflectionのスライドとコードを見るのがわかりやすいと思います。
スライド
GitHub
実装
今回はFStringを渡すとUClassを返す関数をBP関数ライブラリの一つとして作ります。これさえ出来れば後はどうにでもなるので、例に示したようなテーブルを見ながら配置するような実戦的な実装は端折ります。
この実装を考える時に、FindObjectするだけで済むのではと思ったりしてしまいますが、それがうまくいくのは、例えばコンテンツブラウザ上からダブルクリックで開くなどして、エディタ起動後のどこかですでにパッケージが読まれているクラスです。
実際は以下の事を考慮しなければ、ほとんどのケースで意図した動作にはなりません。
- ネイティブではない、BPクラスをCpp側で扱う場合、_Cがクラス名の末尾についている事を考慮しなければならない。
- リネーム後Fixupされていないリダイレクタに絡んだクラスの場合
- ロードされていない(起動後エディタで開かれていない)場合
それらを考慮したのが以下の関数です。
※3/28 リファクタリング&高速化、関数名はfromよりbyだと気づくも画像撮り直しがメンドイのでそのままにしてます。
UClass* UMyBlueprintFunctionLibrary::GetClassFromName(const FString& name) {
UClass* ret = nullptr;
#if UE_EDITOR
auto findClass = [](const FString& name) {
UClass* result = nullptr;
result = FindObject<UClass>(ANY_PACKAGE, *name);
if (nullptr == result) {
// 見つからなかった場合、BPクラスの可能性があるので_Cを付けて再検索.
FString bpname = name;
bpname.Append(TEXT("_C"));
result = FindObject<UClass>(ANY_PACKAGE, *bpname);
if (nullptr == result) {
// それでも見つからない場合、リダイレクタの可能性を探る.
UObjectRedirector* objRedirector = FindObject<UObjectRedirector>(ANY_PACKAGE, *name);
if (nullptr != objRedirector && nullptr != objRedirector->DestinationObject) {
result = Cast<UClass>(objRedirector->DestinationObject);
}
else {
objRedirector = FindObject<UObjectRedirector>(ANY_PACKAGE, *bpname);
if (nullptr != objRedirector && nullptr != objRedirector->DestinationObject) {
result = Cast<UClass>(objRedirector->DestinationObject);
}
}
}
}
return result;
};
// まずはロード済みの物から検索.
ret = findClass(name);
if (nullptr == ret) {
// 最後に読み込まれていない可能性があるので、ロードを試みる.
UPackage* package = LoadPackage(nullptr, *name, LOAD_Verify | LOAD_NoWarn);
if (nullptr != package) {
package->FullyLoad();
// もう一回検索.
ret = findClass(name);
}
}
#endif
return ret;
}
動作テスト
テストのために以下のような、クラス名の文字列、位置、ラベルを指定して配置する簡易的なウィジットを作りました。
ウィジットの実装は、作成した”GetClassFromName”関数からクラスを取得し尚且つクラスがアクターだった場合スポーンするようにしています。
スポーン関数はC++で書いていますが、テストのため適当です。本格的にやるならUWorld::SpawnActorではなく、UWorld::SpawnActorDeferredを用いて細かいプロパティの設定をするのがよいでしょう。
AActor* UMyBlueprintFunctionLibrary::SpawnActor(UObject* worldObj, UClass* actorClass, const FVector& location, const FName& label) {
AActor* ret = nullptr;
ret = worldObj->GetWorld()->SpawnActor<AActor>(actorClass, location, FRotator());
ret->SetActorLabel(label.ToString());
return ret;
}
試しに、StaticMeshActorを配置してみました。
クラス名の文字列から、指定したクラス、位置、ラベルで正しくスポーンされました。
次にエディタ起動後一度もロードされていないBPクラスの配置を試しました。
こちらも指定通りに配置することが出来ました。
最後に
今回は4.24.3で動くコードで書きました。4.25からPreviewが外れた頃に改めて確認しようと思います。
昔実装した時はもう少しシンプルに書けた気もするので、その辺りの突っ込みあればお待ちしております。