Skip to content

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:

PrefabWhat it does
Race CarA ready-to-drive car (chassis + body + cabin + 4 wheel entities, tuned). Drag it in and press Play.
Race CheckpointA translucent gate with a RaceCheckpoint script. Give each gate an increasing Order (1 = start/finish line).
Lap TimerDrop ONE anywhere. Set Total Checkpoints to the number of gates — it shows lap count, live lap time, and best lap on the HUD.
Boost PadShoves 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[].connection is the chassis-local suspension anchor. Front wheels get steering: true, rear wheels driven: true (any mix works — 4WD is all four driven).
  • wheels[].entityId points 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: true on 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.
  • seatOffset is where the driver sits (chassis-local); exitOffset is where they pop out; enterRadius is 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

InputAction
EEnter the nearest car in range / exit the seat
W / SThrottle / reverse (reverse doubles as the brake)
A / DSteer
SpaceHandbrake — 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.