前言

  众所周知,目前的 Unreal Python 还无法编写界面。
  尽管 Python API 文档包含了诸多 UI 组件的调用说明。
  但是无法将窗口显示出来也是没有意义的。

  经过我的研究,可以通过 Editor Utility Widget 来实现 Python 编写界面。
  真的是用 Python API 提供的组件来组装 UI ,甚至可以实现资源选择的功能。
  只可惜目前无法用纯 Python 实现 modal dialog 的功能,多少受到了一定的掣肘。

UObject 罗列

  在 Python 环境下可以获取到 Unreal 继承于 UObject 的对象。
  之前我就是通过路径上的手脚可以罗列出当前注册的菜单名 链接: Unreal Python 进阶菜单扩展

  后来我想到可以写个 C++ 函数将所有的 UObject 打印出来,这样似乎可以实现更多黑科技。

1
2
3
4
5
6
7
8
9
TArray<UObject *> UPyToolkitBPLibrary::GetAllObjects()
{
TArray<UObject *> Array;
for (TObjectIterator<UObject> Itr; Itr; ++Itr)
{
Array.Add(*Itr);
}
return Array;
}

  通过这个调用可以用下属的代码将引擎内置的 uobject 输出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json
import unreal
py_lib = unreal.PyToolkitBPLibrary()
objects = py_lib.get_all_objects()
res_list = []
for obj in objects:
try:
res_list.append(obj.get_path_name())
except:
print("error -> %s" % obj)

path = r"F:\UnrealScript\test\object_list.json"
with open(path,'w') as f:
json.dump(res_list,f,indent=4)

  输出的信息可以参考 我的仓库 链接
  利用这个方法我甚至可以获取到一些不在 Python API 的类型,不过通常这种情况也无法调用到 对象 内置的方法。

EditorUtilityWidget Python API 获取

  通过上面的方法可以获取到 Editor Utility 的一些内置的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:WidgetTree.CanvasPanel_0", 
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:EventGraph.K2Node_Event_2",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:EventGraph.K2Node_Event_1",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:EventGraph.K2Node_Event_0",
"/Engine/Transient.NewEditorUtilityWidgetBlueprint_C_0:WidgetTree_0",
"/Engine/Transient.NewEditorUtilityWidgetBlueprint_C_0",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C:WidgetTree",
"/Engine/Transient.EdGraph_4",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:ExecuteUbergraph_NewEditorUtilityWidgetBlueprint",
"/Engine/Transient.TRASHCLASS_NewEditorUtilityWidgetBlueprint_7",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C",
"/Engine/Transient.WidgetGraphSchema_0",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.PackageMetaData",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.SKEL_NewEditorUtilityWidgetBlueprint_C",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:EventGraph",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:WidgetTree",
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint",

  从上面可以获取到 EditorUtilityWidget:WidgetTree

1
2
3
4
5
tree = unreal.find_object(None,"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:WidgetTree")
print(tree)
# LogPython: <Object '/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint:WidgetTree' (0x000001CD4F78A520) Class 'WidgetTree'>
print(dir(tree))
LogPython: ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_post_init', '_wrapper_meta_data', 'cast', 'get_class', 'get_default_object', 'get_editor_property', 'get_fname', 'get_full_name', 'get_name', 'get_outer', 'get_outermost', 'get_path_name', 'get_typed_outer', 'get_world', 'modify', 'rename', 'set_editor_properties', 'set_editor_property', 'static_class']

  可以获取到 WidgetTree 对象,但是在 Python API 下查不到它。

  下面开始一些骚操作了,从上面罗列的对象,可以看到有 _C 结尾的 EditorUtilityWidget 对象。
  根据我之前写的文章分析过 Unreal Python 修改蓝图组件属性 , _C 结尾的对象是蓝图 compile 之后获取的 CDO
  Unreal 是调用 CDO 生成的对象实现蓝图的功能。

  但是这里只有未生成 CDO CanvasPanel , 于是我尝试打开 EditorUtilityWidget 点击 compile 再次获取

附注: 也可以直接获取 CanvasPanel ,然后调用 Python API 添加组件,只是在这里添加会导致蓝图 compile 出错。

1
2
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C:WidgetTree.CanvasPanel_0", 
"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C:WidgetTree",

  此时就可以获取到 _C 结尾的 CanvasPanel_0

