Score Indicator

Crossy Road with React Three Fiber ● Chapter 9

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.

Keeping Track of the Score in the Game Store

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.

src/stores/game.ts
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;
src/stores/game.js
import { create } from "zustand";
const useStore = create((set) => ({
score: 0,
updateScore: (rowIndex) => {
set((state) => ({ score: Math.max(rowIndex, state.score) }));
},
}));
export default useStore;

Displaying the Score

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.

src/components/Score.tsx
import useStore from "../stores/game";
import "./Score.css";
export function Score() {
const score = useStore((state) => state.score);
return <div id="score">{score}</div>;
}
src/components/Score.jsx
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.

src/components/Score.css
#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.

src/Game.tsx
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>
);
}
src/Game.jsx
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>
);
}

Increasing the Score as the Player Moves Forward

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.

src/stores/player.ts
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);
}
src/stores/player.js
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);
}
Previous:
Generating the Map
Next:
Hit Detection and Result Screen