Bootstrap Chameleon Logo

Get Chameleon Instance Variable

When we develop ChameleonTools, we will have the need to get the Python instance of another tool in one tool. For example, a "parent" tool wants to be able to directly control other "child tools"; or after the task of the current tool is completed, trigger other tasks of another tool, etc.

Example

In this example, we use MinimalExample as an example of "child tool" and use a new tool to control it. Here, when we click the button of the Tricker tool, it will trigger the OnClick of the Miminal Example tool.

A gif demonstrating control the 'child' tool

Get tool instance

When we want to control the "MinimalExample" tool, we need to get the Python instance of the "MinimalExample" tool, that is, chameleon_example.

This variable is defined in InitPyCmd in MinimalExample.json, and the Python code in it will be executed when the interface tool is opened.

MinimalExample.json:

"InitPyCmd": "import Example, importlib; importlib.reload(Example); chameleon_example = Example.MinimalExample.MinimalExample(%JsonPath)",

Command line window

After the tool is opened, we can enter print(chameleon_example) in the command line window to view the value of this variable.

And we can see that the value of this variable is an instance of MinimalExample.

137_minimal_example_instance

And we can call chameleon_example.on_button_click() in the command line window to simulate clicking the button in the "MinimalExample" tool.

Problem

If we try to use chameleon_example.on_button_click() in the py file of other tool instances, we will find that the variable chameleon_example does not exist.

The reason is that they are in different scopes. The InitPyCmd in the command line and the Json file are in the global scope, while the py file in other tool instances is in the local scope.

Global scope

The global scope will have the following:

  • Command line window and REPL command line
  • "InitPyCmd" in Chameleon JSON file
  • "Command", "OnClick" and other fields in Chameleon JSON file
  • The code executed by unreal.PythonBPLib.exec_python_command is in the global scope even if it is executed inside the tool.

In tool code

As the code executed by unreal.PythonBPLib.exec_python_command is in the global scope, we can trigger on_button_click of MinimalExample in the Python code of other tools.

MinimalExampleTricker.py:

import unreal
from Utilities.Utils import Singleton

class MinimalExampleTricker(metaclass=Singleton):
    def __init__(self, json_path: str):
        self.json_path = json_path
        self.data: unreal.ChameleonData = unreal.PythonBPLib.get_chameleon_data(self.json_path)

    def on_click(self):
        unreal.PythonBPLib.exec_python_command("chameleon_example.on_button_click()")

And we can also assign it to a variable in the current tool, so that we can no longer use strings to call it. chameleon_example_ticker is the instance variable name of the current tool. Since it is in the global scope, we cannot access the current instance through self.

MinimalExampleTricker.py:

    ...
    def set_minimal_example(self, chalemeon_instance):
        self.minimal_example = chalemeon_instance

    def on_click(self):
        if not self.minimal_example:
            # set local variable, so that we can access it in other function
            # PythonBPLib.exec_python_command executes in global scope
            unreal.PythonBPLib.exec_python_command(
                "chameleon_example_ticker.set_minimal_example(chameleon_example)"
            )
        if self.minimal_example:
            self.minimal_example.on_button_click()

Get Chameleon Instance Variable through inspect

We can use the inspect module in the Python standard library to get the variables in the upper scope of the current scope, so as to get the Chameleon instance in the global scope.

For example, in the following example, we use:

  • inspect.currentframe It will return the current frame
  • inspect.getouterframes It will return a list of the current FrameInfo and all external FrameInfo, and the last entry represents the outermost FrameInfo on the stack.
    def on_click(self):
        if not self.minimal_example:
            parent_frame = inspect.getouterframes(inspect.currentframe())[1].frame
            self.minimal_example = parent_frame.f_globals.get("chameleon_example")
        if self.minimal_example:
            self.minimal_example.on_button_click()

To demonstrate simply, we directly hard code [1] to get the upper frame of the current frame. Then use frame.f_globals to get the chameleon_example variable in the upper Scrope global variable.

Furthermore, we can also encapsulate this logic into a function, so that it can be used in other tools.

Utils.py

def get_chameleon_instance_in_parent_frame(name:str):
    for frame in inspect.getouterframes(inspect.currentframe()):
        if name in frame.frame.f_globals:
            return frame.frame.f_globals[name]

Reference