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.
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
.
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 frameinspect.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]