Accelerate
Accelerate
Brake / Reverse
Brake / Reverse
Restart
const renderLayers = {
foreground: 1 << 0,
};
const gravity = new Vector2(0, -600);
export const createCarGame = async (): Promise<Game> => {
const { game, world, renderContext, time } = createGame('demo-game');
// `isStatic: true` since this camera's position is driven by
// `createCameraFollowEcsSystem` rather than `createCameraEcsSystem`'s
// input-driven pan/zoom.
const cameraEntity = createCamera(world, {
isStatic: true,
zoom: 0.5,
cullingMask: renderLayers.foreground,
clearColor: new Color(0.6, 0.6, 0.8),
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});
const physicsWorld = new PhysicsWorld({ gravity });
const random = new Random('car');
const { throttleInput, restartInput } = createInputs(world, time);
const groundPosition = await createTerrain(
world,
renderContext,
renderLayers.foreground,
random,
);
const chassisBody = await createCar(
world,
renderContext,
renderLayers.foreground,
groundPosition,
throttleInput,
restartInput,
);
addCameraFollowComponent(world, cameraEntity, {
target: chassisBody,
offset: new Vector2(140, 70),
smoothTime: 0.25,
maxSpeed: 3000,
});
// `createCarResetEcsSystem` may teleport every body back to its spawn
// transform, `createPrismaticJointEcsSystem` / `createRevoluteJointEcsSystem`
// register each wheel mount's joints (see `createWheelMount`) with
// `physicsWorld`, and `createGroundContactEcsSystem` refreshes each
// wheel's grounded state from `physicsWorld.collisionStarts`/
// `collisionEnds` (one tick stale, the same lag any contact-based "am I
// grounded" check in a fixed-step engine has). `createWheelDriveEcsSystem`
// (sets each wheel's motor target from `throttleInput`, but only requests
// full speed while that wheel's own ground contact says it's grounded) /
// `createAngularVelocityMotorEcsSystem` / `createLinearSpringEcsSystem` /
// `createLinearDamperEcsSystem` / `createChassisStabilizerEcsSystem` /
// `createAirControlEcsSystem` compute this tick's torque/forces from the
// (possibly just-changed) state above - all ten must run before
// `createPhysicsSyncEcsSystem`, which is what steps `physicsWorld` (see
// the Applying Forces guide's registration-order caution).
// `createWheelDriveEcsSystem`/`createChassisStabilizerEcsSystem`/
// `createAirControlEcsSystem` must also run after
// `createGroundContactEcsSystem` in this same list, so they see this
// tick's grounded state rather than last tick's. `createCameraFollowEcsSystem`
// only needs to run before `createRenderEcsSystem`, so this tick's camera
// position is reflected in this tick's render.
world.addSystem(createCarResetEcsSystem());
world.addSystem(createPrismaticJointEcsSystem(physicsWorld));
world.addSystem(createRevoluteJointEcsSystem(physicsWorld));
world.addSystem(createGroundContactEcsSystem(physicsWorld));
world.addSystem(createWheelDriveEcsSystem());
world.addSystem(createAngularVelocityMotorEcsSystem(time));
world.addSystem(createLinearSpringEcsSystem(time));
world.addSystem(createLinearDamperEcsSystem(time));
world.addSystem(createChassisStabilizerEcsSystem(time));
world.addSystem(createAirControlEcsSystem(time));
world.addSystem(createCameraFollowEcsSystem(time));
world.addSystem(createCameraEcsSystem(time));
world.addSystem(createRenderEcsSystem(renderContext));
world.addSystem(createPhysicsSyncEcsSystem(physicsWorld, time));
return game;
};
This demo composes several of the physics engine's existing primitives into a drivable car: the chassis, both wheels, and a small invisible 'upright' body per wheel (a wheel hub/knuckle) are all independent RigidBody instances, and each wheel is driven by an AngularVelocityMotorEcsComponent (see the Torque and Motors demo). A demo-only GroundContactEcsComponent/system pair, attached to each wheel's own entity, tracks how many ground columns that wheel is touching from PhysicsWorld's collisionStarts/collisionEnds. While a wheel is grounded, its motor's target speed tracks the throttle input directly, and the engine's own Coulomb friction model - not an artificial clamp - is what correctly limits how much of that becomes real acceleration versus slip, the same way it would for a real tire. Only while a wheel is airborne does WheelDriveEcsSystem clamp its target to a bounded slip band around its current rolling speed (derived from the chassis's own velocity every tick): airborne, a wheel has nothing but its own rotational inertia to resist the motor, so left unclamped it would accelerate towards an arbitrary top speed almost instantly and land spinning far faster than the car is actually moving, burning torque on wheel spin instead of quickly regaining grip. Each wheel mounts to the chassis through its upright: a PrismaticJoint (see the Prismatic Joint demo) constrains the upright to slide only vertically relative to the chassis, a RevoluteJoint (see the Revolute Joint demo) pins the wheel's position to that upright while leaving its rotation completely free to spin, and a LinearSpring/LinearDamper pair (see the Linear Spring and Damper demo) along that same axis supplies the suspension's force. Wiring either joint straight to the wheel wouldn't work - a RevoluteJoint would pin it rigidly in place with no suspension travel, and a PrismaticJoint locks relative rotation, which would lock the wheel's spin to the chassis - so the upright exists specifically to give the wheel a rotation-free attachment point. Unlike a spring-only mount, both joints are hard, iteratively-solved constraints with no lateral give, so the wheel only ever travels along that one axis, no swinging or sideways slop. Because the mount's *force* still comes from a soft spring rather than a rigid frame, accelerating and braking visibly pitches the chassis backward and forward, the 'leaning' feel the genre is named for; a light demo-only ChassisStabilizerEcsComponent nudges the chassis back level once nothing else is actively tipping it, without fighting a deliberate acceleration or brake lean. Each wheel mount's PrismaticJoint axis is also tilted outward rather than straight up/down, so the line from each chassis anchor to its wheel splays into a trapezoid instead of a rectangle, like a monster truck's lifted suspension - besides the visual, this gives an impact that's perpendicular to the chassis (hitting a ledge face-on) a component along the suspension axis for the spring to absorb, rather than landing entirely on the joint's hard, unsprung constraint. The terrain is a row of static, unrotated ground columns following a procedurally generated height profile, and a demo-only camera-follow system (the engine's built-in camera only supports input-driven pan/zoom) keeps the car in view as it drives across a course much wider than the canvas. ChassisStabilizerEcsComponent and AirControlEcsComponent, which both live on the chassis rather than either wheel, hold direct references to both wheels' GroundContactEcsComponent objects to check grounded state - ChassisStabilizerEcsSystem stays out of the way entirely while both wheels are airborne, and in that same situation AirControlEcsSystem instead drives the chassis's angular velocity towards a target proportional to the throttle input (the same targetVelocity/maxTorque approach AngularVelocityMotorEcsComponent uses for the wheels) - gas targets a nose-up-and-back spin, brake targets nose-down-and-forward, and releasing the input targets zero rotation, actively cancelling existing spin rather than just coasting on momentum, the classic mid-air control for lining up a landing. Letting go of the throttle on the ground also drops the driven wheel's motor torque budget to zero rather than braking it to a stop, so the car coasts and freewheels downhill under gravity instead of being held by an invisible parking brake. The hills get steep further along the course - if the car flips or gets stuck, press R to restart.