Stumbling Into UE4 Python
Out of the gate, it’s important to understand that the Python exposure wraps BluePrint, not the C++ SDK, if something isn’t exposed to blueprint, you cannot access it with Python.
A Note on Naming
There are notable differences that should be pointed out regarding the python exposure and the UE4 programming documentation. UE4 class names have dropped the prefix, an FRotator would be unreal.Rotator, FVector, unreal.Vector, etc. Functions on those classes have been converted to PEP8 style names that differ from their C++ counterparts, for example Actor.GetActorBounds is Actor.get_actor_bounds, just FYI.
Detective Work
If you have ever used an initial alpha or beta embedded Python implementation in an application, like Maya or MotionBuilder, (if you’re old like me!) –you know the drill. There’s no documentation yet, but you can use existing blueprint/C++ documentation and native python built-in functions like dir, help, and type to try and navigate.
DIR()
Python’s dir command returns a ‘directory’ of all attributes of an object. This is extremely useful. Let’s query the attributes of a static mesh actor:
print dir(actor) |
This returns a massive list, if you print each item, you’ll see something like this:
... get_remote_role get_squared_distance_to get_tickable_when_paused get_typed_outer get_velocity get_vertical_distance_to get_world has_authority hidden initial_life_span instigator is_actor_being_destroyed ... |
HELP()
Jamie Dale has put a lot of work into making help work as expected, through dir we saw the actor has a get_velocity property, let’s ask help abut it:
help(actor.get_velocity) |
Help returns not only info about get_velocity, but tells us what a function takes and returns:
Help on built-in function get_velocity: get_velocity(...) x.get_velocity() -> Vector -- Returns velocity (in cm/s (Unreal Units/second) of the rootcomponent if it is either using physics or has an associated MovementComponent param: return_value (Vector) |
TYPE()
Type just returns the type of object, but it’s useful when you’re not sure exactly what something is. Let’s ask what type our actor is:
print type(actor) #<type 'StaticMeshActor'> |
Now let’s ask it about the get_velocity above, we know it’s a function because I just used it, but as an example:
print type(actor.get_velocity) #<type 'builtin_function_or_method_with_closure'> |
With these core concepts, you can really begin stumbling around and making useful scripts and tools!
In Practice
Let’s query all StaticMeshes in a level (the default UE4 map). First thing is to load the level:
import unreal level_path = '/Game/StarterContent/Maps/Minimal_Default' level = unreal.find_asset(None, name=level_path) |
Next we use get_all_actors_of_class to query all the static meshes:
static_meshes = unreal.GameplayStatics.get_all_actors_of_class(level, unreal.StaticMeshActor) print static_meshes #returns: [StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Table"', StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Statue"', StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Floor_14"', StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Floor"', StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Chair_15"', StaticMeshActor'"/Game/StarterContent/Maps/Minimal_Default.Minimal_Default:PersistentLevel.Chair"'] |
That’s returned a long list of class objects, let’s make that more readable by calling the get_name function of each class with a simple list comprehensions:
print [mesh.get_name() for mesh in static_meshes] #returns: ['Table', 'Statue', 'Floor_14', 'Floor', 'Chair_15', 'Chair'] |
How did I know there was a get_name functions? I saw it when I ran directory on the static mesh actor: dir(actor).
Let’s move the table:
if mesh.get_name() == 'Table': xform = actor.get_actor_transform() location = actor.get_actor_location() print xform print location #returns: #<Struct 'Transform' (0x0000000095E5D3C0) {rotation: {x: 0.000000, y: 0.000000, z: 0.000000, w: 1.000000}, translation: {x: -180.000000, y: 0.000000, z: 32.000000}, scale3d: {x: 1.000000, y: 1.000000, z: 1.000000}}> #<Struct 'Vector' (0x00000000B8ACE5C0) {x: -180.000000, y: 0.000000, z: 32.000000}> |
Let’s change the location and use set_actor_location to set a new location:
location.z += 28 #location = 60 actor.set_actor_location(location) #returns: #Traceback (most recent call last): #TypeError: Required argument 'sweep' (pos 2) not found |
Wow, so that fails, we need to find out why, let’s ask help:
print help(actor.set_actor_location) |
Help on built-in function set_actor_location: set_actor_location(...) x.set_actor_location(new_location, sweep, teleport) -> HitResult or None -- Move the Actor to the specified location. param: new_location (Vector) -- The new location to move the Actor to. param: sweep (bool) -- Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something. Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect. param: teleport (bool) -- Whether we teleport the physics state (if physics collision is enabled for this object). If true, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location). If false, physics velocity is updated based on the change in position (affecting ragdoll parts). If CCD is on and not teleporting, this will affect objects along the entire swept volume. param: sweep_hit_result (HitResult) -- The hit result from the move if swept. param: return_value (bool) return: Whether the location was successfully set (if not swept), or whether movement occurred at all (if swept). |
Ok, awesome, so I’ll tell it I don’t want it to sweep, and I do want it to teleport:
location.z += 28 #location = 60 actor.set_actor_location(location. False, True) |
You should see the table move!
NOTE: Careful, in order for your Python changes to be added to the undo stack, you need to use unreal.ScopedEditorTransaction.
I hope this was helpful, I can post some other examples later.
From the looks of the print syntax, it appears the plugin is built with python2.
Are there plans to support Python3?
Comment by Bob white — 2018/04/03 @ 2:49 PM
Nice! This was quite helpful for sure. Nice to the the conversion of functions to PEP8 for the python folks. Totally dig this addition to UE4.
Comment by Randall Hess — 2018/04/03 @ 8:20 PM
Super cool! Glad other people are doing more scripting with python + ue4 so we can community build a lot of tooling around pain points.
FWIW, we’ve been supporting an open-source community python plugin that’s a bit much further developed. For example, there’s a lot of wrapping around C++ editor only features, decently robust wrapping around slate widgets, editor extensions, etc. Here’s an (outdated) sizzle: https://twitter.com/KNLstudio/status/932657812466843648
We use it to create custom editor modes, toolbar extensions, custom viewports and tooling, etc. It’s also supported at runtime but we only use it for iterative dev purposes
Our fork is here: https://github.com/kitelightning/UnrealEnginePython
Comment by ikrima — 2018/04/04 @ 12:11 AM
Also we’re happy to share a lot of our existing tooling if a community effort ever springs forth/someone else can manage it 🙂
Comment by ikrima — 2018/04/04 @ 12:17 AM