In Chameleon Tool, our Python code appears in several different locations:
"InitPyCmd"
in the JSON file"OnClosePyCmd"
in the JSON file- The constructor function
__init__()
of the Python tool class - Callback function code on widgets, such as
OnTextChanged
on SEditableText and other widgets
Actual Execution Order¶
After opening the tool through the menu item or unreal.ChameleonData.launch_chameleon_tool(json_path)
, the following events are triggered in order:
- TAPython插件通过C++代码创建用户在JSON中定义的界面
- 界面控件中的部分回调被触发
- 调用JSON文件中的
"InitPyCmd"
(其中会创建Python工具类的实例) - Python工具类的构造函数
__init__()
1 注意:Python工具类都继承自单例,所以它们的构造函数只会被调用一次,除非它们所在的模块被reload "InitPyCmd"
中,"创建"工具类实例之后的代码-
"OnClosePyCmd"
中的代码 -
TAPython plugin creates the interface defined by the user in the JSON file through C++ code
- Callbacks in the interface widgets are triggered
- Invoke the
"InitPyCmd"
in the JSON file (which creates an instance of the Python tool class) - The constructor function
__init__()
[1 of the Python tool class - The code after "creating" the tool class instance in "InitPyCmd"
- The code in "OnClosePyCmd"
NOTE
Python tool classes are all derived from singletons, so their constructors are called only once unless their module is reloaded
Note¶
- We usually call methods in the Python tool instance through the widget's callback functions (order 2). But when the tool is first opened, the tool instance does not exist (it exists after order 4). At this point, a judgment is needed
"OnTextChanged": "if 'chameleon_inst' in globals()\n\tchameleon_inst.do_something()"
Similarly, SComboBox.OnSelectionChanged will throw an error when first run because chameleon_inst
has not been assigned yet.
"OnSelectionChanged": "if 'chameleon_inst' in globals():\n\chameleon_inst.do_something()
- Users can hold a reference to a UObject in the Python tool instance, and the tool instance is not deleted after the interface is closed. In 99% of cases, this is what we want, and we won't lose data in the tool. But when switching UE levels, holding a reference to an object in a previous scene can cause problems. At this point, you can add an operation to clean up the object in
"OnClosePyCmd"
.
For example: In ObjectDetailViewer, I call the on_close() method in "OnClosePyCmd"
and clear the held object reference.
"OnClosePyCmd": "chemeleon_objectDetailViewer.on_close()",
NOTE
If a menu configuration item in MenuConfig.json has both a "command"
and a "ChameleonTools"
field, only the "command"
will be executed, and the content in "ChameleonTools"
will be ignored.
Priority¶
"command"
is higher than "ChameleonTools"
{
"name": "Chameleon Minimal Example",
"ChameleonTools": "../Python/Example/MinimalExample.json",
"command": "print('command called.')"
},
...
Threads¶
Rule 1: Any operation on the interface part must be executed in the main thread. Executing in other threads will cause UE to crash.
When dealing with some slow tasks, such as network-related tasks, we usually start a thread in Python to execute the related tasks without blocking the UE editor. Usually, after the task is completed, we need to update the content on the interface. At this point, if you call the interface modification API directly in Python, it will cause a crash.
def on_button_click(self):
self.thread = threading.Thread(target=self.some_slow_task)
self.thread.start()
The solution is to put the interface update operation in the main thread. At this point, we have at least two options:
- In the OnTick of Chameleon Tool's Python code, check if there is an update task that needs to be done, and if so, update the interface
- Execute the interface update code using
unreal.PythonBPLib.execute_python_command()
and pass in the parameterforce_game_thread = True
In the example below, we use the second method:
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
Note that the first parameter of execute_python_command
is a string, not a function. This string will be executed as Python code. Similar to the "InitPyCmd"
, "ConClick"
and other commands specified in the JSON file.
When force_game_thread=True
, an AsyncTask will be started, and the Python code will be executed in the main thread (GameThread).
NOTEforce_game_thread
option was added in TAPython 1.0.11. If you are using an older version of TAPython, you will need to manually update the TAPython plugin.