The example project
MountingAndVehicles, alongside this plugin. Five stations around a plaza, each demonstrating a
different thing the framework does, each with a sign telling you what to try.
| Station | What it is for |
|---|---|
| Car | Four seats, doors that swing open, a steering wheel that turns. |
| Blocked doors | The same car parked against a wall, so the seats on that side stop being offered. |
| Low garage | Drive in, press E, and watch the exit have nowhere to put you. |
| Turret | A mount that is not a vehicle, with its seat on the part that rotates. |
| Horse | Two seats on a saddle that bobs with the gait, and a rider carried by it for free. |
Plus a track to drive on and a ramp to jump.
Everything is C++, in Source/MountingAndVehicles/Example/. No Blueprint, no imported art: every prop
is an engine primitive tinted at runtime, so the demo has no content dependencies, cannot break when
assets move, and looks the same on a fresh clone as it does here.
Running it
Open MountingAndVehicles.uproject and press Play. The demo map is the project's default, and you
start in the middle of the plaza facing the first car.
| Key | On foot | Seated |
|---|---|---|
| WASD | Walk | Steer, accelerate, brake |
| Mouse | Look | Look |
| E | Get into whatever is nearest | Get out |
| Q | — | Move to the next free seat |
| Space | Jump | Handbrake |
Failures print on screen: walk up to a car whose only free seat has a blocked door and it says so.
Try these
ED.Mounting.DrawSeats 1. Every seat and every door, colour-coded by whether a rider actually fits there right now.ED.Mounting.Dumpprints the same thing as text.- Watch the door. Getting in swings the door for that seat and closes it behind you. There are no animations in this project at all — the door is driven by the procedural transition, which is the point.
- Watch the steering wheel while you drive. It is a component turned by the same value the wheels get.
- The car against the wall. Its wall-side doors have no room to stand, so those seats are not offered and their capsules are red. Drive it away and they come back.
- The low garage. 150 cm of headroom against a 176 cm clearance capsule. Drive in, press E, and the exit falls back — or refuses, with Allow Unsafe Exit Fallback turned off in project settings.
- Press Q while seated. The shuffle releases one seat and takes the other in one step.
- The turret. Same two components, no wheels, no physics. Its seat is parented to the rotating head, so you turn with the gun.
- The horse. Two seats. Ride it and watch the saddle bob; the rider bobs with it because the seat is parented to the part that moves, not because anything animates the rider.
- Two players. Number of Players: 2, Run Under One Process off. Both walk to the same car and press E on the same frame — exactly one gets the seat, and the other is rolled back.
What is where
| File | Shows |
|---|---|
Public/Example/EDMountExampleCharacter.h | The whole rider-side integration: one component, three calls. |
Private/Example/EDMountExampleVehicle.cpp | Four seats, four doors, a steering wheel, and the Chaos setup. |
Private/Example/EDMountExampleHorse.cpp | A mount with no wheels, and a seat on a moving part. |
Private/Example/EDMountExampleTurret.cpp | The smallest possible mount that still steers. |
Private/Example/EDDemoStyle.cpp | The palette, and how everything gets its colour with no assets. |
Scripts/build_demo_map.py | The map, as a script you can read. |
The character, in full
This is the entire mounting integration on the rider side:
// Constructor
Rider = CreateDefaultSubobject<UEDRiderComponent>(TEXT("Rider"));
MotionWarping = CreateDefaultSubobject<UMotionWarpingComponent>(TEXT("MotionWarping"));
// The interaction key
Rider->IsMounted() ? Rider->RequestDismount() : Rider->RequestMountNearest();
// The shuffle key
Rider->RequestShuffleSeat(true);
// The movement axis
if (Rider->IsSeated())
{
Rider->ApplyDriveInput(Axis, FMath::Max(0.f, Axis.Y), FMath::Max(0.f, -Axis.Y), bHandbrakeHeld);
return;
}
The rest of that file is camera work and a mannequin.
The demo map
Generated by Scripts/build_demo_map.py, not committed as authored content:
UnrealEditor-Cmd.exe MountingAndVehicles.uproject -run=pythonscript -script="Scripts/build_demo_map.py"
A binary map is the one thing in an example nobody can review, merge or diff: when it stops working after an engine upgrade, the only way to find out what it used to contain is to open it. The script is the map, in a form you can read, and re-running it is the repair — it clears what it placed last time and rebuilds, leaving the sky and sun alone because those are not its to own.
It also generates the level's material instances from the engine's BasicShapeMaterial, so the whole
palette is the PALETTE dictionary at the top of the file. Change a colour there and re-run.
The car is built, not imported
The engine template's sports car mesh is still underneath — Chaos needs a skeletal mesh with wheel bones and a physics asset, and there is no sense generating one of those — but its chassis is hidden and the bodywork you see is built from primitives. Only the wheels are imported, because a round thing made of boxes is not a wheel.
It is laid out from a table, not part by part
Every piece is placed against one list of heights and one list of stations, so the parts meet by construction rather than by being nudged until they look close:
Z -17 the road X 250 front bumper
Z 0 underbody, 15 cm of clearance X 214 front of the bonnet
Z 10 floor pan, and the cabin floor X 76 base of the windscreen
Z 24 top of the sills X -100 base of the rear glass
Z 38 the seats, so a rider's hips X -180 rear deck
Z 61 top of a tyre X -246 rear bumper
Z 62 the wheel-arch line
Z 78 the dashboard
Z 88 the beltline: bodywork stops, glass starts
Z 118 top of the glass
Z 128 the roof
That is not decoration. The first version of this car chose numbers one at a time, and every fault it had came from the same place: the floor pan ended up level with the road, the rear bumper dragged along it, and the tail lights floated on posts behind a spoiler that touched nothing.
The beltline is not the arch line
This one is worth stating on its own, because getting it wrong is subtle and the symptom looks like something else entirely. An arch sits on a tyre; a beltline sits at the shoulder of the person inside. Put them at the same height and there is no cabin volume left — and then the dashboard has nowhere to go, so it ends up ahead of the windscreen, inside the engine bay, which is what somebody looking at the car actually notices.
An arch has to come down on both sides of the tyre
A single long fender over each side is not a wheel arch — it leaves a rectangular hole four times the size of the wheel. Each wheel gets a bar across the top of the tyre and a short post fore and aft of it, reaching down to the sill line and no further: take the posts to the road and the four corners of the car turn into four visible legs.
The wheels carry a trap
Attaching a wheel mesh to the bone Chaos drives is what makes it roll and steer with no per-frame work — but a component attached to a bone inherits the bone's orientation, not the mesh's. On this skeleton the wheel bones are turned ninety degrees in yaw and another ninety in roll, so an uncorrected wheel lies on its side with its axle pointing down the road.
The constructor undoes it, measured from the reference skeleton rather than written down:
const FQuat Bone = RefBoneRotation(CarMesh, WheelBones[Index]);
Wheel->SetRelativeRotation(Bone.Inverse() * Wanted);
The spin the physics applies is on top of that, so the correction is a constant and the wheel still turns. In the constructor rather than at begin play, because a car whose wheels are only straight once the game runs is a car that looks broken in the editor.
The wheels are classes, not Blueprints
UEDMountExampleWheelFront and Rear replace the vehicle template's wheel Blueprints. A wheel
Blueprint is forty numbers in a details panel, and "why does it slide" is not a question you can answer
from a diff. The three that decide whether a hard turn holds:
- Cornering stiffness — how hard a slipping tyre pulls back towards where it is pointed. This is the one that finishes a turn. Raising friction alone just makes the car push wider more slowly.
- Ackermann steering — the inside wheel has a tighter circle to follow, and steering both front wheels to the same angle drags one of them sideways through the whole corner.
- The centre of mass — it sits at the middle of the physics asset, which for this chassis is up at the beltline. A car that tall through the middle rolls onto its outside wheels and unloads the inside pair, and a wheel carrying no weight has no grip.
The steering curve matters too: the stock one falls away hard with speed, which reads as a car that starts a fast turn and then stops turning half way through it.
Glass needs its own material
Translucency is a blend mode, and a blend mode is not something a material instance can change. A very
dark opaque pane is not a window: it reads as a hole, and you cannot see the driver, the dashboard or
the road through it — which is most of what makes an occupied car look occupied. M_DemoGlass is
generated alongside the surface material for exactly this.
The circuit and the race
The demo is also a small racing game, because a mounting system that only ever mounts is only half demonstrated: the other half is what happens after — input reaching the vehicle, a camera that works at speed, a rider who is visibly driving.
AEDMountExampleTrack grows a circuit from a dozen spline points the map script gives it: tarmac,
alternating kerbs, guard rails, a covered tunnel, a start gantry, a grandstand, trees, lamp posts and
tyre stacks on the outside of the corners. A second instance with an open spline is the access road
from the plaza onto the start straight; the circuit leaves a gap in its rail where it arrives. Nothing
is imported.
AEDMountExampleGameMode puts four cars on the grid and gives each an AI driver. The driver is an
AEDMountExampleRacerController possessing the same character class the player is, using the
same rider component the same way: it calls RequestMount on its car, waits to be Seated, and
feeds ApplyDriveInput every frame. Nothing about the AI talks to the vehicle directly. That is the
point of doing it this way rather than possessing the cars: the AI goes through exactly the path the
player does, so anything that breaks for one breaks for both, where it can be seen.
The driving itself is pure pursuit on the track's spline plus a pull towards the centreline, with
corner speeds from one line of physics: the tyres hold sqrt(grip / curvature). Measured over a race,
the cars peak at 0.40 of half the road width and spend none of it beyond 0.75 — they hold their line.
Pole position is left empty for you. Take the car, or any car, across the line and you are in the standings.
The screen
AEDMountExampleHUD draws straight to the canvas: speed, gear, a rev bar and the two pedals while you
are in a car; laps and positions while the race is on; and, on foot, a prompt over the door you are
looking at.
That prompt is the framework's own query with a narrow view cone. UEDMountingSubsystem::FindBestMount
already scores candidates on how close they are to the middle of the view; the character asks it a few
times a second with MaxViewAngle at 38 degrees, keeps the answer, and the interaction key mounts
that candidate through RequestMountWithOptions — never "whatever is nearest". The prompt and the
key agree by construction, which is what makes it "look at the door" rather than "stand near the car".
Three details in the framework make the cone honest, and all three were bugs first. The angle is measured to
the door — halfway between where the rider would stand and where they would sit, in the horizontal
plane — not to the entry point, which is a spot on the ground beside the car that a rider standing at
the door is on top of or past, and therefore out of any cone while the far door's entry point sits
comfortably in view across the roof. And bPreferDriverSeat, which is decisive when a query has no
cone ("get in that car" means the driver's seat), drops to a tie-breaker when it has one: a rider
looking at the passenger door has already said which seat they mean. The seat's SortPriority does
the same — it is the designer's order for "any seat", and at a thousand centimetres a step it was
offering the front passenger seat from every door on the car.
Hands on the wheel
UEDMountExampleDriverAnimInstance is what a rider's mesh runs while seated: the framework's seated
pose, sampled, with two-bone IK on each arm onto the grips the car publishes at ten-and-two on its
rim. The grips are children of the steering column, so they turn with it, and the hands follow —
measured at 0.0 cm off the grip with the wheel centred and 5 cm at full lock, where the far hand runs
out of arm.
It is a native anim instance with a hand-written Evaluate: no Blueprint, no Control Rig, six bone
names. A production character with its own anim graph would do the identical maths with a Two Bone IK
node in a seat anim layer; this is only where it lives when there is no graph to put it in.
Three poses from one asset
Scripts/build_seated_pose.py writes three poses and one animation set. The set's entries are all on
Mount.Phase.Seated and the framework picks the most specific:
| Entry | Matches on | Score | Who gets it |
|---|---|---|---|
AS_MountSeated | phase | 1 | anybody, anywhere |
AS_MountDriving | phase + Mount.Role.Driver | 5 | the driver of anything |
AS_MountRiding | phase + both roles + a query for Mount.Type.Horse | 7 | anybody on the horse |
The horse carries Mount.Type.Horse in its mountable's MountTags, which the animation set's context
query sees. The riding entry names both roles because a query alone scores 3 and would lose to the
driving pose on the horse's saddle — which is exactly the wrong way round, and the arithmetic is worth
knowing before you build a set of your own.
A horse with its own ideas
The horse is a Character now, not a Pawn with a floating movement component: it stands on the ground,
walks up the ramp and jumps, and the character movement component already knew how to do all three.
Nothing about it is possessed. The rider keeps their own controller under the seat's policy and the
horse runs its movement with none (bRunPhysicsWithNoController), which is also what lets it do the
two things it does on its own.
It wanders. Unridden, it picks a point within fifteen metres of where it started, walks there at a
stroll, stands for a few seconds, and picks another. It steers itself through the same ApplyDriverInput
a rider uses, so the wander is exactly a rider who is not in a hurry, and it gives up on a walk that
has got stuck behind something rather than on walking. Get on and it stops; get off and it stands a
moment before going anywhere, so you are not chasing it across the plaza when you turn round.
It jumps. Space, the key that is the handbrake in a car, is the jump on a horse — the same key
arrives through the same ApplyDriverInput and the mount decides what it means. In the air the body
holds its pose and the legs tuck, and a horse turning on the spot steps round rather than pivots: the
legs get a slow, short stride while the reins are pulled and the body is still.
None of this touched the framework. A mount that moves itself is still a mount; the seats ride along.
Two players
Play As Client with two players and the demo is a two-player demo: the second player gets in the passenger seat of the car the first is driving, or takes the horse. What makes that work is one rule, applied four times: simulate where the thing is simulated, show everywhere else.
- The car's commanded inputs, engine speed and gear are replicated from the server, so the wheel turns, the brake lights come on, the rev counter sweeps and the engine note follows on every machine that can see the car. The driving client sets its own ahead of the round trip.
- The car feeds Chaos and runs its assists on the server and on the driver's own client, which simulates ahead of the server and is corrected by replicated movement — the framework hands the driver's controller ownership of the car for exactly this. A spectating client's copy is replicated movement; torque applied to it fights the correction.
- The horse wanders, turns and jumps on the server. A client's copy is a replicated character; only the legs' step-in-place reads a replicated value.
- The race lives in
AEDMountExampleGameState, because the game mode that runs it exists only on the server and the HUD is drawn on clients.
The framework's half of this is in the replication concept page: a client's driving input reaches the server through
ServerApplyDriveInput, and OnRiderSeated / OnRiderLeft fire on clients too.
Compared with the reference plugin
This example was built alongside a commercial mounting plugin, and the framework's shape is partly an
answer to it. The reference exposes seats as an array of structs with integer ids, mounts by a relative
direction enum, and reports state through six booleans. This framework exposes seats as scene
components with stable handles, mounts through validated entry points, and reports one phase. Where
the reference has something this does not, it is deliberate: its walk-to-the-door step is what motion
warping is for here, and its "linked mount actor" is two mountables. The one thing it had that was
simply missing — what did I just get off — is now GetPreviousMountActor.
Holding the road
The car is a keyboard car, and a keyboard is a step function: every input is full lock or nothing,
arriving in one frame. Three things in AEDMountExampleVehicle turn that into something a tyre can
answer, and they are worth reading in order because each one exists because the one before was not
enough.
The steering is rate-limited. What reaches the tyres is a ramp towards the command, at five full-locks a second at rest and just over two at speed, and allowed to unwind faster than it winds on. A tap of D at a hundred and forty is a quarter turn of the wheel that builds, not full lock in a physics step.
Rear-wheel drive, with a turn-in assist. The rears spin on a hard exit and the tail comes out,
which is the appeal. What makes that a game rather than a simulator is yaw from the steering itself:
TurnInAssist adds a little rotation in the direction of the lock, fading in above walking pace and
back out towards top speed, so a tap of the key is a change of direction at a hundred and forty rather
than a polite drift wide. It is off with the wheels in the air. The rear axle also runs more friction
and cornering stiffness than the front, and downforce does the rest — grip that arrives with speed,
which is exactly when it is needed.
Traction control of its own. Chaos has one and it is not enough for a thousand newton-metres through two tyres: left to it the rears spin from the line to sixty and the car is slow, smoky and loose all at once. The car reads the driven wheels' longitudinal slip and takes throttle away past a threshold — quick to bite, slow to give it back — so a launch chirps the tyres and hooks up. It steps aside while the handbrake is held, because a handbrake turn wants the rears spinning.
A stability assist that lets the tail out. The slip angle is how far the nose points away from
where the car is going. Up to six degrees it is yours; past that the assist applies a yaw
acceleration back towards the direction of travel, bleeds off the yaw rate, and lifts up to
forty-five percent of the throttle — the counter-steer a driver with a wheel would give half a second
before a keyboard could. With the handbrake held the slide belongs to the driver and only the damping
stays, so a handbrake turn rotates until the key is released and then stops. GetStabilityAssist
reports how hard it is working.
None of this is the framework's business, which is the point of saying so: the framework delivered
ApplyDriverInput and stepped back. Everything above is the car.
Sounds from nothing
Scripts/build_demo_audio.py writes two MetaSounds with the builder API, so the project ships no
audio files at all.
MS_CarEngine is a straight six, built additively: the crank's harmonic series with the third order
loudest (a six fires three times a revolution) and the half orders underneath it, which are most of
why a six sounds smooth where a four sounds flat; a narrow pulse train at the firing rate, low-passed
hard, for the exhaust; throttle-gated noise for the intake. All of it goes through a ladder filter
whose cutoff follows revs and throttle and a compressor that keeps the idle audible, plus band-passed
noise for the tyres and low-passed noise for the wind. The car sets four
parameters every frame: RPM, Throttle, Skid, Speed. It starts when a driver sits down and
fades when they leave; a passenger in a parked car sits in silence, which is right.
MS_CarDoor is a latch: a click of filtered noise and a thump of sine under two short envelopes,
played at the hinge on every swing and a little lower when the door shuts. It is triggered from
SetSeatDoorOpen, which is the same call the framework makes for the door animation — so the sound
is on the frame the hand reaches the handle, for free.
The sounds are spatialised from the audio components rather than from an attenuation asset, so four AI cars on the far side of the circuit are a distant hum and not four engines in the driver's ear.
Rubber on the road
Scripts/build_demo_fx.py writes two Niagara systems and their materials the same way: from nothing.
Niagara has no scripting API of its own for building emitters, but the Cascade-to-Niagara converter
does — it is how Epic's own converter assembles a system module by module — and the script uses it:
an empty emitter, engine modules added by path, inputs set by name, a renderer attached. The plugin
is editor-only and is only needed to run the script, so it is enabled for the run
(-EnablePlugins=CascadeToNiagaraConverter) rather than for the project. It also needs the editor
proper rather than a commandlet: finalising an emitter builds its stack widgets, which need Slate.
NS_TyreSmoke is grey puffs that drift through curl noise and fade in a second and a half.
NS_SkidMark is a ribbon facing straight up, so it lies flat, dark for twelve seconds. Both are
world-space, which is the whole trick: the emitter rides the tyre's contact patch and the particles it
leaves do not, so the smoke hangs where it was made and the ribbon is drawn along the path the tyre
took. The car reads each wheel's FWheelStatus — sideways skid and longitudinal slip, in centimetres
a second — spawns one of each system on the contact point when a wheel starts to slide, and
deactivates them when it grips. A fresh system per slide is what keeps two slides
from being joined by a straight line of rubber across the track.
Two cameras
V switches between the boom behind the character and a camera in its head. The first-person camera
is positioned every frame from the head bone — its location, never its rotation, because on the
mannequin the head bone points up — and the head itself is hidden so the camera is not inside a face.
The body stays: hands on the wheel and feet on the pedals are most of what first person is for.
Nothing about mounting changes between the two. The seat, the seat camera anchor and the control rotation set on seating are the same in both views, and the one thing first person has to do — turn the body with the view on foot — is switched off while seated, because the framework owns the pawn's rotation there and a controller yaw fighting it would twist the rider on the seat.
Where the demo stops
Worth knowing before you judge the framework by it:
- There are no authored transition animations. Every get-in, get-out and shuffle is the procedural slide. That is deliberate — it is the honest demonstration that the framework works before an animator has touched it.
- The seated poses are generated, not animated.
Scripts/build_seated_pose.pywrites two from a description of where each limb should point: a plain seated one, and a driving one whose legs stretch into the footwell and whose hands are solved onto the steering wheel's actual position, 52 cm ahead of the driver's hips and 34 above them. The animation set picks between them on the seat's role tag, which is the whole of what makes a driver sit differently from the passenger beside them. - The hands are IK'd onto the grips, not gripping. They reach the rim and follow it; the wrists do not turn with it, because that needs the grip's frame authored against this hand bone's, and a wrong guess there reads worse than a still wrist.
- The example holds the pose on the mesh itself, not through the slot. The framework plays a
seated pose into the animation set's slot, which is right for a project with its own anim graph.
This example does not have one: it borrows
ABP_Unarmedfrom the engine template, and that graph'sDefaultSlotnode is not wired through to the output pose. The montage plays,Is Slot Activereturns true andGet Slot Montage Local Weightreturns 1.0, and the skeleton does not move — every number says it is working. So the character asksResolveSeatedPosewhat the framework wanted and applies it in single-node mode for as long as it is seated. Your project should not need that. - The steering wheel is a component, not a Control Rig. The sports car mesh has no steering-wheel bone to drive. A production vehicle would rig one and feed it exactly the value this reads.
- Everything is boxes. The car, the horse and the turret are all engine primitives wearing a generated material with some grain in it. Only the wheels are imported, because a round thing made of boxes is not a wheel. The example spends its complexity on the mounting rather than on art.
Tests
UnrealEditor-Cmd.exe MountingAndVehicles.uproject \
-ExecCmds="Automation RunTests EDMounting;Quit" -unattended -nopause -nosplash
Twelve tests. Five are pure; seven build a world in code and exercise seat registration, the mount and dismount round trip over real ticked transitions, the same rider getting back in afterwards, the occupancy race, reach, access rules, seat changes and the interface defaults. None needs content or a map.