Python plugin

Enable the plugins you wish to use. There might be more scripting modules under the scripting tab depending on what you have installed and what you wish to be able to modify with python.

  • Edit > Plugins > Scripting >
    • Editor Scripting Utilities
    • Python Editor Script Plugin
    • Sequencer Scripting
    • etc …

Developer Mode

The developer mode will enable extra warnings eg for deprecated code. It will also generate stub code for use to get auto completion for external IDEs. The stub file will be found here: Your Project > Intermediate > PythonStub > unreal.py. The stub file is generated every time the editor starts and is generated with your exact set of available code, plugins etc. Note that the file is quite large, about 10 mb, so your might have to increase the parsing limit of your external IDE to get the auto-complete working.

  • Edit > Editor Preferences > Plugins > Python
    • Developer Mode : Enable

You can also enable it on a project basis. Note! The tooltip of this setting informs us that “Most of the time you want to enable bDeveloperMode in the Editor Preferences instead”. But if you would like to enabled it on a project basis:

  • Edit > Project Settings > Plugins > Python
    • Developer Mode : Enable
    • Note! as stated above you probably want to enabled it with the editor prefes instead

Python Version

I am using Unreal 5.0 and it uses Python 3.9.7 by default. The path to the Python location is:

  • C:\Program Files\Epic Games\UE_5.0\Engine\Binaries\ThirdParty\Python3\Win64

You can not change the version of Python in a binary build (a build from the launcher). This is because the Python plugin needs to be recompiled. If you are using a source build, the simplest way of replacing the Pyhon version is to set an environment variable called “UE_PYTHON_DIR” and point that to the Python installation you wish to use. The downside to this is that it is not very portable since it ends up baking a path into your build to a local Python SDK. This means that if it were run on another system the user must have the same version of python in the same place. A better way to replace the Python version is to replace the Python version that are shipped with the engine. Theese are stored inside the Third Party directory and then rebuild. Because the location is inside the engine path it will be portable.

Output Log

  • Output Log
    • Window > Output Log
    • With the bottom-left dropdown you can switch between:
      • CMD
      • Python
      • Python (REPL)
    • Clear Log
      • RMB click > Clear Log
    • Filters
      • You can use the Filters dropdown on the top of the Output Log to disable warnings if you find them annoying. (Could be useful to have them on, but in certain situations it might improve the readability of the log)
      • Categories
        • If you only want to log Python print statements
          • Filters > Categories > Show All
            To disable all
          • Then enable LogPython (that you find in the lower middle of the list)

Crashes

When scripting you will inevitably chrash the editor form time to time. When The chrash Reporter windows appears you can see a line that says “Crash reports comprise diagnostics files (Click here to view directory) …” click that link to open up the log for the chrased session. Can be really useful to sift through.

x:\Path\To\Unreal Projects\MyBlend\Saved\Crashes...


Init Script

If the Editor detects a script file called init_unreal.py in any of the paths it is configured to use, it automatically runs that script immediately.

This is a good approach for situations where you are working on a Project or a Plugin and you know that everyone working with that content needs to run the same initialization code every time the Editor starts up. You could put your initialization code inside a script with this name, and put it into the Content/Python folder within that Project or Plugin.

When you use a relative path to run a Python script, or to import another script module using the import command in one of your scripts, the script that you run or import can be in any path that is listed in the sys.path variable of your Python environment.

To list the paths that unreal looks for python scripts we can run the following in the output log:

print('\n'.join(sys.path))
  • init_unreal.py
    • If the Editor detects a script file called init_unreal.py in any of the paths it is configured to use it automatically runs that script immediately.
  • Add init file
    • Lets use C:/Users/user_name/Documents/UnrealEngine/Python as the directory where we will keep our init_unreal.py file.
    • If this dir does not exist, create it.
    • In this script we can add additional paths that we want to have in our sys.path, for instance site.packages for PySide2 etc
    • We can also create a menu on the main menu for our tools
    • it might look something like:
import unreal
import sys

sys.path.append('C:/path/to/pyside2/Lib/site-packages')
sys.path.append('C:/your/custom/location/dev/unreal')

import petfactory.menu
petfactory.menu.add_menu()

  • Project Settings
    • You can also specify Startup Scripts and Additional Paths that the engine will add to sys.path on a per project basis Project Settings > Python > Startup Scrpts etc

Environment var

In a tool I am writing I wanted a reference to a project specific directory. I also wanted it present when I was developing outside of UE, so I decided to set a environment var and access the directory this way. To set the environment var in Unreal we can use the init_unreal.py script (Like said above UE will scan various places for a script with this name and run it, but in this case I created a directory called Python in my project dir and added the snippet below).

import os
from pathlib import Path
import unreal

your_dir = Path(unreal.Paths.game_user_developer_dir(), 'Your_dir_name')
if your_dir.is_dir():
	os.environ['YOUR_VARIABLE_NAME'] = str(your_dir.resolve())
	unreal.log('Environ Variable "YOUR_VARIABLE_NAME" was set')
