Score & UI
Two components handle in-game scoring: Score (data + events) and Scoreboard (DOM overlay that renders the data). Use them together or just Score if you're rendering UI elsewhere.
Score
A score tracker keyed by player ID. The Score component is typically attached to a world-level entity rather than per-player — it tracks all players' scores in one place.
export interface ScoreInput {
scores?: Record<string, number>; // initial scores keyed by player ID
}
export const ScoreEvents = {
CHANGED: 'score.changed',
RESET: 'score.reset',
} as const;
export const ScoreInputEvents = {
ADD: 'score.add', // { playerId, delta }
SET: 'score.set', // { playerId, value }
RESET: 'score.reset',
} as const;
JSON
A common pattern: a top-level "World" entity owns the Score component.
"World": {
"tags": ["global"],
"components": {
"Score": { "scores": { "p1": 0, "p2": 0 } }
}
}
Driving the score from gameplay
When a player picks up a coin, kills an enemy, scores a goal — emit score.add:
this.eventBus.emit('score.add', { playerId: 'p1', delta: 10 });
The Score System updates its internal map and emits score.changed with the new value. Subscribe to score.changed in your UI / sound / particle systems if you want effects when scores update.
Scoreboard
A DOM overlay that renders the current Score state. It creates a <div> outside the canvas, positioned by CSS, and re-renders whenever score.changed fires.
export interface ScoreboardInput {
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center';
fontSize?: number;
players: Array<{ id: string; label: string; color?: [number, number, number] }>;
}
JSON
"ScoreboardUI": {
"components": {
"Scoreboard": {
"position": "top-right",
"fontSize": 18,
"players": [
{ "id": "p1", "label": "Player 1", "color": [1, 0.3, 0.3] },
{ "id": "p2", "label": "Player 2", "color": [0.3, 0.6, 1] }
]
}
}
}
The Scoreboard listens for score.changed on the EventBus and re-renders only the affected player's row. It's lightweight DOM work — fine to run alongside your 60fps game loop.
Custom styling
The Scoreboard renders inline styles. To restyle, pass an explicit style field or replace the component with your own Solid/React/whatever UI that subscribes to score.changed.
The component is just an event listener + DOM writer — you can absolutely build your own HUD that subscribes to the same events without using Scoreboard at all.
Common patterns
Game over on threshold
this.eventBus.on('score.changed', ({ playerId, value }) => {
if (value >= 100) {
this.eventBus.emit('game.over', { winnerId: playerId });
}
});
Persisting score across scene loads
Save the score state when transitioning out:
const score = world.getEntitiesByTag('global')[0]?.get(ScoreComponent);
localStorage.setItem('score', JSON.stringify(score?.scores ?? {}));
And restore by passing it in via the scene JSON or directly setting it after instantiation.
Multi-team scoring
Don't fight the API — give each team its own player ID:
"Score": { "scores": { "red": 0, "blue": 0 } }
And tag entities 'team-red' or 'team-blue' so your gameplay Systems know who scored for whom.
Where to next
- Animation & Mesh —
.glbloading and skeletal animation - Extending — viz panels, custom registries
