Appearance
Vehicles & driving
Galatrix has a built-in drivable-vehicle system: a vehicle component on a dynamic chassis turns it into a raycast car — each wheel is a suspension ray (no wheel bodies, which is what keeps cars stable), front wheels steer, driven wheels take engine force. Players walk up to a car and press E to drive: WASD steers, Space is the handbrake, E again exits at the door. Cars work in the editor play-test, standalone builds, and multiplayer (server-simulated: the server owns the seat and the physics; other players see the car move and its front wheels steer).
The quick way: the Vehicle Kit
Open Packs → Built-in Kits → Vehicle Kit → Import. You get:
| Prefab | What it does |
|---|---|
| Race Car | A ready-to-drive car (chassis + body + cabin + 4 wheel entities, tuned). Drag it in and press Play. |
| Race Checkpoint | A translucent gate with a RaceCheckpoint script. Give each gate an increasing Order (1 = start/finish line). |
| Lap Timer | Drop ONE anywhere. Set Total Checkpoints to the number of gates — it shows lap count, live lap time, and best lap on the HUD. |
| Boost Pad | Shoves any vehicle that drives over it along its facing (physics.applyImpulse). |
There is also a full example map: open the editor with ?example=racetrack — an oval circuit with barriers, three gates, boost pads, and a parked race car.
The vehicle component
Put vehicle on an entity that also has a dynamic rigidBody and a collider (the chassis):
json
{
"rigidBody": { "type": "dynamic", "mass": 150, "ccd": true },
"collider": { "shape": "box", "size": [1.9, 0.6, 3.8] },
"vehicle": {
"wheels": [
{ "connection": [-0.85, -0.2, 1.35], "radius": 0.35, "steering": true, "entityId": "wheel-fl" },
{ "connection": [0.85, -0.2, 1.35], "radius": 0.35, "steering": true, "entityId": "wheel-fr" },
{ "connection": [-0.85, -0.2, -1.35], "radius": 0.35, "driven": true, "entityId": "wheel-rl" },
{ "connection": [0.85, -0.2, -1.35], "radius": 0.35, "driven": true, "entityId": "wheel-rr" }
],
"engineForce": 900,
"brakeForce": 600,
"maxSteerAngleDeg": 28,
"seatOffset": [0, 0.85, 0],
"exitOffset": [2.3, 0.6, 0],
"enterRadius": 3.5
}
}- Forward is +Z (the entity's
transform.forward), matching everything else in Galatrix. wheels[].connectionis the chassis-local suspension anchor. Front wheels getsteering: true, rear wheelsdriven: true(any mix works — 4WD is all fourdriven).wheels[].entityIdpoints at an optional child entity used as the wheel VISUAL — the engine yaws it with live steering and spins it with speed. Author it as a cylinder rotated 90° about Z so it lies like a tire.- Children of the chassis get no physics of their own. They are pure visuals that ride along — body panels, a cabin, the wheels. (Giving them colliders would freeze copies of the car in the world; the engine prevents that.)
ccd: trueon the rigidBody is recommended: a car at speed crosses more than a meter per physics step and would tunnel through thin walls without continuous collision detection.seatOffsetis where the driver sits (chassis-local);exitOffsetis where they pop out;enterRadiusis how close a player must be for the E prompt.
Suspension fields per wheel (suspensionRestLength, suspensionStiffness, suspensionDamping, frictionSlip) are optional — defaults are tuned for a ~150 kg car and suspension values are chassis-mass-normalized, so they hold across weights. engineForce/brakeForce are absolute newtons: scale them with mass if you build a heavy truck.
Cars ride the same at any world gravity. You do not need to lower your map's gravity for a car to feel right — the engine caps the chassis's effective gravity at the value the suspension is tuned for (a per-body gravity scale), so the car settles and drives identically whether the map runs the default snappy -50, a realistic -9.81, or moon gravity, while the player and loose props still feel the map's real gravity. (Lowering a map's gravity for the car's sake would only make on-foot jumps float.)
Driving
| Input | Action |
|---|---|
| E | Enter the nearest car in range / exit the seat |
| W / S | Throttle / reverse (reverse doubles as the brake) |
| A / D | Steer |
| Space | Handbrake — locks the rear wheels and lets the tail slide (steer into it to drift) |
Steering is speed-sensitive: the lock is full at low speed (tight turns and parking) and eases off as the car speeds up, so a fast car carves smooth, planted arcs instead of spinning out on a tap. maxSteerAngleDeg sets the low-speed lock; the high-speed easing is automatic.
A light grip assist also keeps the car from spinning when you lift off the throttle mid-corner (trailing-throttle oversteer) — it only pulls the tail back once it slides past a normal cornering angle, so ordinary grippy turns and a little drift are untouched. It's on by default (VEHICLE_TUNINGgripAssist / gripSlipDeadzone); set gripAssist to 0 for a looser, driftier car.
While seated, the driver's avatar rides at the seat, the chase camera follows the car, and zones / checkpoints keep firing for the driver (so damage zones, kill planes, and race gates all work from the driver's position). Dying at the wheel exits the car; the car keeps rolling to a stop.
Reacting to cars from scripts
A driven car arrives at triggers as its chassis entity (the driver's own capsule is ghosted while seated), so a gate script listens to both players on foot and cars:
js
class RaceCheckpoint extends Behaviour {
order = 1;
_fire(byId) { this.events.broadcast('race:checkpoint', { order: this.order, by: byId }); }
onPlayerEnter(player) { this._fire(player ? player.id : ''); } // on foot
onTriggerEnter(other) { if (other && !other.compareTag('Player')) this._fire(other.id); } // a car
}Boost or shove a car with the physics API — other.forward is the car's facing:
js
class BoostPad extends Behaviour {
impulse = 1200; // ~ +8 m/s on the default 150 kg car
onTriggerEnter(other) {
if (!other || other.compareTag('Player')) return;
const f = other.forward;
this.physics.applyImpulse(other.id, [f[0] * this.impulse, 0, f[2] * this.impulse]);
}
}Spawning a car from a script works like any prefab — the whole tree (body, cabin, wheels) comes with it and the controller builds automatically:
js
const car = this.game.instantiate('Race Car', [10, 2, 0]);Scripting your own controller (the vehicles API)
The whole driving feel can live in a map script instead of the engine. Set scriptedControl: true on the vehicle (Inspector → Vehicle, or the component) and the built-in WASD steps aside — your script owns the controls through the vehicles API:
js
class MyDriver extends Behaviour {
onUpdate(dt) {
const car = this.entity.id;
const throttle = (input.getKey('w') ? 1 : 0) - (input.getKey('s') ? 1 : 0);
const steer = (input.getKey('a') ? 1 : 0) - (input.getKey('d') ? 1 : 0); // A = left
vehicles.setInput(car, { throttle, steer, handbrake: input.getKey(' ') }); // Space = handbrake
// read state + retune live:
const speed = vehicles.getSpeed(car); // signed m/s
if (!vehicles.isGrounded(car)) { /* airborne */ }
vehicles.setConfig(car, { engineForce: 1200 }); // any handling param, live
}
}vehicles.*: setInput / setThrottle / setSteer / setBrake / setHandbrake to drive, getSpeed / isGrounded to read state, getConfig / setConfig to read + retune any handling param at runtime. Works in the editor, standalone, and multiplayer (the server runs it authoritatively). The engine still gives you the raycast-car physics, seating, and networking — you just own the controller.
The Custom Vehicle Controller built-in kit (Packs → Built-in Kits) is a ready-to-edit starting point: a scriptedControl car + a VehicleController script that does input → drive + chase camera. Import it, open the script, rewrite the driving. (There's a matching Custom Character Controller kit for the player, built on the characterController primitive + characterController.move().)
Camera control (the chase cam)
The camera is fully scriptable — camera.setPosition, camera.setRotation, camera.lookAt, camera.setFov. A script's camera calls run after the engine's default camera each frame, so they override it: set the camera every frame in onUpdate and it sticks; stop setting it and the default camera returns. Detect who's driving with players.getVehicle(playerId) — it returns the entity id of the car a player is driving, or null on foot (client-only).
The Vehicle Kit ships a ChaseCam script (attached to the Race Car) that keeps the camera behind the car as it drives and turns. It's a normal map script — open it and edit distance / height / smoothing / lookHeight, or delete it to fall back to the default orbit camera:
js
class ChaseCam extends Behaviour {
distance = 8; height = 3.2; smoothing = 6; lookHeight = 1.0;
_cx = null; _cy = 0; _cz = 0;
onUpdate(dt) {
const me = this.players.getLocal ? this.players.getLocal() : null;
const driving = (me && this.players.getVehicle) ? this.players.getVehicle(me.id) : null;
if (driving !== this.entity.id) { this._cx = null; return; } // on foot → default camera
const p = this.entity.position, f = this.entity.forward;
// entity.forward points out the car's BACK (a vehicle drives toward +Z = -forward), so ADD it = behind.
const want = [p[0] + f[0]*this.distance, p[1] + this.height, p[2] + f[2]*this.distance];
// Smooth OUR OWN stored position — the default camera rewrites the camera before this runs each
// frame, so easing from camera.getPosition() would never converge.
if (this._cx === null) { this._cx = want[0]; this._cy = want[1]; this._cz = want[2]; }
const t = Math.min(this.smoothing * dt, 1);
this._cx += (want[0]-this._cx)*t; this._cy += (want[1]-this._cy)*t; this._cz += (want[2]-this._cz)*t;
this.camera.setPosition([this._cx, this._cy, this._cz]);
this.camera.lookAt([p[0], p[1] + this.lookHeight, p[2]]);
}
}Two gotchas the script comments call out: entity.forward is THREE's −Z (it points out the car's back, since a vehicle drives +Z), and because the default camera overwrites the camera every frame, a smoothed follow must keep its own position rather than easing from camera.getPosition(). Camera scripts run on the client (editor play-test + standalone) — the multiplayer server has no camera, so a chase cam there is a no-op (each player runs their own camera locally).
Tips for tracks
- Make checkpoint gates deep: give a thin glowing pane a collider like
{"size": [1, 1, 6], "isTrigger": true}— at speed a car can cross several meters between physics steps, and a paper-thin sensor gets skipped. - Solid barrier walls keep racers on the circuit; make them at least ~1.5 m tall or a fast crash can vault them.
- The lap timer in the kit tracks one racer (the local play-test / solo session) — per-player lap boards for multiplayer races are on the roadmap.