Skip to main content

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​

FieldTypeDefaultMeaning
idstringrequiredURL-safe, kebab-case id.
titlestringrequiredThe game's name, shown in the HUD.
descriptionstringrequiredOne sentence for the game's card.
badgestringa race car emojiCard metadata; the race never shows it.
lapsnumber3Laps to finish, 1 to 10.
trackobjectsee belowThe track shape and size.
carobjectsee belowThe player's car.
controlsobjectsee belowThe control layout.
opponentsobjectsee belowThe AI rivals.
themeobjectsee belowColours, scenery, light and post-processing.

track​

FieldTypeDefaultMeaning
preset'oval' | 'figure-eight' | 'hairpin' | 'circuit' | 'speedway''circuit'A built-in shape.
points[number, number][]noneYour own closed loop as [x, z] in metres. Needs 4 or more points, otherwise the preset is used. Overrides preset.
widthnumber11Road width in metres, 8 to 16.
seednumber7Seed for scenery placement.

The presets, with their approximate size:

PresetSizeCharacter
oval140 x 84 mfast and simple
speedway240 x 120 mvery fast, long straights
figure-eightabout 160 m acrosscrosses itself in the middle
hairpinabout 225 x 55 mtwo long straights, tight ends
circuitabout 205 x 120 mvaried corners, the default

TRACK_PRESETS exports the point lists, so you can start from one and edit it.

car​

FieldTypeDefaultMeaning
colorCSS colour'#7C5CFF'Body colour.
accentCSS colour'#2EE6D6'Cabin colour. Also the HUD accent, the Race again button and the game's accent.
maxSpeedKmhnumber150Top speed in km/h, 80 to 220.
accelerationnumber1Engine force, 0.5 (sluggish) to 2 (rocket).
handlingnumber1Steering response, 0.5 (boat) to 2 (go-kart). Higher also means less understeer.
steeringAssistnumber0.120 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​

FieldTypeDefaultMeaning
layout'classic' | 'mirrored''classic'classic puts steering on the left and pedals on the right; mirrored swaps them.

opponents​

FieldTypeDefaultMeaning
countnumber3AI cars, 0 to 6.
colorsstring[]six bright coloursBody colours, cycled when shorter than count.
speednumber0.62Pace 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​

FieldTypeDefaultMeaning
timeOfDay'day' | 'sunset' | 'night''day'Picks the lighting and the default sky, ground, road and line colours. Night turns on car lights.
skyCSS colourper time of daySky and fog colour.
groundCSS colourper time of dayGround colour.
roadCSS colourper time of dayRoad colour.
lineCSS colourper time of dayEdge line colour.
barrier[string, string]['#E63946', '#F8F9FA']Alternating barrier colours.
scenery'trees' | 'rocks' | 'cacti' | 'pillars' | 'none''trees'What stands around the track.
sceneryColorCSS colour'#2F7D4A' ('#1F4D3A' at night)Scenery colour.
fogbooleantrueDistance 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​

WhatHow
Looktheme colours, scenery, timeOfDay, postfx, car.color and accent, opponents.colors.
Difficultyopponents.count and speed, laps, car.steeringAssist, track.width.
Feelcar.maxSpeedKmh, acceleration, handling.
SoundsThe 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 modelThe cars are a procedural low-poly model; there is no field for a .glb.
HUD, rules, anything elseEdit 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: useLeaderboard with order: 'asc', submitting milliseconds, one board per track. Online, the backend's BOARDS table 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 points list with fewer than 4 points is ignored without a warning; the preset is used.
  • Clamping is silent: laps: 20 plays 10 laps.
  • opponents.speed above 0.9 makes rivals very hard to beat; 1.1 is unfair on purpose.
  • The badge is 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.