[Python] - calling method on main game thread from current thread

Hi @anonymous_user_58fd81f3,

Short answer: Yes.

Long answer:
You have to expose new function for python, which will then run a custom python code (string code) on main thread. The way I did it is not the best one ( i guess), but its the only way I could find.

First I created an editor c++ plugin ( as everyone should do, because the later distribution is easier…) called PythonExtension. In the plugin there are two important files. Public/PythonExtension.h and Private/PythonExtension.cpp ( if they are not here, create them and then regenerate project solution (how to generate solution)

In PythonExtension.h file, new blueprint/python callable method was created:

/**
	 * Makes possible to run python script on the main thread from any other thread.
	 * @param pythonCode - input python code in form of a string example: "print('Hello Unreal')", can be multiline.
	 */
	UFUNCTION(BlueprintCallable, Category="PythonExtension")
	static void LaunchScriptOnGameThread(FString pythonCode);

PythonExtension.cpp is simple as well:

#include "PythonExtension.h"
// its important to include IPythonScriptPlugin.h as well
#include "../Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Public/IPythonScriptPlugin.h"
#include "Async/TaskGraphInterfaces.h"
#include "Microsoft/MinimalWindowsApi.h"

void UPythonExtension::LaunchScriptOnGameThread(FString pythonCode)
{
	FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady( [&]()
	{
		IPythonScriptPlugin::Get()->ExecPythonCommand(*pythonCode);
	}, TStatId(), NULL, ENamedThreads::GameThread);

	FTaskGraphInterface::Get().WaitUntilTaskCompletes(Task);
}

You also have to add PythonScriptPlugin into PrivateDependencyModuleNames in your Build.cs ( so you can include IPythonScriptPlugin.h in PythonExtension.cpp) and also to you .uplugin so Unreal knows its using the PythonScriptPlugin.

PythonExtension.uplugin:

{
	"FileVersion": 3,
	"Version": 1,
	"VersionName": "1.0",
	"Description": "",
	"Category": "PythonExtension",
	"CanContainContent": true,
	"IsBetaVersion": false,
	"IsExperimentalVersion": false,
	"Installed": false,
	"IsHidden": false,
	"EnabledByDefault": true,
	"Modules": [
	{
			"Name": "PythonExtension",
			"Type": "Editor",
			"LoadingPhase": "Default"
	}],
	"Plugins": [
		{
			"Name": "PythonScriptPlugin",
			"Enabled": true
		}
	]
}

PythonExtension.Build.cs:
PrivateDependencyModuleNames.AddRange(new string[] {"Core", "CoreUObject", "Engine", "PythonScriptPlugin",});

Now when You build editor, you should be able to call this from python console:

import unreal
unreal.PythonExtension.launch_script_on_game_thread("print('hello from main thread')")

Now the python part:
Our launch_script_on_game_thread() method takes python string code as an input argument so it will be nice to have some method, which will read another method as input and create string from the code inside:

def run_on_main_thread(function):
    """
    This will launch input function on unreal main game thread
    """
    func_lines = inspect.getsourcelines(function)[0][1:]

    code_in_string = ""
    
    # remove blank spaces at start and end in every line
    for line in func_lines:
        code_in_string += line.strip() + "\n"
    
    unreal.PythonExtension.launch_script_on_game_thread(code_in_string)

And final touch - calling the function somewhere in your python thread:

# this is somewhere inside python class..
        def import_main_thread_prep(self):
            """
            This function is launched by our c++ PythonExtension module on Main Thread.
            It is the only way how to import asset if working on different thread.
            Returns:

            """
            # since this is run on different thread, we have to repeat the import statements for our import_manager
            # in c++ source code we simulate this like if person writes following three lines of 
            # code into python console
            from your_custom_module import YourClass
            # YourClass could be singleton, so its quite easy to init data on it and then get them without using arguments
            my_custom_singleton_class = YourClass()
            my_custom_singleton_class .call_ue_asset_methods()

Thats it. It will probably take you some time to tinker with it, but once you grasp how it works, it should be pretty easy to do your own stuff on it.

PS: its also possible, that in code above there are some mistakes as I had to change my code in order to simplify it for you. If you find a mistake, please feel free to respond so I can change it.

3 Likes