else:
	unreal.log_warning('Environ Variable "YOUR_VARIABLE_NAME" not set!')

I used unreal.Paths.game_user_developer_dir to get the game user developer dir, used this as the dir root and then appended a directory. If the dir is found I keep a ref to it using an env var.

Gotcha

Console Variables

  • Note!
    • To be able to use the function below the Console Variables Editor plugin needs to be loaded.
    • Console Variables Editor Docs
  • Set CVar
    • unreal.ConsoleVariablesEditorFunctionLibrary.set_console_variable_by_name_int(‘r.RayTracing.Reflections.MaxBounces’, 3)
    • unreal.ConsoleVariablesEditorFunctionLibrary.set_console_variable_by_name_float(‘r.RayTracing.Reflections.MaxRoughness’, .5)
    • etc …
  • Get CVar
    • unreal.ConsoleVariablesEditorFunctionLibrary.get_console_variable_string_value(‘r.RayTracing.Reflections.MaxBounces’)
    • etc …

Properties

  • Get/Set
    • You can read/write properties with get_editor_property/set_editor_property
    • In the python api docs under the section Editor Properties you can see available properties for the class
  • Which class has the property?
    • The properties that you see in the details panel (in the UI of the editor) is not always on the actor itself, but on a component of the actor, so you might have to look up the api docs on the component itself to find the proprties you want to get/set.
  • Find the “API name” of the UI Property
    • To find the property in the api docs that matches the property that you see in the UI, hover the mouse over the property and the first row will be the name of the propery in the docs.
      • Note it will be snake cased and lower cased

EditorActorSubsystem

  • EditorActorSubsystem
    • When using the “EditorActorSubsystem” class we need to call methods on an instance of the class
    • There are 2 ways of creating an instance that of the subsystem that we can call our methods on
      • actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
      • actor_subsystem = unreal.EditorActorSubsystem()
        • Note the parenthesis meaning that we call it to get an instance
      • actors = actor_subsystem.get_all_level_actors()
    • I guess this is similar to the other subsystems…
  • EditorUtilityLibrary
    • unreal.EditorUtilityLibrary.get_selected_assets()
      Note! that get_selected_assets can be called as a static method of the unreal.EditorUtilityLibrary, but it can also be called on the instance of it like actor subsystem. Why it is designed like this is I do not know.
  • EditorAssetLibrary
    • unreal.EditorAssetLibrary.list_assets(directory_path)
      Note! that list_assets is also called as a static method, but can also be called as an instance method

Remote Execution

This is really interesting! This lets us control UE from lets say Houdini. Can be quite useful. I have just started to look into this, but here is a snippet to explore:

import sys
import pprint
sys.path.append(r'C:\Program Files\Epic Games\UE_5.3\Engine\Plugins\Experimental\PythonScriptPlugin\Content\Python')
import remote_execution as remote

def executeCommand(command):
    remote_exec = remote.RemoteExecution()
    remote_exec.start()
    remote_exec.open_command_connection(remote_exec.remote_nodes)
    exec_mode = 'ExecuteFile' # note this

    return remote_exec.run_command(command, True, exec_mode=exec_mode)
    
cmd_str = '''
def test():
    actor_list = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_selected_level_actors()
    for i, actor in enumerate(actor_list):
        label = actor.get_actor_label()
        x, y, z = i*100,0,i*100
        output = f\'{label} was moved to {x=}, {y=}, {z=}\'
        if z < 300:
            unreal.log(output)
        else:
            unreal.log_warning(output)
        actor.set_actor_location(unreal.Vector(x, y, z), False, False)
        
test()
''' 
result = executeCommand(cmd_str)    
pprint.pprint(result)

Snippets

Blueprint

Edit a variable or call function of CDO

Unreal Objects Docs

The UCLASS macro gives the UObject a reference to a UCLASS that describes its Unreal-based type. Each UCLASS maintains one Object called the ‘Class Default Object’, or CDO for short. The CDO is essentially a default ‘template’ Object, generated by the class constructor and unmodified thereafter. Both the UCLASS and the CDO can be retrieved for a given Object instance, though they should generally be considered read-only. The UCLASS for an Object instance can be accessed at any time using the GetClass() function.

We can set variables or call methods (that in turn set varaibles of the blueprint itself) of a CDO with the snippet below. (Make sure that the BP asset is selected in the content browser) If we compile and save the blueprint the edits will persist. Might be useful might be bad…

bp_asset = unreal.EditorUtilityLibrary.get_selected_assets()[0]
bp_class = unreal.load_object(None, bp_asset.generated_class().get_path_name())
bp_cdo = unreal.get_default_object(bp_class)
bp_cdo.set_editor_property('MyVar', 42)
bp_cdo.call_method("MyFunc", ('hello You!',))

Set variable of BP instance We can du this by using the standard set_editor_property method

bp_actor.set_editor_property('my_array', [0, 1, 2])
bp_actor.set_editor_property('my_int', 42)

