CodeWithYou

Mastering Three.js: Create Your Own Crossy Road Game Clone

Published on
Authors
"Mastering Three.js: Create Your Own Crossy Road Game Clone"
Photo by AI

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!

Advertisement