Mastering Three.js: Create Your Own Crossy Road Game Clone
- Published on
- Authors
- Name
- Binh Bui
- @bvbinh
Mastering Three.js: Create Your Own Crossy Road Game Clone
If you're looking to dive into the exciting world of game development and bring the joy of endless crossing to life, you've come to the right place! In this detailed tutorial, we’ll embark on a journey to create a clone of the popular mobile game Crossy Road using Three.js. Your goal? Navigate your character through an endless landscape filled with static and dynamic obstacles like trees and speeding cars. Let’s get started!
Table of Contents
Setting Up the Game
To kick off, we need to set up our working environment. I recommend using Vite, an efficient build tool, for easy initialization of our project. Open your terminal and run:
npm create vite my-crossy-road-game
cd my-crossy-road-game
npm install three
npm run dev
Ensure you select Vanilla as we won’t be using a front-end framework. Once we've created our project and started the development server, we will adjust the HTML structure by replacing the default <div>
with a <canvas>
element designated for Three.js rendering:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crossy Road Game</title>
</head>
<body>
<canvas class="game"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
The Main Scene Setup
In your main.js
file, you will establish a Three.js scene, a camera, and a renderer. Here’s how:
import * as THREE from 'three';
import { Renderer } from './Renderer';
import { Camera } from './Camera';
import { player } from './Player';
const scene = new THREE.Scene();
scene.add(player);
const camera = Camera();
player.add(camera);
const renderer = Renderer();
renderer.render(scene, camera);
Next, we’ll create our player character as a simple 3D box:
import * as THREE from 'three';
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' }));
body.position.z = 10;
player.add(body);
return player;
}
Rendering the Map
Defining the Landscape
Now it’s time to create the map where our character will navigate. We can break the map into multiple rows, with each row represented by metadata defining the landscape components. Let’s start by exporting some constants for our map layout:
export const minTileIndex = -8;
export const maxTileIndex = 8;
export const tilesPerRow = maxTileIndex - minTileIndex + 1;
export const tileSize = 42;
Adding the Grass
To start, we’ll create a grass foundation for our first row:
import * as THREE from 'three';
import { tilesPerRow, tileSize } from './constants';
export function Grass(rowIndex) {
const grass = new THREE.Group();
grass.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh(
new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3),
new THREE.MeshLambertMaterial({ color: 0xbaf455 })
);
foundation.position.z = 1.5;
grass.add(foundation);
return grass;
}
Animating the Cars
Enabling Car Movement
The next challenge is to bring our lanes to life by adding moving cars. We achieve this by creating metadata for the lanes and spacing them accordingly. Let’s add a lane represented by the direction and speed of each vehicle,
export const metadata = [
{
type: 'car',
direction: true,
speed: 1,
vehicles: [{ initialTileIndex: 2, color: 0xff0000 }],
},
];
Rendering Cars
We would then invoke a Car
function to visualize these vehicles in motion. We'll animate them where necessary:
import * as THREE from 'three';
import { tileSize } from './constants';
export function Car(initialTileIndex, direction, color) {
const car = new THREE.Group();
car.position.x = initialTileIndex * tileSize;
if (!direction) car.rotation.z = Math.PI;
// Create the car body and details here...
return car;
}
Moving the Player
Collecting User Inputs
To facilitate player movement, we’ll implement event listeners for keyboard controls. As the player navigates, we will check for directional commands:
window.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowUp': queueMove('forward'); break;
case 'ArrowDown': queueMove('backward'); break;
case 'ArrowLeft': queueMove('left'); break;
case 'ArrowRight': queueMove('right'); break;
}
});
Executing Moves
Implementing the player's movement animations becomes key now. Utilizing linear interpolation and sine functions allows the player to jump from tile to tile realistically:
function setPosition(progress) {
const startX = position.currentTile * tileSize;
const startY = position.currentRow * tileSize;
// Calculate the new position based on movesQueue...
}
Hit Detection
To ensure everything runs smoothly, we’ll include collision detection within the game. This checks if the player collides with a vehicle and appropriately ends the game with a popup.
if (playerBoundingBox.intersectsBox(vehicleBoundingBox)) {
window.alert('Game over!');
window.location.reload();
}
Next Steps
Congratulations on reaching the end of this tutorial! You've successfully rendered a map, animated vehicles, handled player movements, and incorporated hit detection to challenge your skills. Much more awaits if you choose to expand this game – consider enhancements like truck lanes, a scoring system, or a UI!
For further details, check out the extended version of this tutorial at JavaScriptGameTutorials.com or catch the video walkthrough on YouTube. Happy coding!