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:
MetaSoundBuilderSubsystemis an engine subsystem; the asset is written byMetaSoundEditorSubsystem.build_to_asset.- Node class names are
(namespace, name, variant). Most areUE::<Name>::Audio|Float, butAD Envelope,ClampandMapRangeuse their own name as namespace. When in doubt, add the node and list its pins; the script'sGraphhelper asserts on every pin it cannot find. build_to_assetwill not create over a name that exists — not even one deleted this session. The script overwrites an existing asset in place withbuild_and_overwrite_meta_soundinstead, 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 disabled —
set_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_FloatonParticles.NormalizedAgeinstead; - Scale Sprite Size fed from a scripted
Vector2DFromFloatzeroes 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.