Racing module
defineRaceGame(config) makes a full-screen arcade race on a closed track: the player's car against up to six AI rivals, for a number of laps. Use it for any car racing game: circuits, ovals, karts, night races, desert rallies. The starter game every new repo ships with, Sunset Sprint, is this module.
import { defineRaceGame, TRACK_PRESETS } from '@/engine/modules/racing';
import type { RaceConfig } from '@/engine/modules/racing';
Source: templates/crescent-game/src/engine/modules/racing/.
The smallest complete game
This is Sunset Sprint, the starter game in templates/crescent-game/src/game/index.tsx (the file there also sets a badge):
// src/game/index.tsx
import { defineRaceGame } from '@/engine/modules/racing';
export default defineRaceGame({
id: 'sunset-sprint',
title: 'Sunset Sprint',
description: 'Three laps around a coastal circuit as the sun goes down.',
laps: 3,
track: { preset: 'circuit', width: 12, seed: 11 },
car: { color: '#7C5CFF', accent: '#2EE6D6', maxSpeedKmh: 160, acceleration: 1.1, handling: 1.1 },
opponents: { count: 3, speed: 0.8 },
theme: { timeOfDay: 'sunset', scenery: 'trees', fog: true },
});
Only id, title and description are required. Everything else has a default.
What the player gets
- A 3D track built from the preset or your points: road, edge lines, a dashed centre line, alternating barriers and scenery placed by
track.seed. - A "3, 2, 1, GO!" countdown, during which nobody moves.
- The player's car, starting last on the grid, and AI rivals that follow the racing line, lift for corners and brake rather than drive through the player.
- Laps that count only when the car has passed three checkpoints (at a quarter, half and three quarters of the lap). Reversing over the line clears them, so the line cannot be farmed.
- A HUD: lap (
LAP 1/3), position, race timer, a speed gauge, a mute button and an exit button. - A finish card with the position, the total time, the best lap, Race again and Back to games.
- Engine sounds that follow the speed, tyre squeal, braking, bumps, a whoosh when a rival passes, and a music track.
- Lighting, sky and fog from
theme.timeOfDay, with car lights at night.
Controls are the engine's TouchControls: steer left and right on one side, gas and brake on the other. In a desktop browser the same actions are on the keyboard: A / D or the left and right arrows steer, W or the up arrow is the gas, S, the down arrow or Space brake. See Input.
Config reference
Values outside a range are clamped. Speeds and ratios are multipliers unless stated.
Top level
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | required | URL-safe, kebab-case id. |
title | string | required | The game's name, shown in the HUD. |
description | string | required | One sentence for the game's card. |
badge | string | a race car emoji | Card metadata; the race never shows it. |
laps | number | 3 | Laps to finish, 1 to 10. |
track | object | see below | The track shape and size. |
car | object | see below | The player's car. |
controls | object | see below | The control layout. |
opponents | object | see below | The AI rivals. |
theme | object | see below | Colours, scenery, light and post-processing. |
track
| Field | Type | Default | Meaning |
|---|---|---|---|
preset | 'oval' | 'figure-eight' | 'hairpin' | 'circuit' | 'speedway' | 'circuit' | A built-in shape. |
points | [number, number][] | none | Your own closed loop as [x, z] in metres. Needs 4 or more points, otherwise the preset is used. Overrides preset. |
width | number | 11 | Road width in metres, 8 to 16. |
seed | number | 7 | Seed for scenery placement. |
The presets, with their approximate size:
| Preset | Size | Character |
|---|---|---|
oval | 140 x 84 m | fast and simple |
speedway | 240 x 120 m | very fast, long straights |
figure-eight | about 160 m across | crosses itself in the middle |
hairpin | about 225 x 55 m | two long straights, tight ends |
circuit | about 205 x 120 m | varied corners, the default |
TRACK_PRESETS exports the point lists, so you can start from one and edit it.
car
| Field | Type | Default | Meaning |
|---|---|---|---|
color | CSS colour | '#7C5CFF' | Body colour. |
accent | CSS colour | '#2EE6D6' | Cabin colour. Also the HUD accent, the Race again button and the game's accent. |
maxSpeedKmh | number | 150 | Top speed in km/h, 80 to 220. |
acceleration | number | 1 | Engine force, 0.5 (sluggish) to 2 (rocket). |
handling | number | 1 | Steering response, 0.5 (boat) to 2 (go-kart). Higher also means less understeer. |
steeringAssist | number | 0.12 | 0 to 1. Gently steers toward the racing line while the player is not steering. 0 is none, 0.5 and up is very easy. |
controls
| Field | Type | Default | Meaning |
|---|---|---|---|
layout | 'classic' | 'mirrored' | 'classic' | classic puts steering on the left and pedals on the right; mirrored swaps them. |
opponents
| Field | Type | Default | Meaning |
|---|---|---|---|
count | number | 3 | AI cars, 0 to 6. |
colors | string[] | six bright colours | Body colours, cycled when shorter than count. |
speed | number | 0.62 | Pace as a fraction of the player's top speed, 0.4 to 1.1. 0.62 is beatable, 0.75 a fair fight, 0.9 hard. Each rival varies by a few percent so the pack spreads out. |
theme
| Field | Type | Default | Meaning |
|---|---|---|---|
timeOfDay | 'day' | 'sunset' | 'night' | 'day' | Picks the lighting and the default sky, ground, road and line colours. Night turns on car lights. |
sky | CSS colour | per time of day | Sky and fog colour. |
ground | CSS colour | per time of day | Ground colour. |
road | CSS colour | per time of day | Road colour. |
line | CSS colour | per time of day | Edge line colour. |
barrier | [string, string] | ['#E63946', '#F8F9FA'] | Alternating barrier colours. |
scenery | 'trees' | 'rocks' | 'cacti' | 'pillars' | 'none' | 'trees' | What stands around the track. |
sceneryColor | CSS colour | '#2F7D4A' ('#1F4D3A' at night) | Scenery colour. |
fog | boolean | true | Distance fog in the sky colour. |
postfx | 'glow' | 'cinematic' | 'retro' | 'off' | 'off' | A post-processing look. glow makes lights and neon bloom (night and arcade races), cinematic is moodier, retro punchier. |
A custom track
import { defineRaceGame } from '@/engine/modules/racing';
export default defineRaceGame({
id: 'square-loop',
title: 'Square Loop',
description: 'A rounded square at night with neon barriers.',
track: { points: [[-70, -70], [0, -85], [70, -70], [85, 0], [70, 70], [0, 85], [-70, 70], [-85, 0]], width: 12 },
theme: { timeOfDay: 'night', scenery: 'pillars', barrier: ['#FF2BD6', '#1B1E3A'], postfx: 'glow' },
controls: { layout: 'mirrored' },
});
The loop closes itself and a spline smooths the corners. Shapes that drive well:
- 6 to 20 points, roughly 150 to 400 m across.
- Consecutive points 15 to 60 m apart.
- No turn sharper than about 100 degrees.
- The loop does not cross itself unless you want a crossing.
How the car feels
An arcade bicycle model. Leaving the road slows the car. Barriers are a hard wall: the car scrubs speed and slides along them, and never crosses. Cars collide: the player is pushed out of a rival and loses speed, more for a head-on hit.
What you can customise
| What | How |
|---|---|
| Look | theme colours, scenery, timeOfDay, postfx, car.color and accent, opponents.colors. |
| Difficulty | opponents.count and speed, laps, car.steeringAssist, track.width. |
| Feel | car.maxSpeedKmh, acceleration, handling. |
| Sounds | The module plays fixed files: assets/audio/engine-idle.wav, engine-high.wav, squeal.wav, accelerate.m4a, brake.m4a, bump.m4a, passby.m4a and music-synthwave.m4a. Replace a file to change its sound. There is no sounds field. |
| Car model | The cars are a procedural low-poly model; there is no field for a .glb. |
| HUD, rules, anything else | Edit the game's own copy of the module in src/engine/modules/racing/ (RaceGame.tsx is the screen, RaceHud.tsx the HUD, raceState.ts the lap rules, vehicle.ts the physics). Each game repo carries its own engine copy. Or write your own race from the engine pieces: TouchControls, Environment, RaceAudio and the path utilities are exported from @/engine. |
Backend
The racing module uses no backend: no leaderboard, no rooms, no saves. To put a race online, add the calls yourself in your copy of RaceGame.tsx:
- Best times:
useLeaderboardwithorder: 'asc', submitting milliseconds, one board per track. Online, the backend'sBOARDStable decides the order; a family such as"lap-*": { order: "asc", min: 5_000, max: 3_600_000 }covers every track. - Live races:
useRoom, sending the car's pose and drawing the other cars from the room. - Unlocked cars and tracks: saves and progression.
See Networking and the backend overview.
Dev and testing
In development builds (__DEV__) the HUD shows a telemetry line (testID hud-debug: inputs, speed, position on the track, collisions, sound counters, touches) and an AUTO button (btn-autopilot) that drives the car by itself. Production builds hide both.
Other testIDs: btn-exit, btn-mute, hud-lap, hud-position, hud-countdown, hud-speed, hud-finished, btn-restart, btn-finish-exit, and game-<id> on the root view.
Gotchas
- A custom
pointslist with fewer than 4 points is ignored without a warning; the preset is used. - Clamping is silent:
laps: 20plays 10 laps. opponents.speedabove 0.9 makes rivals very hard to beat; 1.1 is unfair on purpose.- The
badgeis card metadata. Keep it out of the game's own UI. - Race again remounts the 3D canvas, so anything you add to the scene restarts with it.