1
2
3
canvas = unreal.find_object(None,"/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C:WidgetTree.CanvasPanel_0")
print(canvas)
LogPython: <Object '/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint_C:WidgetTree.CanvasPanel_0' (0x000001CD52D81F80) Class 'CanvasPanel'>

  经过千辛万苦,终于可以获取到 CDO 生成的 CanvasPanel 容器。
  接下来就可以 Python API 添加组件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from __future__ import print_function
widget_BP = unreal.load_asset('/Game/sequence/NewEditorUtilityWidgetBlueprint.NewEditorUtilityWidgetBlueprint')
canvas = unreal.find_object(None,'%s_C:WidgetTree.CanvasPanel_0' % widget_BP.get_path_name())
layout = unreal.VerticalBox()

button = unreal.Button()

# NOTE 添加按钮点击事件
delegate = button.on_clicked
delegate.add_callable(lambda:print("button click"))

block = unreal.TextBlock()
block.set_text("click")
button.add_child(block)
layout.add_child(button)
slot = canvas.add_child_to_canvas(layout)

# NOTE 构筑 Vertical Layout 撑满效果
slot.set_anchors(unreal.Anchors(maximum=[1,1]))
slot.set_offsets(unreal.Margin())


# NOTE 生成界面
editor_sub = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem)
widget,id = editor_sub.spawn_and_register_tab_and_get_id(widget_BP)

  如上图所示,可以通过 Python 动态生成 UMG 按钮触发相应事件。

纯 Python 触发蓝图 compile | UMG 全自动生成

  上述的方案多少需要一些人工的点击,比如 EditorUtilityWidget 的生成, compile 按钮点击。
  有没有可能在不使用 C++ 的前提下完成所有功能呢?

  其实资源自动生成的问题还好, Python API 有相关的 Factory EditorUtilityWidgetBlueprintFactory
  问题是如何通过 Python 完成 蓝图 的 compile
  我之前研究过这个问题,找不到相关的 API 只好写了一个 C++ 蓝图函数来触发 compile

1
2
3
4
void UPyToolkitBPLibrary::CompileBlueprint(UBlueprint *InBlueprint)
{
FKismetEditorUtilities::CompileBlueprint(InBlueprint);
}

  然而我还想再挣扎一下,于是我全局搜索了 FKismetEditorUtilities::CompileBlueprint
  希望有别的蓝图函数调用到它,没想到还真给我找到了一个地方 ~

alt

  通过 UBlueprint 的 rename 功能可以触发蓝图的强制 compile。
  如此就可以不自己写 C++ API 用纯 Python 的方式实现蓝图 compile 了。

  关于 rename 的方式,我最初想到是使用 rename_asset 函数。
  后来我发现 UObject 也有 rename ,这样不需要保存 asset ,调用效率更高。

2022-5-15 补充

  我发现 4.27 版本关于这个部分已经优化掉了,如果想要编译蓝图,还是用 C++ API 比较稳妥。
  另外 UE5 添加了 BlueprintEditorLibrary.compile_blueprint 链接 可以用 Python 调用。
  UE4.27 下如果想要纯 Python 实现估计是不行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from __future__ import print_function
import posixpath
from functools import partial
import unreal

asset_lib = unreal.EditorAssetLibrary
asset_tool = unreal.AssetToolsHelpers.get_asset_tools()

def create_asset(asset_path="", unique_name=True, asset_class=None, asset_factory=None):

if unique_name:
asset_path, _ = asset_tool.create_unique_asset_name(asset_path,'')
if not asset_lib.does_asset_exist(asset_path=asset_path):
path, name = posixpath.split(asset_path)
return asset_tool.create_asset(
asset_name=name,
package_path=path,
asset_class=asset_class,
factory=asset_factory,
)
return unreal.load_asset(asset_path)

material = unreal.load_asset('/Game/test/CubeTest.CubeTest')



directory = "/Game/test"
name = "TestWidget3"
path = posixpath.join(directory,name)
factory = unreal.EditorUtilityWidgetBlueprintFactory()
widget_BP = create_asset(path,True,unreal.EditorUtilityWidgetBlueprint,factory)
# NOTE 改名强制 compile
widget_BP.rename("%s_" % name)

canvas = unreal.find_object(None,'%s_C:WidgetTree.CanvasPanel_0' % widget_BP.get_path_name())

