How the Lap Leaderboard and Champion Car System Works
I added the leaderboard as an afterthought. "People are racing together — they should see who's winning." That five-minute feature became the reason people keep playing.
The leaderboard
Every player in an online room — you plus up to 29 ghosts — gets ranked by their best lap time on the current circuit. The code lives in net.gd:
func leaderboard() -> Array:
var out: Array = []
out.append({"name": GameState.player_name, "bl": GameState.best_lap_for(GameState.current_map), "me": true})
for id in remotes:
out.append({"name": str(remotes[id].name), "bl": float(remotes[id].get("bl", 0.0)), "me": false})
out.sort_custom(func(a, b):
var ax: float = a.bl if a.bl > 0.0 else INF
var bx: float = b.bl if b.bl > 0.0 else INF
return ax < bx)
return outYour best lap comes from GameState. Everyone else's comes from the data they broadcast over the relay. Players who haven't completed a lap yet sort to the bottom (their best lap is treated as infinity).
The HUD renders the top 5 in a panel directly below the minimap:
func _draw_leaderboard(pos: Vector2, accent: Color) -> void:
var lb: Array = Net.leaderboard()
var rows := mini(lb.size(), 5)
...
for i in range(lb.size()):
if shown >= 5:
break
var e: Dictionary = lb[i]
var me: bool = e.me
var col := Color(0.75, 0.85, 0.95)
if me:
col = Color.WHITE
if i == 0 and float(e.bl) > 0.0:
col = Color(1.0, 0.85, 0.2) # gold for the room leaderFirst place is gold. Your own entry is white. Everyone else is light blue-grey. Simple visual hierarchy, instantly readable at a glance while you're racing.
The leaderboard only appears when online_players > 1. If you're alone, it hides. No point showing a leaderboard where you're the only entry.
The Champion Gold car
Hold first place in a room where at least one other racer has posted a lap time. That's it. That's the condition.
func room_leader_is_me() -> bool:
var lb := leaderboard()
if lb.size() < 2:
return false
return bool(lb[0].me) and float(lb[0].bl) > 0.0When the world detects you're the room leader, it calls GameState.award_champion():
func award_champion() -> void:
if owned.has("champion"):
return
owned.append("champion")
garage_changed.emit()
save_game()One-shot. Once you have it, you have it forever. It's the only car in the game that can't be bought with money. You can only earn it by racing faster than someone else.
This creates this wonderful dynamic where a new player joins a room, sees someone in a gold car, and thinks "I want that." They race harder. They learn to drift. They figure out the boost loop. And eventually, in some room on some track, they post the fastest lap and earn it themselves.
The economy
Money is earned by racing. Every gate you pass through pays $25. Every completed lap pays $200.
const GATE_REWARD := 25
const LAP_REWARD := 200
func award_gate() -> void:
total_gates += 1
add_money(GATE_REWARD)
func award_lap() -> void:
total_laps += 1
add_money(LAP_REWARD)Ten gates per lap means $250 from gates plus $200 for the lap = $450 per lap. A good player can do a lap in 40–60 seconds, so you're earning roughly $400–600 per minute. Cars cost between $0 (the starter kart) and several thousand. It's not grindy — you can buy a mid-tier car in 10 minutes of racing. But the premium cars take commitment.
Persistence: ConfigFile
All of this saves to a ConfigFile at user://olivers_racers_save.cfg:
func save_game() -> void:
var cf := ConfigFile.new()
cf.set_value("player", "money", money)
cf.set_value("player", "owned", owned)
cf.set_value("player", "equipped", equipped)
cf.set_value("player", "total_gates", total_gates)
cf.set_value("player", "total_laps", total_laps)
cf.set_value("player", "quests_done", quests_done)
cf.set_value("player", "current_map", current_map)
cf.set_value("player", "best_laps", best_laps)
cf.set_value("player", "unlocked_maps", unlocked_maps)
cf.set_value("player", "name", player_name)
cf.save(SAVE_PATH)I save after every transaction. Not on quit, not on a timer — every time money changes, every time you buy a car, every time you set a personal best. On web, this is critical because there's no reliable "app is closing" event. If the user closes the tab, _notification(NOTIFICATION_WM_CLOSE_REQUEST) never fires. But the ConfigFile was already saved two seconds ago when they passed through gate 7. Nothing is lost.
Defensive loading
The load function is paranoid. It validates every saved ID against the current catalog:
for id in owned:
if id is String and CarCatalog.has(id) and not clean.has(id):
clean.append(id)
if not clean.has(CarCatalog.STARTER):
clean.append(CarCatalog.STARTER)If a future update removes a car, old saves don't crash — the invalid ID gets silently dropped and the starter car is guaranteed. If the current map was deleted or locked, it falls back to the default. If the best-laps dictionary has a key for a map that no longer exists, it's skipped.
Save files are a trust boundary. I treat them like user input: assume nothing is valid until proven otherwise.
Written by Oliver
Indie game developer building 3D web experiences in Godot and Next.js. I write about creative coding, procedural generation, and game feel.