※この記事で使用しているUnrealのVersionは5.3.2です。

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト

※この記事はUnreal Engine (UE) Advent Calendar 2023の17日目の記事になります。
昨日は@suzuki_takashiさんの「LearningAgentで華麗に崖を飛び越えさせてみたかった」でした!機械学習でAIグレイマンがぴょんぴょん跳ねて、崖を越えられるようになるのは色々面白かったです!

前置き

どうも、お魚のキンアジと申します! サカナダヨッ>🐟

今回はUnrealEngine内で使用できる「PythonEditorScriptPlugin」を使った「UnrealPython」について、UnrealEngine側と連携する上で気をつけておきたいルール、覚えておくと便利な機能、EditorUtility系のBlueprintとの比較等について書いていきます。

UnrealPythonについての公式ドキュメントはこちらを御覧ください。
Python を使用したエディタのスクリプティング

※尚、初期のセットアップや起動方法等の説明は省かせていただきます。また、通常のPythonの記述や基礎知識についても省きます(記事が長くなってしまったので…)
このあたりは、以前書いた以下の記事にかるくまとめてありますので合わせて参考にしていただければと思います(UE4のものですが、現在でもほとんど変わらず使用できるかなと思います)

早速ですが、まずはUnrealPythonでUnrealエディタと連携する上で覚えておくと良い基本的な事柄をいくつかご紹介していきます!

Unrealエディタと連携可能なクラス・関数・変数の定義

UnrealPythonではunrealという名のモジュールをインポートすることができ、UnrealEngine内の様々の機能にアクセスすることができます。

#unrealモジュールをインポート
import unreal

基本的に、通常のPythonで使用できることはUnrealPythonでも実装することは可能で、Pythonのプリミティブな型を扱うこともできますが、UnrealPythonが提供している型を使うことを推奨しています。

また、クラス・関数・変数を定義する際、UnrealPythonが提供するデコレータ(クラス、関数に任意の処理を追加できる機能)やメソッド(クラスのメンバ関数)を使用することで、Blueprintと同じような扱いのインスタンスを作成することができ、エディタ側との連携がより行いやすくなります。

そこで、まずはエディタと連携可能なクラス・関数・変数について見ていきたいと思います。

~クラス~ @unreal.uclass

UnrealPythonでクラスを定義する際に、その定義の前の行に@unreal.uclassというデコレータを記載することで、Blueprintクラスと似たような扱いのクラスを定義することができます。
(C++で使用できるUCLASSマクロとほぼ同義です)

その際、定義するクラスの親クラスはunrealモジュールが提供するクラスタイプを指定する必要があり、継承を行わないとエラーが出てしまいます。

標準でunrealモジュールが提供しているクラスタイプはUnreal Python API Documentationに記載されています。

@unreal.uclassを使ったクラス定義

#クラス定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):
	pass

@unreal.uclassで定義しておくと、EditorUtility系のBlueprintにあるExecutePythonScript関数で実行した際に生成したインスタンスをそのままわたすことができます。

・Pythonで作成したオブジェクトをBlueprintにわたす例

#クラス定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):
	pass

#クラスインスタンスを生成
result_obj = TestClass()
#PrintString出力(Blueprint側)
#LogBlueprintUserMessages: [None] TestClass_0

これを応用すれば、UnrealPythonとEditorUtility系のBlueprintとの連携がやりやすくなり、別のPython同士の連携等も行うことができます。

尚、TestClass()のように引数なしだと、Object名は自動生成され、ObjectのOuter(親みたいなもの)は/Engine/Transientとなってしまいますが、Outerや名前は引数で追加可能です。また、unreal.new_objectメソッドで生成することでも、これらを任意のものにできます。

unreal.new_objectでのインスタンス生成

#クラス定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):
	pass

#load.new_objectでインスタンスを生成
result_obj = unreal.new_object(TestClass,unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem),"Object_Unique")
#Object名を出力
unreal.log(unreal.SystemLibrary.get_display_name(result_obj))
#LogPython: Object_Unique

~関数~ @unreal.ufunction

クラスと同じように、UnrealPythonで関数を定義する際に、その定義の前の行に@unreal.ufunctionというデコレータを記載することで、Blueprintのノードと同じ扱いの関数を定義することができます。

ただ、基本的にはメソッドとして実装しないとあまり意味はありません。また、メソッドとして定義する場合は@unreal.uclassで定義したクラス内で定義しないとエラーがでます。

@unreal.ufunctionを使ったメソッドの定義

#関数定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):

	#メソッドの定義
	@unreal.ufunction()
	def test_method(self):
		unreal.log("テストメソッド実行")
		
#クラスインスタンスを生成
result_obj = TestClass()
#メソッドを呼び出し
result_obj.test_method()
#LogPython: テストメソッド実行