layout = unreal.VerticalBox()
button = unreal.Button()

# NOTE 添加按钮点击事件
delegate = button.on_clicked
delegate.add_callable(lambda:print("button click"))

block = unreal.TextBlock()
block.set_text("test")
button.add_child(block)
layout.add_child(button)
slot = canvas.add_child_to_canvas(layout)

# NOTE 构筑 Vertical Layout 撑满效果
slot.set_anchors(unreal.Anchors(maximum=[1,1]))
slot.set_offsets(unreal.Margin())


# NOTE 生成界面
editor_sub = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem)
widget,id = editor_sub.spawn_and_register_tab_and_get_id(widget_BP)

  上面的函数就可以做到从零开始生成一个界面。

关闭窗口 自动清理蓝图资产

  利用 Python API 的 tick 函数,我可以实现更加便利的操作。
  比如说关闭窗口 自动清理蓝图资产。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
global __dialog_dict__
__dialog_dict__ = {
id:{
"widget":widget,
"widget_BP":widget_BP,
}
}

editor_sub = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem)
def __slate_handler__(delta):

global __dialog_dict__
remove_list = []

for _id,info in __dialog_dict__.items():
widget = info.get("widget")
widget_BP = info.get("widget_BP")
if not editor_sub.does_tab_exist(_id):
remove_list.append(_id)
continue

# NOTE 自动清理本地资产
for _id in remove_list:
info = __dialog_dict__[_id]
widget_BP = info.get("widget_BP")
# NOTE rename force compile
widget_BP.rename("%s__delete__" % widget_BP.get_name())
QtCore.QTimer.singleShot(0,partial(asset_lib.delete_asset,widget_BP.get_path_name()))
del __dialog_dict__[_id]

tick_handle = unreal.register_slate_pre_tick_callback(__slate_handler__)
unreal_app = QtWidgets.QApplication.instance()
__QtAppQuit__ = partial(unreal.unregister_slate_pre_tick_callback,tick_handle)
unreal_app.aboutToQuit.connect(__QtAppQuit__)

通过 UMG 获取 property 属性

  于是我想到更加进阶的用法,那就是利用 EditorUtilityWidget 提供的 组件 用原生的方式获取虚幻的资产。
  EditorUtilityWidget 的设计界面提供了 SinglePropertyView 可以作为属性的选择。
  经过我的摸索,我可以用 Python 构建 UClass 来模拟一个 uproperty
  通过这个 uproperty 获取到选择的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from __future__ import print_function
# NOTE 蓝图构筑省略 ...

canvas = unreal.find_object(None,'%s_C:WidgetTree.CanvasPanel_0' % widget_BP.get_path_name())
layout = unreal.VerticalBox()
property_view = unreal.SinglePropertyView()

# NOTE 生成一个材质的 property 添加到 SinglePropertyView
@unreal.uclass()
class PropertyObject(unreal.Object):
material = unreal.uproperty(unreal.MaterialInstanceConstant)
obj = PropertyObject()
property_view.set_object(obj)
property_view.set_property_name("material")
layout.add_child(property_view)

button = unreal.Button()
delegate = button.on_clicked
delegate.add_callable(lambda:print(obj.material))
block = unreal.TextBlock()
block.set_text("Get Material")
button.add_child(block)
layout.add_child(button)

slot = canvas.add_child_to_canvas(layout)

# NOTE 构筑 Vertical Layout 撑满效果
slot.set_anchors(unreal.Anchors(maximum=[1,1]))
slot.set_offsets(unreal.Margin())


# NOTE 生成界面
editor_sub = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem)
widget,id = editor_sub.spawn_and_register_tab_and_get_id(widget_BP)

  如此生成的界面获取可以更加灵活。
  也可接入 detail view 直接让用户编辑特定对象的属性,只是属性太多配置不太灵活。

总结

  利用这个黑科技,可以使用纯 Python 构筑 UMG 界面。

  Qt 的痛点就是很难像原生 Unreal 的选择窗口一样快速罗列出所有可以选择的资源对象。
  利用 Python API 可以做,但是要获取缩略图转换到 Qt 下效率就很低,只能做到无缩略图的效果。
  目前虽然实现了 Python 构筑 UMG 但是无法使用 Modal Dialog 的阻塞方式配合 Qt 的 GUI 一起使用,还是差了临门一脚。
  这个问题只能 C++ 处理了。