Skip to content

Round

Round times a round of play: “collect 8 mushrooms before 60 seconds run out”, or “two laps, then the finish”. The score is the coins in the player’s wallet, which Pickup fills. The round is won the step the score reaches the target or the last lap is in, and lost the step the time runs out first. A rule you leave out is off: a round with no seconds never runs out.

Unity’s Karting Microgame splits this between an objective and a time manager, and Unreal between a racing GameMode and its GameState. Here one Round carries both, so a game configures a race rather than writing one.

Its props are in RoundProps.

The round counts simulated seconds, the steps that useTime counts, not the wall clock. A modal that pauses, the devtools’ pause, and a headless world that nobody steps all hold its clock. Once it is won or lost, the round keeps its clock and its score.

Put Round inside the scene it times. It starts when the scene opens and goes when the scene closes.

A race starts on a signal, counts down, and ends when the player crosses the line, or when the game says it failed. Round covers each part:

  • ready: the round waits in RoundState.Ready, its clock held, until start(). A sling’s release or a Start button calls it.
  • countdown: seconds in RoundState.Countdown before it plays. Without ready, the countdown starts when the round mounts.
  • laps: the laps that win it. Each entry into a TrackTrigger marked lap counts one while the round plays. Start the mover past the line, or its first crossing counts.
  • finish() wins a round in play, and fail(reason) loses one that has not ended. The round keeps the reason, so the screen says why: a crash, a fall, a false start. A round that runs out of time has the reason "time".

A system calls startRound(world), finishRound(world) and failRound(world, reason) from @daniel-zarinski/engine/core; a component calls the same verbs from useRound().

useRound() returns a RoundStatus: the seconds left and played, the countdown left, the score and its target, the laps done and wanted, the RoundState, the reason it was lost, the verbs above, and restart(), which starts the round’s scene again.

The player’s wallet is her hero’s. In a game whose player is no hero, such as a rider on a track, it is the wallet on the entity under the client’s authority, and useWallet() shows it the same way.

Before a round mounts, useRound() reads a fresh one from the defaults.

<RoundScreen> opens a Modal the step the round is won or lost: a title, a line of text, the score where the round has a target, anything you pass as children, and a button that calls restart(). It draws nothing until the round ends.

Its props are in RoundScreenProps.

Mount one <RoundScreen> beside the round it screens; a game replaces its title and text with its own, but keeps the score and the button.

Server. The round has a consequence: it decides the game’s outcome.

The step runs the round after the behaviours, so a coin picked up in a step counts in that step. The headless harness runs it too: spawn the RoundTrait entity in the scene, step the world, and read the round back from the dump under round.

import {
Hud,
Panel,
Player,
Round,
RoundScreen,
RoundState,
Slot,
Text,
useRound,
World,
} from "@daniel-zarinski/engine";
function Clock() {
const { secondsLeft, score, target, state } = useRound();
return (
<Hud>
<Panel slot={Slot.TopLeft}>
<Text>
{state === RoundState.Playing
? `${Math.ceil(secondsLeft)} s, ${score} of ${target}`
: state}
</Text>
</Panel>
</Hud>
);
}
export function Meadow() {
return (
<World map="meadow">
<Player />
<Round seconds={60} target={8} />
<Clock />
<RoundScreen wonText="Every mushroom, with time to spare." />
</World>
);
}