おおよそシンプルな実装において@unreal.ufunctionをつけたとしてもメリットを感じることは少ないですが、これを用いると「デリゲートのバインド」が行えるようになります。

例としては、アセットのインポート後に実行されるunreal.ImportSubsystem.on_asset_post_importのデリゲートを受け取るような実装をしてみます。

・アセットのインポート後のデリゲート受け取りメソッドを実装

#インポートの受け取りテスト
import unreal
#クラスの定義
@unreal.uclass()
class TestClass(unreal.Object):
	
	#インポート受け取り用のメソッド
	@unreal.ufunction(ret=None, params=[unreal.Factory,unreal.Object])
	def post_import(self,factory,import_asset):
		unreal.log("インポート完了")
		
#インポート後のデリゲートをバインド
test_obj = TestClass()
unreal.get_editor_subsystem(unreal.ImportSubsystem).on_asset_post_import.add_function_unique(test_obj,"post_import")

・実行動画

また、@unreal.ufunctionで定義をしておけばインスタンスからcall_methodを通じてメソッド名で実行する事ができます。

・メソッド名で実行

#名前でメソッド実行テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):

	#メソッドの定義
	@unreal.ufunction()
	def test_method(self):
		unreal.log("テストメソッド実行")
		
#クラスインスタンスを生成
result_obj = TestClass()
#メソッドを呼び出し
result_obj.call_method("test_method")
#LogPython: テストメソッド実行

@unreal.functionで引数や返り値を定義する場合、引数はリストで型を代入し、返り値は単体の型を代入することで機能するようになります。

そして、call_methodで呼び出す際はパラメータをタプル型で追加することで引数を入れられ、call_methodの返り値がそのままメソッドの返り値となります。

・メソッドに引数と返り値を定義した例

#名前でメソッド実行テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):

	#メソッドの定義
	@unreal.ufunction(params=[str,int],ret=str)
	def test_method(self,in_str,in_int):
		return in_str + str(in_int)
		
#クラスインスタンスを生成
result_obj = TestClass()
#メソッドを呼び出し
unreal.log(result_obj.call_method("test_method",args=("テスト実行",1)))
#LogPython: テストメソッド実行1

このcall_methodを用いる例として、Blueprintで作成したクラスのイベントや関数に対して呼び出しを行う場合に有効です。
→Blueprintで定義したクラスを使用する場合、Blueprintのクラスとして生成することが難しく継承している親のクラスとして抽象化されてしまうために、名前から呼び出すテクニックが必要となります。

・BPで定義したクラスのイベントを呼び出す例

#名前でメソッド実行テスト
import unreal
#BPのアセットからクラスを取得("BPアセットのパス"_"BPアセットの名前"_Cで指定できる)
cls = unreal.load_class(None,"/Game/PyToBP/EUB_Test.EUB_Test_C")
#インスタンスを生成
result_obj = unreal.new_object(cls)
#TestEventを呼び出す(result_obj.TestEvent()では実行できないので、call_methodを使う)
result_obj.call_method("TestEvent")
#LogBlueprintUserMessages: [None] テストイベント実行

・実行動画

補足ですが、@unreal.ufunctionでは、通常だとC++でしか定義のできないメタデータ指定子を使うことができます。メタデータ指定子は、関数に対して様々な補助的な機能を施すことができます。
(関数の方はUnrealPython上だけでは有用なメタが少ないので省略します)

それと、@unreal.ufunctionを少し応用すればPythonで定義したメソッドをBlueprintノードとして公開することができます(最後におまけとして紹介します)

~変数~ unreal.uproperty

変数は、unreal.upropertyメソッドで生成した値を代入することで、Blueprintのプロパティと同じ扱いの変数を作成できます。関数同様に、@unreal.uclassで定義したクラスのアトリビュート(クラスのメンバ変数)として定義することで色々機能を使えるようになります。

・変数を定義

#変数定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):
	
	#アトリビュートの定義
	property_test = unreal.uproperty(str)
	
#クラスインスタンスを生成
result_obj = TestClass()
#アトリビュートの値をセット
result_obj.property_test = "テストのプロパティ"
#アトリビュートの値を出力"テストのプロパティ"
unreal.log(result_obj.property_test )
#LogPython: テストのプロパティ

尚、unreal.upropertyで定義した変数であれば、関数に対して行ったようにインスタンスに対してget_editor_propertyset_editor_propertyメソッドにて値にアクセスが可能です。

set_editor_propertyget_editor_propertyの使用例

#変数定義テスト
import unreal
#テストクラスの定義
@unreal.uclass()
class TestClass(unreal.Object):

	#アトリビュートの定義
	property_test = unreal.uproperty(str)
	
