Now that we can move forward through infinite rows of obstacles let’s add a score indicator to keep track of how many rows we’ve crossed.
Now that we can move forward through infinite rows of obstacles let’s add a score indicator to keep track of how many rows we’ve crossed.
Let’s add our last store called game
store. This is another zustand
store that keeps track of the score and other top-level game states.
For now, we will only keep track of the score. The store has a score
property and an updateScore
method that will update the score if the new score is higher than the current score.
import { create } from "zustand";
interface StoreState { score: number; updateScore: (rowIndex: number) => void;}
const useStore = create<StoreState>((set) => ({ score: 0, updateScore: (rowIndex: number) => { set((state) => ({ score: Math.max(rowIndex, state.score) })); },}));
export default useStore;
import { create } from "zustand";
const useStore = create((set) => ({ score: 0, updateScore: (rowIndex) => { set((state) => ({ score: Math.max(rowIndex, state.score) })); },}));
export default useStore;
Now that we have a store that tracks the score, let’s display it on the screen. This simple React component reads the score from the store and displays it.
import useStore from "../stores/game";import "./Score.css";
export function Score() { const score = useStore((state) => state.score);
return <div id="score">{score}</div>;}
import useStore from "../stores/game";import "./Score.css";
export function Score() { const score = useStore((state) => state.score);
return <div id="score">{score}</div>;}
It also comes with a CSS file to style the score indicator. We are positioning the score in the top-left corner of the screen with absolute position. This element will use the Press Start 2P
font we set in the game.css
style.
#score { position: absolute; top: 20px; left: 20px;
font-size: 2em; color: white;}
Finally, we need to add the Score
component to the Game
component. Since the score indicator is not part of the 3D scene, we add it to the container div element.
import { Scene } from "./components/Scene";import { Player } from "./components/Player";import { Map } from "./components/Map";import { Score } from "./components/Score";import { Controls } from "./components/Controls";import "./index.css";
export default function Game() { return ( <div className="game"> <Scene> <Player /> <Map /> </Scene> <Score /> <Controls /> </div> );}
import { Scene } from "./components/Scene";import { Player } from "./components/Player";import { Map } from "./components/Map";import { Score } from "./components/Score";import { Controls } from "./components/Controls";import "./index.css";
export default function Game() { return ( <div className="game"> <Scene> <Player /> <Map /> </Scene> <Score /> <Controls /> </div> );}
Finally, once the player completes a step, we call the updateScore
method from the game store. We can do this as the last statement of the stepCompleted
function of the player store.
The updateScore
method is smart enough to only update the score if the current row index is higher than the current score, so we can call it after every movement.
import type { MoveDirection } from "../types";import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";import useMapStore from "./map";import useGameStore from "./game";
20 collapsed lines
export const state: { currentRow: number; currentTile: number; movesQueue: MoveDirection[];} = { currentRow: 0, currentTile: 0, movesQueue: [],};
export function queueMove(direction: MoveDirection) { const isValidMove = endsUpInValidPosition( { rowIndex: state.currentRow, tileIndex: state.currentTile }, [...state.movesQueue, direction] );
if (!isValidMove) return;
state.movesQueue.push(direction);}
export function stepCompleted() { const direction = state.movesQueue.shift();
if (direction === "forward") state.currentRow += 1; if (direction === "backward") state.currentRow -= 1; if (direction === "left") state.currentTile -= 1; if (direction === "right") state.currentTile += 1;
// Add a batch of new rows if the player is running out of them; rows are infinite if (state.currentRow === useMapStore.getState().rows.length - 10) { useMapStore.getState().addRows(); }
useGameStore.getState().updateScore(state.currentRow);}
import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";import useMapStore from "./map";import useGameStore from "./game";
16 collapsed lines
export const state = { currentRow: 0, currentTile: 0, movesQueue: [],};
export function queueMove(direction) { const isValidMove = endsUpInValidPosition( { rowIndex: state.currentRow, tileIndex: state.currentTile }, [...state.movesQueue, direction] );
if (!isValidMove) return;
state.movesQueue.push(direction);}
export function stepCompleted() { const direction = state.movesQueue.shift();
if (direction === "forward") state.currentRow += 1; if (direction === "backward") state.currentRow -= 1; if (direction === "left") state.currentTile -= 1; if (direction === "right") state.currentTile += 1;
// Add a batch of new rows if the player is running out of them; rows are infinite if (state.currentRow === useMapStore.getState().rows.length - 10) { useMapStore.getState().addRows(); }
useGameStore.getState().updateScore(state.currentRow);}