Score Indicator

Crossy Road with Three.js ● 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.

Displaying the Score

Let’s add a new div element with id score to our HTML file. This simple div contains the number 0 as the initial value. We will update this value as the player progresses.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<canvas class="game"></canvas>
<div id="controls">
<div>
<button id="forward">▲</button>
<button id="left">◀</button>
<button id="backward">▼</button>
<button id="right">▶</button>
</div>
</div>
<div id="score">0</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<canvas class="game"></canvas>
<div id="controls">
<div>
<button id="forward">▲</button>
<button id="left">◀</button>
<button id="backward">▼</button>
<button id="right">▶</button>
</div>
</div>
<div id="score">0</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

We also style the score indicator. We position the score in the top-left corner of the screen with absolute positioning. We also load the Press Start 2P font from Google Fonts and set it as our font.

src/style.css
@import url("https://fonts.googleapis.com/css?family=Press+Start+2P");
body {
margin: 0;
display: flex;
font-family: "Press Start 2P", cursive;
}
27 collapsed lines
#controls {
position: absolute;
bottom: 0;
min-width: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
}
#controls div {
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: auto auto;
grid-column-gap: 10px;
grid-row-gap: 10px;
margin-bottom: 20px;
}
#controls button {
width: 100%;
background-color: white;
border: 1px solid lightgray;
}
#controls button:first-of-type {
grid-column: 1/-1;
}
#score {
position: absolute;
top: 20px;
left: 20px;
font-size: 2em;
color: white;
}

Increasing the Score as the Player Moves Forward

Once the player completes a step, the stepCompleted function gets called. This is a good place to update our score indicator.

We get a reference to the score element by id. Then, we set its innerText value to the current row. Feel free to refine this to update the value only if the new value is higher than the previous one.

src/components/Player.ts
63 collapsed lines
import * as THREE from "three";
import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";
import { metadata as rows, addRows } from "./Map";
import type { MoveDirection } from "../types";
export const player = Player();
function Player() {
const player = new THREE.Group();
const body = new THREE.Mesh(
new THREE.BoxGeometry(15, 15, 20),
new THREE.MeshLambertMaterial({
color: "white",
flatShading: true,
})
);
body.castShadow = true;
body.receiveShadow = true;
body.position.z = 10;
player.add(body);
const cap = new THREE.Mesh(
new THREE.BoxGeometry(2, 4, 2),
new THREE.MeshLambertMaterial({
color: 0xf0619a,
flatShading: true,
})
);
cap.position.z = 21;
cap.castShadow = true;
cap.receiveShadow = true;
player.add(cap);
const playerContainer = new THREE.Group();
playerContainer.add(player);
return playerContainer;
}
export const position: {
currentRow: number;
currentTile: number;
} = {
currentRow: 0,
currentTile: 0,
};
export const movesQueue: MoveDirection[] = [];
export function queueMove(direction: MoveDirection) {
const isValidMove = endsUpInValidPosition(
{
rowIndex: position.currentRow,
tileIndex: position.currentTile,
},
[...movesQueue, direction]
);
if (!isValidMove) return;
movesQueue.push(direction);
}
export function stepCompleted() {
const direction = movesQueue.shift();
if (direction === "forward") position.currentRow += 1;
if (direction === "backward") position.currentRow -= 1;
if (direction === "left") position.currentTile -= 1;
if (direction === "right") position.currentTile += 1;
// Add new rows if the player is running out of them
if (position.currentRow > rows.length - 10) addRows();
const scoreDOM = document.getElementById("score");
if (scoreDOM) scoreDOM.innerText = position.currentRow.toString();
}
src/components/Player.js
59 collapsed lines
import * as THREE from "three";
import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";
import { metadata as rows, addRows } from "./Map";
export const player = Player();
function Player() {
const player = new THREE.Group();
const body = new THREE.Mesh(
new THREE.BoxGeometry(15, 15, 20),
new THREE.MeshLambertMaterial({
color: "white",
flatShading: true,
})
);
body.castShadow = true;
body.receiveShadow = true;
body.position.z = 10;
player.add(body);
const cap = new THREE.Mesh(
new THREE.BoxGeometry(2, 4, 2),
new THREE.MeshLambertMaterial({
color: 0xf0619a,
flatShading: true,
})
);
cap.position.z = 21;
cap.castShadow = true;
cap.receiveShadow = true;
player.add(cap);
const playerContainer = new THREE.Group();
playerContainer.add(player);
return playerContainer;
}
export const position = {
currentRow: 0,
currentTile: 0,
};
export const movesQueue = [];
export function queueMove(direction) {
const isValidMove = endsUpInValidPosition(
{
rowIndex: position.currentRow,
tileIndex: position.currentTile,
},
[...movesQueue, direction]
);
if (!isValidMove) return;
movesQueue.push(direction);
}
export function stepCompleted() {
const direction = movesQueue.shift();
if (direction === "forward") position.currentRow += 1;
if (direction === "backward") position.currentRow -= 1;
if (direction === "left") position.currentTile -= 1;
if (direction === "right") position.currentTile += 1;
// Add new rows if the player is running out of them
if (position.currentRow > rows.length - 10) addRows();
const scoreDOM = document.getElementById("score");
if (scoreDOM) scoreDOM.innerText = position.currentRow.toString();
}
Previous:
Generating the Map
Next:
Hit Detection and Result Screen