#クラスインスタンスを生成
result_obj = TestClass()
#アトリビュートの値をセット
result_obj.set_editor_property("property_test","テストのプロパティ")
#アトリビュートの値を出力
unreal.log(result_obj.get_editor_property("property_test"))
#LogPython: テストのプロパティ

@unreal.ufunctionと同じくBlueprintで定義したクラスのプロパティにアクセスするには上記のテクニックが有効です。
ただし、Blueprint側のプロパティの設定でInstanceEditableになっていないとset_editor_propertyメソッドは使えないのでご注意ください。

・Blueprintクラスのプロパティへのアクセス例

#名前でプロパティへアクセステスト
import unreal
#BPのアセットからクラスを取得("BPアセットのパス"_"BPアセットの名前"_Cで指定できる)
cls = unreal.load_class(None,"/Game/PyToBP/EUB_Test.EUB_Test_C")
#インスタンスを生成
result_obj = unreal.new_object(cls)
#TestPropertyの値をセット
result_obj.set_editor_property("TestProperty","テストのプロパティです")
#TestPropertyの値を出力
unreal.log(result_obj.get_editor_property("TestProperty"))
#LogPython: テストのプロパティです

・実行動画

また、@unreal.ufunctionと同じくメタデータ指定子を使うことができ、エディタ側との連携にも使えるため、後ほどご紹介します。

その他@unreal.uenum,@unreal.ustruct,unreal.uvalue等のUnrealEngine側のプリミティブな型もありますが、今回は紹介を省かせていただきます。

Blueprintの関数やプロパティをUnrealPythonで使用する際のルールについて

次は、UnrealPythonからBlueprintの各種機能にアクセスする上でのルールをご紹介します。

Unreal側の関数を呼び出す際のルール

UnrealEngineのC++で実装されているBlueprintに公開されている関数は、基本的にUnrealPythonではunrealモジュールから呼び出しが可能です。

Blueprint関数をUnrealPythonで使用する場合は、定義されている関数の名前を少し変換させることで使用ができます。

例えば、選択中のアセットを取得するEditorUtilityLibrary::GetSelectedAssetという関数があります。

これをPythonで使う場合、get_selected_assetというメソッドを使用します。

メソッド名は、Blueprintに表示されている名前をすべての文字を小文字にし、単語の間に”_”をつけるようなルールとなっています(小文字のスネークケースになります)

get_selected_assetの使用例

#記述例(#/Game/EUW_PythonTestを選択した状態で実行)
import unreal

for asset in unreal.EditorUtilityLibrary.get_selected_assets():
    unreal.log(asset)
#LogPython: <Object '/Game/EUW_PythonTest.EUW_PythonTest' (0x000005CC7E7E6200) Class 'EditorUtilityWidgetBlueprint'>

UnrealEngine内から公開されているメソッドに関しては、Unreal Python API DocumentationにAPIが記載されているので、わざわざBlueprintやC++から辿る必要はあまりないですが、Blueprint側の知識がある場合や自作の関数を公開する際などでもこのルールで自動的にUnrealPythonで使用できるので覚えておくとよいでしょう。

ちなみに、Blueprint関数のメソッドはPython上で明示的に引数を指定する場合、その引数名も小文字のスネークケースにする必要があります。また、UnrealEngineのコーディングルールにあるboolプロパティの頭につける「b」は取り除く必要があります

log_stringの使用例

#記述例
import unreal

unreal.SystemLibrary.log_string(string='Hello', print_to_log=True)
#LogBlueprintUserMessages: Hello

#C++上の定義のbPrintToLogは「b」が外されている
#UFUNCTION(BlueprintCallable, Category="Utilities|String", meta=(BlueprintThreadSafe, Keywords = "log print", DevelopmentOnly))
#static ENGINE_API void LogString(const FString& InString = FString(TEXT("Hello")), bool bPrintToLog = true);

Unreal側のEnumの記述

UnrealEngine内のEnum(列挙型)の場合は逆に、すべての文字を大文字にし、単語の間に”_”をつけるようなルールとなっています(大文字のスネークケースになります)

・Widgetの表示状態などで使われるESlateVisibilityの場合

#記述例
import unreal

unreal.log(unreal.SlateVisibility.VISIBLE)
#LogPython: <SlateVisibility.VISIBLE: 0>

Unreal側と一致しない名前について

一部の関数は、Blueprint側の名前やクラス名・関数名とUnrealPython側のクラス名・関数名が一致しないものもあります。

例えば、オブジェクトの内部的なPathを取得するKismetSystemLibrary::GetObjectPathStringという関数があります。

見たままであれば、この関数をPythonで使う場合は

unreal.KismetSystemLibrary.get_object_path_string(object)

となるはずですが、これではエラーが出てしまいます。

