Mario Kart Taught Me to Let Players Wait
it worked on the first try, and it was boring
I added power-ups to Oliver's Racers the lazy way first. Boosters float along the track, you drive through one, you get an instant shove of speed. That's it. It worked the moment I ran it, which in hindsight should have made me suspicious.
It was fine. It was also completely forgettable. You'd catch a booster on some straight where you were already going fast, waste the whole thing, and not feel anything. The power-ups were just sort of happening at you. You weren't doing anything with them.
I couldn't put my finger on why it felt so flat until I thought about Mario Kart, which I played an absurd amount of as a kid. And the thing about Mario Kart items is you don't fire them the instant you get them. You sit on them. You drag a green shell around for half a lap waiting for the exact second someone tries to pass you. The waiting IS the fun part. That was the whole thing my version was missing.
grab and use are two different buttons
So I tore it up and rebuilt it around one rule: picking up an item and using an item are separate actions. You drive over a box, it hands you something, and that something goes into a slot and just sits there until you decide to spend it.
That tiny change fixed everything. Now you're hauling a rocket up the last hill going "do I fire it now or hold it for the line?" — and that little argument with yourself is the entire point. It did not exist in the instant version. There was nothing to decide.
Here's the state. It's almost nothing:
var held_item := "" # what you're holding
var held_charges := 0 # a couple of items get more than one use
func take_item(id: String) -> bool:
if held_item != "":
return false # already holding something — spend it first
held_item = id
held_charges = ItemCatalog.charges_of(id)
item_changed.emit()
save_game()
return trueThat return false when the slot's full is quietly doing most of the design work. You can only hold one thing. You can't stockpile five rockets and empty them all at the finish line. Want a new item? You have to burn the one you've got first. So every box you pass while you're already holding something is a tiny "use it or lose it" poke in the ribs.
make the box a surprise
The other thing I changed was making the boxes hand you something random. In the first version a booster was always a booster, which is exactly as exciting as it sounds. Now you don't know what's in there. Could be a plain shove, could be a triple, could be a rocket that flings you forward, a star that welds you to the road so you stop spinning out, or the golden one with a boost that basically never runs out.
Random rewards are a cliché because they work — not knowing makes you want to pop the box open. I just weight it so the boring stuff is common and the good stuff is rare, then roll:
func roll(rng: RandomNumberGenerator) -> String:
var total := 0
for id in ITEMS:
total += ITEMS[id].weight
var pick := rng.randi() % total
for id in ITEMS:
pick -= ITEMS[id].weight
if pick < 0:
return id
return "mushroom"No magic there. But because you can only hold one thing at a time, even a common roll is a real question: is this mushroom worth locking up my slot, or do I gamble and hope the next box has something better?
the unglamorous half
Then there was the boring work, which is most of the work. The slot saves to disk the second you grab something, so rage-quitting mid-race doesn't rob you of your rocket. And it had to actually be readable on a phone while you're driving, so it's a little coloured chip in the corner with the item name and a "tap to use." Big enough to hit with your thumb without looking down.
Getting that chip to sit right ate more time than any of the fun stuff. It shares a column with the speedo, the online status and the quest tracker, so when you're holding an item everything below it has to shuffle down or they overlap into mush. Not exciting. But if you can't clearly see what you're holding, you forget you're holding it, and then the whole "decide when to use it" idea just quietly dies. So, layout math it was.
Anyway — if a mechanic feels cheap and you can't work out why, my money's on this: you took away a decision the player should've been making. Wasn't more content I needed. It was putting a little "when?" back in their hands.
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.