Restricting Player Movement

Crossy Road with React Three Fiber ● Chapter 6

In the previous chapter, we added event handlers and logic to move the player around the map. But so far, the player can walk across trees and vehicles and go outside the map. In this chapter, we are going to make sure that the player can’t take an invalid position.

We will check if a move is valid by calculating where it would take the player. If the player would end up outside the map or on a tile occupied by a tree, we will ignore that move.

Calculating Where the Player ends up

First, we need to calculate where the player would end up if they made a move. Whenever we add a new move to the queue, we need to calculate where the player would end up if they made all the moves in the movesQueue and then take the current movement command.

For instance, if the current position is { rowIndex: 0, tileIndex: 0 } and the moves are ["forward", "left"], the final position would be { rowIndex: 1, tileIndex: -1 }.

src/utilities/calculateFinalPosition.ts
import type { MoveDirection } from "../types";
export function calculateFinalPosition(
currentPosition: { rowIndex: number; tileIndex: number },
moves: MoveDirection[]
) {
return moves.reduce((position, direction) => {
if (direction === "forward")
return {
rowIndex: position.rowIndex + 1,
tileIndex: position.tileIndex,
};
if (direction === "backward")
return {
rowIndex: position.rowIndex - 1,
tileIndex: position.tileIndex,
};
if (direction === "left")
return {
rowIndex: position.rowIndex,
tileIndex: position.tileIndex - 1,
};
if (direction === "right")
return {
rowIndex: position.rowIndex,
tileIndex: position.tileIndex + 1,
};
return position;
}, currentPosition);
}
src/utilities/calculateFinalPosition.js
export function calculateFinalPosition(currentPosition, moves) {
return moves.reduce((position, direction) => {
if (direction === "forward")
return {
rowIndex: position.rowIndex + 1,
tileIndex: position.tileIndex,
};
if (direction === "backward")
return {
rowIndex: position.rowIndex - 1,
tileIndex: position.tileIndex,
};
if (direction === "left")
return {
rowIndex: position.rowIndex,
tileIndex: position.tileIndex - 1,
};
if (direction === "right")
return {
rowIndex: position.rowIndex,
tileIndex: position.tileIndex + 1,
};
return position;
}, currentPosition);
}

Evaluating if a Move is Valid

Then, we create another utility function to calculate whether the player would end up in a valid position after making a series of moves.

We use the calculateFinalPosition function to calculate where the player would end up after a move. Then, we check if the player would end up outside the map or on a tile occupied by a tree. If the move is invalid, we return false.

First, we check if the final position is before the starting row or if the tile number is outside the range of the tiles. Then, we check the metadata of the row the player ends up in. Here, the index is off by one because the row metadata does not include the starting row. If we end up in a forest, we check whether the tile we would move to is occupied by a tree. If any of this is true, we return false.

src/utilities/endsUpInValidPosition.ts
import type { MoveDirection } from "../types";
import { calculateFinalPosition } from "./calculateFinalPosition";
import { minTileIndex, maxTileIndex } from "../constants";
import { rows } from "../metadata";
export function endsUpInValidPosition(
currentPosition: { rowIndex: number; tileIndex: number },
moves: MoveDirection[]
) {
// Calculate where the player would end up after the move
const finalPosition = calculateFinalPosition(
currentPosition,
moves
);
// Detect if we hit the edge of the board
if (
finalPosition.rowIndex === -1 ||
finalPosition.tileIndex === minTileIndex - 1 ||
finalPosition.tileIndex === maxTileIndex + 1
) {
// Invalid move, ignore move command
return false;
}
// Detect if we hit a tree
const finalRow = rows[finalPosition.rowIndex - 1];
if (
finalRow &&
finalRow.type === "forest" &&
finalRow.trees.some(
(tree) => tree.tileIndex === finalPosition.tileIndex
)
) {
// Invalid move, ignore move command
return false;
}
return true;
}
src/utilities/endsUpInValidPosition.js
import { calculateFinalPosition } from "./calculateFinalPosition";
import { minTileIndex, maxTileIndex } from "../constants";
import { rows } from "../metadata";
export function endsUpInValidPosition(currentPosition, moves) {
// Calculate where the player would end up after the move
const finalPosition = calculateFinalPosition(
currentPosition,
moves
);
// Detect if we hit the edge of the board
if (
finalPosition.rowIndex === -1 ||
finalPosition.tileIndex === minTileIndex - 1 ||
finalPosition.tileIndex === maxTileIndex + 1
) {
// Invalid move, ignore move command
return false;
}
// Detect if we hit a tree
const finalRow = rows[finalPosition.rowIndex - 1];
if (
finalRow &&
finalRow.type === "forest" &&
finalRow.trees.some(
(tree) => tree.tileIndex === finalPosition.tileIndex
)
) {
// Invalid move, ignore move command
return false;
}
return true;
}

Updating the Player Store Logic

Finally, we extend the player’s queueMove function with the endsUpInValidPosition to check if a move is valid.

If it returns false, we return from the function before the move command is added to the queue. This way, only valid moves are added to the movesQueue.

src/stores/player.ts
import type { MoveDirection } from "../types";
import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";
9 collapsed lines
export let 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);
}
8 collapsed lines
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;
}
src/stores/player.js
import { endsUpInValidPosition } from "../utilities/endsUpInValidPosition";
5 collapsed lines
export let 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);
}
8 collapsed lines
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;
}
Previous:
Moving the Player
Next:
Following the Player