この関数をPython上で使う場合は、

unreal.SystemLibrary.get_path_name(object)

と記述する必要があります。

get_selected_assetsの使用例

#記述例(#/Game/EUW_PythonTestを選択した状態で実行)
import unreal

for asset in unreal.EditorUtilityLibrary.get_selected_assets():
	unreal.log(unreal.SystemLibrary.get_path_name(asset))
#LogPython: /Game/EUW_PythonTest.EUW_PythonTest

クラス名も関数名もルールと一致しないですが、理由としては以下となります。

1.クラス名に関して
UKismetSystemLibraryのC++定義のクラス指定子(UCLASS)に
meta = (ScriptName = "SystemLibrary")
と付いている。
メタデータ指定子ScriptNameはUnrealPython上で使用する際の名前を上書きする機能があります。

2.関数名に関して
関数指定子(UFUNCTION)に
meta = (DisplayName = "Get Object Path String")
と付いている。
メタデータ指定子DisplayNameはBlueprint上で表示する名前を上書きする機能があります。

これらの要素によってBlueprint上で扱っている関数をUnrealPython上で扱う場合記述する名前が変わってきます。

Enumに関しても、前述の2にあるDisplayNameがUnrealEngine内のコードには至る所に使用されております。

・メッシュ等のコリジョンのチャンネルを定義しているECollisionChannel

ECollisionChannel::WorldStaticは表示上はWorldStaticとなっていますが、内部的な定義は

ECC_WorldStatic UMETA(DisplayName="WorldStatic")

となっているため、使用する場合は

unreal.CollisionChannel.ECC_WORLD_STATIC

となります。

ECollisionChannelを扱う例

#記述例
import unreal

unreal.log(unreal.CollisionChannel.ECC_WORLD_STATIC)
#LogPython: <CollisionChannel.ECC_WORLD_STATIC: 0>

公式ドキュメントにも上記の記載ルールは書かれていますので、参考にすると良いでしょう。このあたりは、知っておくとUnrealPythonがより使えるようになるので、是非抑えておきましょう。

UnrealPython VS EditorUtility

UnrealPythonではできない・不得意とすること

UnrealPythonでは、Blueprintに公開されている関数やプロパティは”ほぼ”すべて使用することができます。基本的に、C++で定義された関数にUFUNCTION(BlueprintCallable)マクロがついているものが対象になります。

つまり、EditorUtility系のBlueprintでできることはおおよそUnrealPythonでも実行できてしまいます

「であれば、Pythonのほうが外部ライブラリも使えるし有用?」

と思うかもしれませんが、そんなことはありません。UnrealPythonでもできないことや苦手なことはあります。

例えば、UnrealPythonから呼び出せない、または呼び出すのに困難なBlueprintのノードとして、以下のような特殊なノードがあります。

1.K2_Nodeで作られたノード

K2_Nodeとは、Blueprintで特殊なノード(例えばピンの可変がノードから行えたり、Execピンを複数持てるようなノード)です。
通常はC++で関数を定義するときに、UFUNCTION(BlueprintCallable)マクロをつけることで自動的にPythonに公開されます。しかし、K2_Nodeをベースに作られたノードは公開方法が特殊であり、Python上でアクセスするすべを持ちません。

2.マクロノード

マクロはUnrealEngine標準のForEachLoopDoonce等の汎用的なノードがあり、アセット作成で「Blueprint→BlueprintMacroLibrary」から独自にマクロを作成することもできます。
マクロノードは様々な場所で使われていますが、Python上では仕組み上扱うことが難しいためこのマクロへのアクセスはできません。

3.非同期ノード(Asyncノード)

非同期ノードは、ノードの右上に時計マークのあるノードで、フレームを跨いだ処理を行うことができます。これはK2_Nodeと似ていてUFUNCTION(BlueprintCallable)で公開されたノードではないので通常の方法ではUnrealPythonからアクセスすることはできません。

ただし、非同期ノードに関してはUnrealPythonからでも再現することは可能です。

AsyncEditorDelayをPythonで実行し、Completeを受け取る

#AsyncEditorDelayをPythonで実行する
import unreal

#Delayが終わったときのDelegateを受け取るオブジェクトクラス
@unreal.uclass()
class DelayCallbackObj(unreal.EditorUtilityObject):

	#Delay終了時のDelegate受け取り関数
	@unreal.ufunction(ret=None)
	def complete_task(self):
		print("DelayComplete")

#Delayのパラメータ
second = 2.0
min_frame = 30

#Callbackを受け取るためのObjectを生成
callback_obj = DelayCallbackObj()

#AsyncEditorDelayを実行
delay_task = unreal.AsyncEditorDelay().call_method("AsyncEditorDelay",args = (second,min_frame))
delay_task.complete .add_function_unique(callback_obj,"complete_task")

