Bootstrap Chameleon Logo

Chameleon Tool中的Python代码执行顺序

在Chameleon Tool中,我们的Python代码会出现了多个不同的位置:

  • JSON 文件中的 "InitPyCmd"
  • JSON 文件中的 "OnClosePyCmd"
  • Python工具类的构造函数__init__()
  • 控件上的回调函数中的代码,比如SEditableText等控件上的 OnTextChanged

实际执行顺序

通过菜单项或者 unreal.ChameleonData.launch_chameleon_tool(json_path)打开工具后,以下事件会依次触发:

  1. TAPython插件通过C++代码创建用户在JSON中定义的界面
  2. 界面控件中的部分回调被触发
  3. 调用JSON文件中的"InitPyCmd"(其中会创建Python工具类的实例)
  4. Python工具类的构造函数__init__()1 注意:Python工具类都继承自单例,所以它们的构造函数只会被调用一次,除非它们所在的模块被reload
  5. "InitPyCmd"中,"创建"工具类实例之后的代码
  6. "OnClosePyCmd" 中的代码

注意:

  • 我们通常会在控件的回调函数调用Python工具实例中的方法(顺序2)。但当第一次打开工具时,工具的实例是不存在的(顺序4执行之后才存在)。此时需要加上判断
    "OnTextChanged": "if 'chameleon_inst' in globals()\n\tchameleon_inst.do_something()"

同理,SComboBox.OnSelectionChanged 在第一次运行时,也会因为chameleon_inst还没被赋值而报错。

    "OnSelectionChanged": "if 'chameleon_inst' in globals():\n\chameleon_inst.do_something()
  • 用户可以在Python工具实例中持有某个UObject的引用,并且关闭界面后,工具实例并没有被删除。99%情况下这种情况是我们想要的,我们不会丢失工具中的数据。但在切换UE的关卡时,持有之前场景中的物体的引用就会带来问题。这个时候就可以在"OnClosePyCmd"添加清理对象的操作。

例如:ObjectDetailViewer中, 我在"OnClosePyCmd"中调用了on_close()方法,并在其中清理持有的对象引用。

    "OnClosePyCmd": "chemeleon_objectDetailViewer.on_close()",

如果MenuConfig.json中的一个菜单配置项中,同时出现了"command""ChameleonTools"字段,那么只有"command"会被执行,"ChameleonTools"中的内容会被忽略。

优先级

"command" 高于 "ChameleonTools"

    {
        "name": "Chameleon Minimal Example",
        "ChameleonTools": "../Python/Example/MinimalExample.json",
        "command": "print('command called.')"
    },
    ...

线程

Rule 1: 任何对界面部分的操作,必须在主线程中执行。如果在其他线程中执行,会导致UE崩溃。

当我们在处理一些比较慢的任务,比如网络相关的任务,为了不卡住UE 编辑器,通常我们会在python中启动一个线程,执行相关的任务。通常在任务结束之后,我们会需要更新界面中的内容。此时,如果之间在python中调用修改界面的API,就会导致UE崩溃。

def on_button_click(self):
    self.thread = threading.Thread(target=self.some_slow_task)
    self.thread.start()

解决的方法是把更新界面的操作放到主线程中执行。这个时候,我们有至少两个选择:

  1. 在Chameleon工具的Python代码的OnTick中检查是否有需要更新的任务,如果有,就更新界面
  2. 通过unreal.PythonBPLib.execute_python_command()来执行更新界面的代码,并传入参数force_game_thread = True

下面的例子中,我们使用了第二种方法:

class ThreadExample(metaclass=Singleton):
    def __init__(self, jsonPath:str):
        self.jsonPath = jsonPath
        self.data = unreal.PythonBPLib.get_chameleon_data(self.jsonPath)
        self.ui_output = "InfoOutput"
        self.clickCount = 0
        self.thread = None

    def show_result(self):
        self.data.set_text(self.ui_output, "Clicked {} time(s)".format(self.clickCount))

    def some_slow_task(self):
        time.sleep(1)   # fake slow task
        unreal.PythonBPLib.exec_python_command("chameleon_thread_example.show_result()", force_game_thread=True)

    def on_button_click(self):
        if self.thread is None or not self.thread.is_alive():
            self.clickCount += 1
            self.data.set_text(self.ui_output, "Pending")

            self.thread = threading.Thread(target=self.some_slow_task)
            self.thread.start()
        else:
            ... # skipped, avoid multiple execution

注意execute_python_command中的第一参数是一个字符串,而不是一个函数。这个字符串会被当作Python代码执行。类似与在JSON文件中指定的"InitPyCmd""ConClick"等。

force_game_thread=True时,实际会启动一个AsyncTask,并把Python代码放到主线程(GameThread)中执行。

NOTE
force_game_thread 选项在TAPython 1.0.12中添加。如果你使用的是旧版本的TAPython,那么你需要手动更新TAPython插件。

SComboBox.OnSelectionChanged 在第一次运行时,会报错

JSON "OnSelectionChanged": "if 'chameleon_stable_diffusion' in globals():\n\tchameleon_stable_diffusion.on_change_sampler(%)"