Bootstrap Chameleon Logo

Execution Order of Python Code in Chameleon Tool

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:

  1. TAPython plugin creates the interface defined by the user in the JSON file through C++ code
  2. Callbacks in the interface widgets are triggered
  3. Invoke the "InitPyCmd" in the JSON file (which creates an instance of the Python tool class)
  4. The constructor function __init__() [1 of the Python tool class
  5. The code after "creating" the tool class instance in "InitPyCmd"
  6. 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:

  1. 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
  2. Execute the interface update code using unreal.PythonBPLib.execute_python_command() and pass in the parameter force_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).

NOTE
force_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.