・実行動画(実行してから2秒後に”DelayComplete”という文字を出す)

注意点としては、

1.
UAsyncEditorDelay::AsyncEditorDelayを実行するには、call_methodで実行します。
→これは、AsyncEditorDelayなどのAsync系のスタートの呼び出し口には、C++の定義で
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
とついており、BlueprintやUnrealPython上で直接呼び出しができないため、call_methodを使用してAsync関数を実行します。

2.
call_methodで生成されたAsync系のオブジェクトのCompleteデリゲートに対してCallback用のオブジェクトのメソッドにバインドしています。
→「スタート→バインド」としているため、AsyncEditorDelayの場合はバインドが遅れることはないが、物によっては問題が出るケースもあります。

3.
バインド先が単にPythonで定義したメソッドの場合、add_callable_uniqueでも実装は可能ですが、その場合UnrealEngineのガーベージコレクションが走るとバインド先のメソッドがなくなってしまう(揮発的なPyObjectが消えてしまう)ので、必ずバインド先は@unreal.uclassなオブジェクト内のメソッドが良いかと思います。

このあたりを注意しつつ、いくらかコードを書かなければならないぐらいなら、Blueprintで素直に1ノード記載するほうが得策です。

これら以外も、UnrealPythonはあくまでコードでのみ作成できるため、EditorUtilityWidgetのような見た目を自由にカスタマイズできるような拡張もあまり得意ではありません。

UnrealPythonを使う上で上記の事柄を理解しておくと、EditorUtilityとUnrealPythonの使い分けがしやすくなると思いますので、是非覚えておきましょう。

UnrealPythonでしかできない・得意とすること

さて、UnrealPythonのネガティブな部分を説明してきましたが、UnrealPythonにはそれ以上に有用な場面がたくさんあります。

1.豊富なライブラリ

やはり一番は「豊富なライブラリを簡単に使用することができる」ことかと思います。Pythonは「文字列操作」「画像解析」「自動化」「ディープラーニング」等の開発を効率化する上で様々なライブラリが使いたい放題なので使わない手はないかと思います。

ただ一つ注意点として、外部のライブラリをpipでインストールする場合、UnrealPythonにおける実行環境のベースとなるパス(python.exeのあるパス)は、エンジン側のフォルダにあり、WindowsでUE5.3のデフォルトインストール先の場合は、
C:\Program Files\Epic Games\UE_5.3\Engine\Binaries\ThirdParty\Python3\Win64\
となります。

この場合、例えばnumpyモジュールをインストールするならば、Windowsのコンソールウィンドウで

cd "C:\Program Files\Epic Games\UE_5.3\Engine\Binaries\ThirdParty\Python3\Win64"
python -m pip install numpy

というコマンドでインストールできますが、これだとEngineフォルダにインストールされてしまうのであまりよろしくはありません。

pipでインストールする場合は、実行するプロジェクトのコンテンツかプラグイン内にPythonというフォルダを作成し、そこへインストールするのが良いかと思います。インストール先を指定するには、-tオプションを使用します。

rem "D:\Kinnaji\KAPythonUtility\Content\Python"はインストール先のパス
python -m pip install numpy -t D:\Kinnaji\KAPythonUtility\Content\Python

・インストール動画

こうすることで、プロジェクト内のコンテンツフォルダ内にインストールしたPythonモジュールが入るため、開発側としてもコンテンツさえ共有されていれば同じPythonモジュールを使用することもできます。プロジェクトやプラグイン以下のContent/Pythonパスは、自動的にUnrealPython側でシステムパスとして含まれるため、使用するたびにsys.path.append等でパスを追加する必要が無いこともメリットかなと思います。

ライブラリのインストールについて話したので、一応外部ライブラリを使った例を簡単にですが紹介しておきます。

例えば、

「日本語や特殊文字のアセットをローマ字に変えたいなぁ…
(ビルドとかバージョン管理等色んなところでエラーが出て困るから…)

みたいな場合があったとします(ノンフィクション)

Pythonには日本語をローマ字に変換することができる「pykakasi」という外部ライブラリがあり、特殊文字を通常の文字に変換(Unicodeを正規化)する「unicodedata」という内部ライブラリとあわせて使うことで簡単に一括でアセットの日本語文字をローマ字に変換することができます。

尚、アセットのリネームに関しては、今月CGWORLD様の方で私とトンコツ(遠藤俊太)氏の2人で書きました以下で細かくアセットリネームツールのレシピを公開していますので参照してください。

以下は、上記ので紹介したツールに少し拡張を入れ、日本語文字をローマ字に自動変換するツールの実装例となります。
※解説は長くなるので省きますが、サンプルプロジェクトにアップしていますので、ダウンロードして見てみてください。

