前言

  最近有个需求,需要在工具里面调用 EditorUtilityBlueprint 写好的功能。
  于是踩坑回顾一下。

call_method 调用内部函数

  蓝图里面添加了连接好的函数 Function ,比如如下图所示

alt

  如何通过 Python 来调用这个自定义的函数呢?

1
2
3
4
5
6
7
# NOTE 选择蓝图
bp, = unreal.EditorUtilityLibrary.get_selected_assets()
bp_path = bp.get_path_name()
gc = unreal.load_object(None, "%s_C" % bp_path)
cdo = unreal.get_default_object(gc)

cdo.call_method("TestCall",args=(unreal.World(),))

  经过我的测试,可以利用 _ObjectBase 内置的 call_method 调用对象内置的方法
  但是这个方案有很多问题,上面只是一个简单的蓝图连接,就需要在 args 上加上莫名奇妙的 对象补充才能正常运行函数。

alt

  否则会提示缺少参数而无法执行。
  如果蓝图连接得非常复杂的化,会更加麻烦,需要填充非常多的参数才可以。
  于是出于好奇,我就去找了源码

alt

  从源码上可以看到是参数的识别出问题了。
  因此如果用纯 Python 调用,需要解决大量的参数调用,会非常地麻烦,唯一的好处是可以不用写 C++

UFunction 获取

  既然这个方法不同,于是我想到了之前的思路,先找一下有没有 Function 相关的 UObject 操作链接
  通过这个方法可以获取, Unreal 里面完整的 UObject 列表 链接

1
2
3
4
5
6
7
8
[
// 省略 ...
"/Engine/Transient.REINST_NewEditorUtilityBlueprint_C_229:call",
"/RedArtToolkit/Resources/UVCapture/NewFolder/NewEditorUtilityBlueprint.NewEditorUtilityBlueprint_C:TestCall",
"/Engine/Transient.REINST_NewEditorUtilityBlueprint_C_229:TestCall",
"/RedArtToolkit/Resources/UVCapture/NewFolder/NewEditorUtilityBlueprint.NewEditorUtilityBlueprint_C:call"
// 省略 ...
]

  从上面的路径的确可以获取到对应函数命名的 UObject
  用 Python 加载试试。

1
2
3
4
5
6
7
import unreal

obj = unreal.load_object(None,"/RedArtToolkit/Resources/UVCapture/NewFolder/NewEditorUtilityBlueprint.NewEditorUtilityBlueprint_C:TestCall")
print(obj)
# LogPython: <Object '/RedArtToolkit/Resources/UVCapture/NewFolder/NewEditorUtilityBlueprint.NewEditorUtilityBlueprint_C:TestCall' (0x000001C7BE07E980) Class 'Function'>
print(dir(obj))
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_post_init', '_wrapper_meta_data', 'call_method', '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']

  可以看到获取到了一个 Function 对象,然而神奇的是 文档里面只有 FunctionDef 对象,并没有 unreal.Function 的说明
  尝试用 dir 打印 Function 对象提供的函数,然而也只能获取到 UObject 的白板方法。
  经过网上查阅,可以知道这个就是 C++ 的 UFunction 对象。 官方文档
  看来要实现 UFunction 的调用只能借助 C++ 了

UFunction 调用

  如何才能解决 UFunction 的调用呢?
  我想到的还是去抄 Unreal 的源码,最容易想到就是 EditorUtilityObject
  官方的直播和文档里面都有提到,如果想要用蓝图扩展右键菜单,可以用 EditorUtilityBlueprint 继承相应的对象。

alt

  只要继承 AssetActionUtility 对象,就可以在右键菜单的 Scripted Action 里面调用蓝图相关的功能。 官方文档教程
  相必,可以查 C++ 源码知道里面的函数是怎么被调用的。

alt

  经过一番努力,我定位到代码出发的关键在 ProcessEvent 上。
  于是基于上面的代码,我可以封装一个蓝图库函数,来实现蓝图函数的调用。

1
2
3
4
5
6
7
8
9
10
11
void UPyToolkitBPLibrary::RunFunction(UObject *CDO, UFunction *Function)
{
// We dont run this on the CDO, as bad things could occur!
UObject *TempObject = NewObject<UObject>(GetTransientPackage(), Cast<UObject>(CDO)->GetClass());
TempObject->AddToRoot(); // Some Blutility actions might run GC so the TempObject needs to be rooted to avoid getting destroyed

FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BlutilityAction", "Blutility Action"));
FEditorScriptExecutionGuard ScriptGuard;
TempObject->ProcessEvent(Function, nullptr);
TempObject->RemoveFromRoot();
}

  类似于上面的蓝图库函数。
  Python 的这边就要获取 蓝图的 CDO 对象和 UFunction 对象,然后传入到 RunFunction 的 C++ 调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
import unreal
py_lib = unreal.PyToolkitBPLibrary

func_name = "TestCall"
# NOTE 选择蓝图
bp, = unreal.EditorUtilityLibrary.get_selected_assets()
gc_path = "%s_C" % bp.get_path_name()
gc = unreal.load_object(None, gc_path)
cdo = unreal.get_default_object(gc)
func = unreal.load_object(None,"%s:%s" % (gc_path,func_name))
py_lib.run_function(cdo,func)

alt

  通过上面的方式就可以实现直接调用蓝图内部函数了。
  实测只有 EditorUtilityBlueprint 的对象可以起作用,如果是普通的蓝图还无法调用。

总结

  我之前还花了不少时间,研究有没有可能通过 inspect 或者 异常处理 来实现纯 Python 的调用。
  然而经过我的测试 inspect 获取参数是不行的,毕竟不是 Python 的原生函数。
  利用异常可以解决部分问题,但是需要解析每个报错的信息,然后再将对应的参数提供到 args 里面
  如果是简单的蓝图连接还能应付,如果很复杂的话,需要异常出发很多次来处理出一个正确的参数序列,最后我还是放弃了。
  实践的部分代码在我的 CodeBase 里面 链接