Skip to main content

Scripts

Everything in the example project that is not code is generated: the map, the poses, the sounds, the effects. Five Python scripts in Projects/MountingAndVehicles/Scripts/ build it all from nothing, and a sixth measures the car. This page is what each one does, how to run it, and what it taught us — because each of them hit at least one thing that does not log an error and simply produces nothing.

All of them run from a command line against the example project. Two flavours:

# A commandlet: no window, no game tick. Enough for assets.
UnrealEditor-Cmd.exe <project>.uproject -run=pythonscript -script=Scripts/<name>.py -unattended -nosplash -NullRHI

# The full editor: Slate and Play In Editor available. Needed where noted.
UnrealEditor-Cmd.exe <project>.uproject -ExecCmds="py Scripts/<name>.py" -unattended -nosplash

They can also be run from the editor's Python console (py Scripts/<name>.py). Every script is safe to run again: it deletes and rebuilds what it owns and touches nothing else.

build_demo_map.py — the level

Builds Lvl_MountingDemo: the ground, the plaza and its five stations (car, car against a wall, low garage, turret, horse), the circuit and the access road, the signs, and the two generated materials everything is painted with (M_DemoSurface, M_DemoGlass). Actors it manages carry a tag; on a rerun it removes those and spawns them again, so hand-placed actors in the map survive.

Run it as a commandlet. Run it again whenever a class's root component layout changes — the horse became a Character at one point, and the copy already saved in the map kept its old component data and loaded at the origin with no seats. Respawning is the fix.

build_seated_pose.py — three poses and an animation set

Writes AS_MountSeated, AS_MountDriving and AS_MountRiding (thirty-frame single-pose sequences, solved from joint directions in the mannequin's component space) and DA_MountAnimations, the animation set whose three entries the framework scores per phase, role and mount tag. See the animation guide for the scoring and the example guide for why the riding entry names both roles.

Commandlet. The pose must be at least as long as the blend into it, or the blend never reaches it.

build_demo_audio.py — MetaSounds

Writes MS_CarEngine and MS_CarDoor with the MetaSound builder API. The engine is a straight six built additively — the crank's harmonic series with the third order loudest and the half orders under it, a pulse-train exhaust, throttle-gated intake noise, a ladder filter, a compressor — and it exposes four inputs the car sets every frame: RPM, Throttle, Skid, Speed. The door is a click and a thump under two envelopes, with a Pitch input.

Commandlet. Three things to know:

  • MetaSoundBuilderSubsystem is an engine subsystem; the asset is written by MetaSoundEditorSubsystem.build_to_asset.
  • Node class names are (namespace, name, variant). Most are UE::<Name>::Audio|Float, but AD Envelope, Clamp and MapRange use their own name as namespace. When in doubt, add the node and list its pins; the script's Graph helper asserts on every pin it cannot find.
  • build_to_asset will not create over a name that exists — not even one deleted this session. The script overwrites an existing asset in place with build_and_overwrite_meta_sound instead, and gives each builder a fresh name, because builder names become registered class names.

build_demo_fx.py — Niagara

Writes NS_TyreSmoke and NS_SkidMark and their materials. Niagara has no scripting API of its own for assembling emitters; the Cascade-to-Niagara converter does, and the script uses it — an empty emitter, engine modules added by path, inputs set by name, a renderer attached.

Full editor, not a commandlet: finalising an emitter builds its stack widgets, which need Slate. And the converter plugin is editor-only and enabled for the run rather than for the project:

UnrealEditor-Cmd.exe <project>.uproject -ExecCmds="py Scripts/build_demo_fx.py" -EnablePlugins=CascadeToNiagaraConverter -unattended -nosplash

An emitter built this way is invisible by default, four separate ways, none of which logs anything:

  • the conversion context starts disabledset_enabled(True);
  • a fresh Emitter State loops once for a second — set Life Cycle Mode to Self and Loop Behavior to Infinite, or the component reports inactive before anyone looks;
  • a curve data interface made from script has its lookup table out of sync and evaluates to nothing — use Lerp_Float on Particles.NormalizedAge instead;
  • Scale Sprite Size fed from a scripted Vector2DFromFloat zeroes the sprites — set the size at spawn instead.

Attributes a module does not expose as inputs go through set_parameter_directly("Particles.X", ...); that is how the skid ribbon gets its up-facing vector.

tune_handling.py — the car, in numbers

Starts Play In Editor, takes the pole car to open ground inside the circuit, and puts it through what a keyboard driver does: full lock at 60, 100 and 140 km/h held for a second and a half and released, a left-right flick at 100, a full brake from 120, a handbrake turn at 70. For each it reports the peak slip angle, the peak yaw rate, the heading gained with the key down, the heading it kept gaining after release, and the speed kept. The targets are in the script's header and in the report it writes to Saved/HandlingReport.txt:

slip while cornering    <= 6 deg      grip, not drift
overshoot after release <= 20 deg the car stops turning when you stop asking
yaw rate at 100 km/h >= 35 deg/s a tap of the key is a change of direction
speed kept, 1.5 s lock >= 80 % the corner does not scrub the car to a halt

Full editor. It sets Play settings to Standalone for the session and does not save them. Change a number in EDMountExampleVehicle.h or EDMountExampleWheel.cpp, build, run this, read the table: that is the whole loop, and it is the reason the car's handling is a set of measurements rather than a set of opinions.

Testing anything networked from Python

A caution that applies to any script that pokes at the framework in Play In Editor: every call from Python into the engine runs under the editor's script-execution guard, and under it a Server RPC executes locally. A probe that calls RequestMount from Python on a client will see the server never seat anyone and conclude the framework is broken. It is not. Drive the game through real input instead — EnhancedInputLocalPlayerSubsystem.inject_input_vector_for_action on the client's local player — which is processed by the input system on the next tick, outside the guard.