・実行動画

・動画中にあるPythonコード

#必要なモジュールをインポート
import pykakasi
import unicodedata

#pykakasiインスタンス作成
kakasi = pykakasi.kakasi()
#特殊文字を通常の全角文字に入れ替える
normalize_string = unicodedata.normalize('NFKC', in_string)
#日本語をローマ字に入れ替える
result_dict = kakasi.convert(normalize_string) 
result = ""
for char in result_dict:
	result = result + char['passport']

2.EditorUtilityでは使用できず、UnrealPythonにしか公開されていないメソッド

先程も記載しましたが、Blueprint内で使用できる関数はUnrealPythonにもメソッドとして公開され、使用できるのが基本的なルールとなっていますが、UnrealPython専用に公開されているメソッドもいくつかあります。

その中でも、Blueprintだけでは再現できない機能としてunreal.ScopedSlowTaskがあります。

これは、エディタ上でマップを開いている間等に画面の中心に出てくる進捗バーを表示するためのクラスとなります。

・進捗バーを出す処理

#時間のかかる処理を実行する
import unreal
import time

#ゲージの合計フレーム数
total_frames = 100
#現在のフレーム
current_frame = 0
#実行開始時のテキスト
text_label = "作業中"

#ScopedSlowTaskを生成
with unreal.ScopedSlowTask(total_frames, text_label) as slow_task:
	#ダイアログを表示
	slow_task.make_dialog(True)
	#1秒待つ
	time.sleep(1)
	#内部処理の開始
	for i in range(total_frames):
		#キャンセルが押されたらここで終了
		if slow_task.should_cancel():
			break
		#進捗フレームを1つ進める
		slow_task.enter_progress_frame(1,"処理完了:" + str(current_frame))
		#0.03秒待つ
		time.sleep(0.03)
		#現在のフレームに1足す
		current_frame = current_frame + 1

・実行動画

このように、大量の処理を行いがちなツールでは有用なので是非覚えておきましょう。

Blueprintに公開されていない関数としては、他にもパスからUPackageを取得できるunreal.load_packageやBlueprintのクラスデフォルトオブジェクトを取得できるunreal.get_default_object等いくつか有用なものもあります。

尚、ScopedSlowTaskを始めとしたBlueprintには公開されていない関数は、Engineコード上ではPyCFunctionCastというマクロを使用して定義されています。興味がある方は、Engineコード内を検索してみてください。

3.動的な関数・変数の定義について

エディタと連携可能なクラス・関数・変数の定義でも説明しましたが、@unreal.ufunctionunreal.upropertyで実装したメソッドやアトリビュートに関しては、エディタとの連携でとても有用です。Pythonであれば、ほぼ動的にメソッドやアトリビュートを作成できるため、これを応用するとエディター拡張においてとても便利な使い方をすることができます。

例えば、任意のオブジェクトにあるプロパティの詳細をオブジェクトダイアログウィンドウで表示することができるunreal.EditorDialog.show_object_details_viewUEditorDialogLibrary::ShowObjectDialog)というメソッドがありますが、通常のEditorUtility系のBlueprint等で任意のパラメータを表示するような実装をする場合、いちいちBlueprintクラスをアセットとして定義してからそのBlueprintクラスのインスタンスを作成し、UEditorDialogLibrary::ShowObjectDialogで表示する必要があります。

しかし、UnrealPythonであればクラスはPython内で定義されるため、不要なアセットを増やすことなく自由にカスタマイズ可能なプロパティを構築することができます。

・UnrealPythonで表示するオブジェクトダイアログウィンドウ

#オブジェクトダイアログウィンドウの表示デモ
import unreal
#ダイアログウィンドウ用のオブジェクトクラス
@unreal.uclass()
class PropertyObject(unreal.EditorUtilityObject):
	#プロパティの定義
	property1 = unreal.uproperty(bool)
	property2 = unreal.uproperty(unreal.Object)
#オブジェクト生成
detail_obj = PropertyObject(outer=unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem))
#ダイアログウィンドウの表示
unreal.EditorDialog.show_object_details_view("Test", detail_obj)
#入力した値を確認
unreal.log(detail_obj.property1)
unreal.log(detail_obj.property2)

・実行動画

上記の例だとアトリビュートの定義がクラス内で固定となっているので、汎用性を高めるなら動的にアトリビュートを追加するような仕組みを作るのが良いかなと思います。

ただ、@unreal.uclassの宣言がなされ、デコレータの処理が走ったタイミングでクラスが保持しているアトリビュートしかUnreal側がアクセス可能なプロパティとして宣言できません。

