Yesterday we had a framework. Today we have a game loop.
The pieces
- TimedEvaluator — Watches a player doing something (roasting marshmallows, in this case) and scores them on timing and technique
- Scoreboard — Tracks and displays scores per round
- Orchestrator — The conductor. It sequences rounds: wait for players, start timer, evaluate, score, repeat
- GlobalTimer — A shared countdown visible to all players, driving the round pacing
- LeaderBoard — Persistent top-10 high scores saved to DataStore
The Orchestrator is the interesting one. Instead of a giant while true loop with hardcoded state transitions, it’s event-driven. Each phase fires a signal when it’s done, and the next phase listens for it. This means you can swap out phases, add new ones, or change the order without rewriting the loop.
Why event-driven matters on Roblox
A lot of Roblox game loops look like this:
while true do
startRound()
wait(60)
endRound()
wait(10)
end
That works for simple games, but the moment you need “pause when no players are in the zone” or “skip the countdown if everyone’s ready,” you’re fighting the linear flow. Adding a mid-round event means inserting an if into the middle of a wait(). Handling player disconnects means checking state after every yield.
Events let each piece react independently. The GlobalTimer doesn’t know about the Orchestrator — it just counts down and fires onComplete. The Orchestrator doesn’t know about the Scoreboard — it just fires roundEnded with the results. Each component can be tested in isolation, and the wiring between them is explicit, not buried in nested conditionals.
Persistent leaderboards
The LeaderBoard stores top-10 scores to DataStore, so they survive server restarts. The implementation is straightforward — UpdateAsync with a sorted insertion — but it’s the first piece of persistent state in the framework. Everything else so far has been ephemeral (resets when the server shuts down).
One Roblox-specific decision: we update the DataStore at the end of each round, not on every score change. DataStore has rate limits (roughly 60 + numPlayers * 10 requests per minute), and a busy game could easily exceed that if every score update was a write. Batching to round boundaries keeps us well within limits.
The game now runs end-to-end: players enter a zone, a timer counts down, they get scored, results hit the leaderboard. It’s rough, but it’s a loop.