def get_static_mesh_lod_data():

	# note to have generate lods for an asset we can open it up in the static mesh editor

	# and under LOD Settings set the LOD Group combobox to for instance "Large Prop"

	# to regenerate LODs based on the lod group "preset"

	static_mesh_list = get_content_browser_asset_of_class(asset_path='/Game/Pet', class_name='StaticMesh')
	for static_mesh in static_mesh_list:

		static_mesh_name = static_mesh.get_name()
		static_mesh_tri_count_list = []
		num_lods = static_mesh.get_num_lods()

		for lod_index in range(num_lods):
			# a section is if we have multiple unweleded elements in a mesh

			num_sections = static_mesh.get_num_sections(lod_index)
			lod_tri_count = 0

			for section_index in range(num_sections):
				section_data = unreal.ProceduralMeshLibrary.get_section_from_static_mesh(static_mesh, lod_index, section_index)
				# the second element of array is triangles, that we then divide by 3 sine the verts are shared by the triangles

				section_tri_count = len(section_data[1])/3
				lod_tri_count += section_tri_count

			static_mesh_tri_count_list.append(lod_tri_count)

		mesh_reduction_percent_list = [int((tri_count/static_mesh_tri_count_list[0])*100) for tri_count in static_mesh_tri_count_list]
		print(f' \n{static_mesh_name} has:\n\t{static_mesh_tri_count_list} triangles which is a reduction of\n\t{mesh_reduction_percent_list} percent\n')


def get_static_mesh_data():
	
	static_mesh_list = get_content_browser_asset_of_class(asset_path='/Game/Pet', class_name='StaticMesh')
	for static_mesh in static_mesh_list:
		asset_import_data = static_mesh.get_editor_property('asset_import_data')

		# below is a list of the base classes of "FbxStaticMeshImportData"

		# unreal.FbxStaticMeshImportData

		# unreal.FbxMeshImportData

		# unreal.FbxAssetImportData

		# unreal.AssetImportData

		# unreal.Object

		# unreal._ObjectBase

		# unreal._WrapperBase

		# object (the python object class)


		# we will call the method "extract_filenames" of the base class unreal.AssetImportData

		file_path = asset_import_data.extract_filenames()
		print(file_path)

def get_content_browser_asset_of_class(asset_path, class_name):

	asset_path_list = unreal.EditorAssetLibrary.list_assets(asset_path)
	asset_list = []
	for asset_path in asset_path_list:
		asset_data = unreal.EditorAssetLibrary.find_asset_data(asset_path)
		# asset_name = asset_data.asset_name

		asset_class_name = asset_data.asset_class
		asset = asset_data.get_asset()
		if asset_class_name == class_name:
			asset_list.append(asset)

	return asset_list
		
def get_all_actors():
	actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
	return actor_subsystem.get_all_level_actors()

def get_content_browser_selection():
	'''Note! does not include folders'''
	return unreal.EditorUtilityLibrary.get_selected_assets()

package_path = self.package_path_le.text()
sequence_name = self.sequence_name_le.text()
asset_path = f'{package_path}/{sequence_name}'

asset_data = unreal.EditorAssetLibrary.find_asset_data(asset_path)

PySide2

If you look inside the python install dir you can se pip there. I read somewhere that you can just use pip to install PySide2 in the python dir that comes with unreal. I have not tried that yet. Instead I just created a virtual env elsewhere, installed Pyside2 in that env and pointed unreal to the site-packages of that install. See this post to setup a virtual environment

  • Add site-pacakges dir
    • Project Settings > Plugins > Python > Additional Paths: ../path/to/venv/Lib/site-packages
    • or use the init_unreal.py script to add it to sys

Example

Here is a small snippet of how to creater a widget that is parented to the main window.

import sys

from PySide2 import QtGui
from PySide2 import QtWidgets

import unreal

class TestWidget(QtWidgets.QWidget):
	def __init__(self, parent=None):
		super(TestWidget, self).__init__(parent)

		vbox = QtWidgets.QVBoxLayout(self)
		btn = QtWidgets.QPushButton('Test')
		btn.clicked.connect(self.btn_clicked)
		vbox.addWidget(btn)

	def btn_clicked(self):
		print('Clicked')
		unreal.log('Clicked')


app = None
if not QtWidgets.QApplication.instance():
	app = QtWidgets.QApplication(sys.argv)	

widget = TestWidget()
widget.show()
unreal.parent_external_window_to_slate(widget.winId())

Then we can execute the script:

  • Tools > Execute Python Script : Browse to the script location

QApplication singleton

As you see in the example above we use an if statement to see if an instance of the QApplication exists. If we try to create a qApp without this check we will get the following error:

RuntimeError: Please destroy the QApplication singleton before creating a new QApplication instance.

So this means we create a qApp if it does not exists else we use the “current one”. If we for some reason wants to shutdown the qApp we can use this:

qApp.shutdown()

Resources & Docs