なので、インスタンス作成時に処理される__init__メソッドなどでは動的にアトリビュートを登録できないので、やるのであればアトリビュートを追加するようなデコレータを自作するのがよいでしょう。

・動的にクラスにアトリビュートを追加する処理

#オブジェクトダイアログウィンドウの表示デモ
import unreal
#ダイアログウィンドウ用のオブジェクトクラスを生成する
def create_object_for_dialog(custom_property):

	#動的にアトリビュートを宣言するデコレータ
	def add_custom_attribute(attribute_map):
		attribute_map = dict(reversed(list(attribute_map.items())))
		def decorator(cls):
			for key, value in attribute_map.items() :
				setattr(cls, key, value)
			return cls
		return decorator
	
	#unreal.uclassのあとに、作成したデコレータを宣言(デコレータの処理は宣言とは逆順で処理されるため)
	@unreal.uclass()
	@add_custom_attribute(custom_property)
	class PropertyObject(unreal.EditorUtilityObject):
		pass
	#ダイアログウィンドウ用オブジェクトを生成
	return unreal.new_object(PropertyObject,unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem))

#生成するプロパティの名前とupropertyのdict型変数作成
custom_property=dict()
custom_property["property_bool"] = unreal.uproperty(bool)
custom_property["property_object"] = unreal.uproperty(unreal.Object)

#ダイアログウィンドウ用オブジェクトを生成
detail_obj = create_object_for_dialog(custom_property)
#オブジェクトダイアログウィンドウを表示
unreal.EditorDialog.show_object_details_view("カスタムパラメータウィンドウテスト",detail_obj)

#オブジェクトのプロパティをすべてログに出力する
for key in custom_property:
	unreal.log(detail_obj.get_editor_property(key))

これであれば、任意のプロパティを持ったオブジェクトをcreate_object_for_dialog関数一つで生成できるので、汎用性が高いと思います。

そして、これらのアトリビュートにはエディタと連携可能なクラス・関数・変数の定義でも紹介したように「メタデータ指定子」をつけることが可能です。

これがC++以外から設定できるのはとても便利で、show_object_details_view等と組み合わせるとBlueprintだけでは再現できないプロパティの表示を行うことができます。

例えば、

・プロパティの表示名を任意に指定できるDisplayName
・プロパティ自体を編集可能かどうかを設定できるEditCondition
・Objectのサムネイルを非表示にするDisplayThumbnail
・Object内の特定のプロパティが条件に一致しているもののみを表示するRequiredAssetDataTags

等、便利な指定子が他にもたくさんあります。

・様々なメタデータ指定子を使った例

#オブジェクトダイアログウィンドウの表示デモ
import unreal
#ダイアログウィンドウ用のオブジェクトクラスを生成する
def create_object_for_dialog(custom_property):

	#動的にアトリビュートを宣言するデコレータ
	def add_custom_attribute(attribute_map):
		attribute_map = dict(reversed(list(attribute_map.items())))
		def decorator(cls):
			for key, value in attribute_map.items() :
				setattr(cls, key, value)
			return cls
		return decorator
	
	#unreal.uclassのあとに、作成したデコレータを宣言
	@unreal.uclass()
	@add_custom_attribute(custom_property)
	class PropertyObject(unreal.EditorUtilityObject):
		pass
	#ダイアログウィンドウ用オブジェクトを生成
	return unreal.new_object(PropertyObject,unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem))

#生成するプロパティの名前とupropertyのdict型変数作成
custom_property=dict()
custom_property["property_bool"] = unreal.uproperty(bool,meta=dict(EditCondition="property_bool_condition"))
custom_property["property_bool_condition"] = unreal.uproperty(bool)
custom_property["property_int"] = unreal.uproperty(int,meta=dict(UIMin = 0, ClampMin = 0,UIMax = 10, ClampMax = 10))
custom_property["property_object"] = unreal.uproperty(unreal.Object,meta=dict(DisplayThumbnail = False))
custom_property["property_class"] = unreal.uproperty(unreal.SoftClassPath,meta=dict(MetaClass = "/Game/DummyAsset/EUBP_EUTTest.EUBP_EUTTest_C"))
custom_property["property_dt"] = unreal.uproperty(unreal.DataTable,meta=dict(RequiredAssetDataTags = "RowStructure=/Script/UMG.RichTextStyleRow"))
custom_property["property_file_path"] = unreal.uproperty(unreal.FilePath,meta=dict(FilePathFilter = "なんかアセット(*.uasset)|*.uasset"))

#ダイアログウィンドウ用オブジェクトを生成
detail_obj = create_object_for_dialog(custom_property)
#オブジェクトダイアログウィンドウを表示
unreal.EditorDialog.show_object_details_view("カスタムパラメータウィンドウテスト",detail_obj)

#オブジェクトのプロパティをすべてログに出力する
for key in custom_property:
	unreal.log(detail_obj.get_editor_property(key))
#LogSlate: Window 'カスタムパラメータウィンドウテスト' being destroyed
#LogPython: True
#LogPython: False
#LogPython: 0
#LogPython: <Object '/Engine/EngineLightProfiles/180Degree_IES.180Degree_IES' (0x0000099B378FC400) Class 'TextureLightProfile'>
#LogPython: <Struct 'SoftClassPath' (0x0000099AF4A84640) {}>
#LogPython: <Object '/Game/DummyAsset/DT_RichTextStyleRow_Test.DT_RichTextStyleRow_Test' (0x0000099B39054980) Class 'DataTable'>
#LogPython: <Struct 'FilePath' (0x0000099AF4A84670) {file_path: "D:/Kinnaji/KAPythonUtility/Content/EUW_PythonTest.uasset"}>

・実行動画

通常はC++を管理するプログラマレベルの人でなければ扱えない部分ですが、UnrealPythonでも条件次第では使用することができるので、是非色々試してみてください。

※上記の注意点として、@unreal.upropertyunreal.Classの型を指定した際は、内部的にオブジェクトとして扱われてしまっているために、show_object_details_viewで表示してもうまく扱うことはできないと思います(内部的にFObjectPropertyとして生成されてしまうため、クラス表示するためのSSingleClassPropertyとしてUIを生成することができていない)
UnrealPythonでClass型を定義する際は、unreal.uproperty(unreal.SoftClassPath)を使用し、unreal.SystemLibrary.conv_soft_obj_path_to_soft_obj_ref等でClassとして読むことで擬似的に再現してください。

おまけ:UnrealPythonのメソッドをBlueprintに公開する

UnrealPythonでは、@unreal.ufunctionを用いてBlueprintにノードとしてメソッドを公開することができます。

#PythonのメソッドをBlueprintに公開
import unreal
#BlueprintFunctionLibraryを継承したPythonのクラス
@unreal.uclass()
class PythonTestBlueprintLibrary(unreal.BlueprintFunctionLibrary):
	#static関数としてメソッドを定義(とこからでも使用できるように)
	@unreal.ufunction(static=True)
	def TestScript():
		print("テストが成功しました")

・実行動画

ただし、Pythonから実装したBlueprintは、一度エディタを終了してしまうと定義が無効となり、次にエディタを起動したときに、使用しているBlueprintはコンパイルエラーを起こしてしまいます。

一応、エディタ起動直後にPythonクラスを再び定義すればエラーは解消できます。

公式ドキュメントにもありますが、Explorer上でプロジェクトのContent\Python\init_unreal.pyフォルダ以下にファイルを作成すると、そのPythonファイルはエディタの起動時に実行してくれる機能があります。このタイミングで実行することでBlueprintのエラーは解消はできそうでした。

・実行動画

※上記はあくまでもサンプルです。実際のプロジェクトでこれを使用するのはかなりリスクが高いため、使用はおすすめしません。また、init_unreal.pyよりも前にBlueprint等が読み込まれた場合は、一度Blueprintアセットをリロードする必要がでてくるのでご注意ください。

・リロードを入れる例

# coding: utf-8
#PythonのメソッドをBlueprintに公開
import unreal
#BlueprintFunctionLibraryを継承したPythonのクラス
@unreal.uclass()
class PythonTestBlueprintLibrary(unreal.BlueprintFunctionLibrary):
	#static関数としてメソッドを定義(とこからでも使用できるように)
	@unreal.ufunction(static=True)
	def TestScript():
		print("テストが成功しました")

#定義Pythonの定義後に使用しているBlueprintをリロードし、Python定義のBlueprintでコンパイルエラーを起こさないようにする
assets = unreal.Array(unreal.Object)
#使用しているBlueprintアセットのパス
#※本来は依存関係等で辿った方がいいが、ひとまず固定
assets.append(unreal.load_package("/Game/PyBPNode//EUW_PythonTest_PyBPNode"))
unreal.EditorLoadingAndSavingUtils.reload_packages(assets,interaction_mode = unreal.ReloadPackagesInteractionMode.ASSUME_POSITIVE)

長くなりましたが、大体こんなところになります。

まとめ

いかがでしたでしょうか?

やたら長くところどころ雑な内容になってしまって申し訳ありませんが、UnrealPythonを使ってより幅広い拡張を行うのに役立てば幸いです!

もうすぐ2023年も終わりますが、皆様体調にはお気をつけて良いお年を過ごしてください!

明日はmaichale_sanさんの「GPUを2つ使ったときに詰まったことを書く予定です」です!GPU2枚刺しとかなかなかやらないので、面白そうですね!

以上! オツカレサマ>🐟

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト