Create a Crossy Road Game Clone with HTML, CSS, and JavaScript

Faraz

By Faraz -

Learn how to build your own Crossy Road game clone using HTML, CSS, and JavaScript. Step-by-step guide with examples and code snippets.


Crossy Road Game Clone HTML CSS JavaScript.jpg

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Preview
  6. Conclusion

Crossy Road is a highly popular mobile game that offers a fun and challenging experience to players of all ages. In this tutorial, we will guide you through the process of creating your own Crossy Road game clone using HTML, CSS, and JavaScript Three.js.

Crossy Road takes a simple concept and turns it into an addictive gameplay experience. The objective of the game is to navigate a character across a busy road filled with moving vehicles, rivers with floating logs, and other obstacles. The player must carefully time their movements to avoid colliding with these hazards while also collecting rewards and power-ups along the way.

By creating a Crossy Road game clone, you will gain valuable insights into HTML game development, CSS game design, and leveraging the power of Three.js for creating interactive 3D games. This tutorial is suitable for beginners and intermediate developers who want to delve into game development and enhance their skills.

Throughout this tutorial, we will provide step-by-step instructions, code snippets, and guidance to help you build your own Crossy Road game clone from scratch. By the end of the tutorial, you will have a fully functional game with responsive controls, engaging gameplay mechanics, and captivating visual effects.

Feel free to experiment and customize the game as you progress through the tutorial. This will allow you to add your own creative touch and make the game truly unique. Additionally, we encourage you to test and debug your game at various stages to ensure optimal performance and a seamless gaming experience.

So, let's dive into the exciting world of game development and embark on the journey of creating your very own Crossy Road game clone using HTML, CSS, and JavaScript Three.js.

Created by: Hunor Marton

Source Code

Step 1 (HTML Code):

To get started, we will first need to create a basic HTML file. In this file, we will include the main structure for our Crossy Road game.

After creating the files just paste the following codes into your file. Make sure to save your HTML document with a .html extension, so that it can be properly viewed in a web browser.

The code is divided into several sections, such as the head, body, and script sections. Let's go through each section and understand its role.

<!DOCTYPE html>: This is the document type declaration, which specifies that the document is an HTML5 document.

<html lang="en">: The opening tag for the HTML element, which represents the root of an HTML document. The "lang" attribute specifies the language of the document, which is set to English in this case.

<head>: The head section contains meta-information about the document, such as the title, character encoding, and linked stylesheets or scripts.

<title>Crossy Road Game Clone</title>: The title element sets the title of the web page, which appears in the browser's title bar or tab.

<meta charset="UTF-8" />: The meta tag specifies the character encoding for the document. UTF-8 is a widely used character encoding that supports a wide range of characters.

<meta name="viewport" content="width=device-width" />: This meta tag sets the viewport configuration for responsive web design. It ensures that the page is displayed properly on different devices by adapting to the width of the device.

<link rel="stylesheet" href="styles.css" />: The link tag is used to include an external CSS (Cascading Style Sheets) file. The "href" attribute specifies the path to the CSS file, in this case, "styles.css". CSS is used for styling the web page and defining its visual appearance.

</head>: The closing tag for the head section.

<body>: The body section contains the main content of the web page, including text, images, and interactive elements.

<div id="counter">0</div>: This is a div element with the id attribute "counter". The id attribute provides a unique identifier for the element. In this case, the div is used to display a counter with an initial value of 0.

<div id="controls">: This div element with the id "controls" contains a set of buttons for controlling the game. It allows the player to move in different directions.

<div>: This is another div element without an id. It contains a group of buttons for controlling the movement in specific directions, such as forward, left, backward, and right. Each button has a unique id and an associated SVG (Scalable Vector Graphics) icon that represents the direction.

<button id="retry">Retry</button>: This button element with the id "retry" is contained within a div with the id "end". It represents a retry button that allows the player to restart the game.

<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js'></script>: This script tag includes an external JavaScript file from the specified source. The JavaScript library "three.js" is loaded from the given URL. It is a popular library for creating 3D graphics in web browsers.

<script src="script.js"></script>: This script tag includes another external JavaScript file named "script.js" from the local directory. The file contains custom JavaScript code for the Crossy Road game clone.

</body>: The closing tag for the body section.

</html>: The closing tag for the HTML document.

This is the basic structure of our Crossy Road game using HTML, and now we can move on to styling it using CSS.

Step 2 (CSS Code):

Once the basic HTML structure of the website is in place, the next step is to add styling to the game using CSS. CSS allows us to control the visual appearance of the website, including things like layout, color, and typography.

Next, we will create our CSS file. In this file, we will use some basic CSS rules to create our Crossy Road game.

Here's an explanation of the different parts of the CSS code:

@import url('https://fonts.googleapis.com/css?family=Press+Start+2P');: This line imports a font called "Press Start 2P" from the Google Fonts API. It allows you to use this font in your web page.

body: This selector targets the <body> element of the HTML document. The following rules apply to the body:

  • margin: 0;: Sets the margin of the body element to 0, removing any default margin.
  • font-family: 'Press Start 2P', cursive;: Sets the font family of the body text to "Press Start 2P" and falls back to the cursive font if the specified font is not available.
  • font-size: 2em;: Sets the font size to 2 times the default size.
  • color: white;: Sets the text color to white.

button: This selector targets all <button> elements in the document. The following rules apply to buttons:

  • outline: none;: Removes the outline that is typically displayed around buttons when they are focused.
  • cursor: pointer;: Changes the cursor to a pointer when hovering over buttons.
  • border: none;: Removes the border around buttons.
  • box-shadow: 3px 5px 0px 0px rgba(0,0,0,0.75);: Adds a box shadow effect to buttons with specific dimensions and color.

#counter: This selector targets an element with the ID "counter". The following rules apply to this element:

  • position: absolute;: Positions the element using absolute positioning.
  • top: 20px;: Sets the top position of the element to 20 pixels from the top of its containing element.
  • right: 20px;: Sets the right position of the element to 20 pixels from the right of its containing element.
  • #end: This selector targets an element with the ID "end". The following rules apply to this element:

    • position: absolute;: Positions the element using absolute positioning.
    • min-width: 100%;: Sets the minimum width of the element to 100% of its containing element.
    • min-height: 100%;: Sets the minimum height of the element to 100% of its containing element.
    • display: flex;: Makes the element a flex container.
    • align-items: center;: Aligns the flex items vertically at the center of the container.
    • justify-content: center;: Aligns the flex items horizontally at the center of the container.
    • visibility: hidden;: Initially hides the element.

    #end button: This selector targets <button> elements within the element with the ID "end". The following rules apply to these buttons:

    • background-color: red;: Sets the background color of the buttons to red.
    • padding: 20px 50px 20px 50px;: Sets the padding around the buttons.
    • font-family: inherit;: Inherits the font family from the parent element.
    • font-size: inherit;: Inherits the font size from the parent element.

    #controlls: This selector targets an element with the ID "controlls". The following rules apply to this element:

    • position: absolute;: Positions the element using absolute positioning.
    • min-width: 100%;: Sets the minimum width of the element to 100% of its containing element.
    • min-height: 100%;: Sets the minimum height of the element to 100% of its containing element.
    • display: flex;: Makes the element a flex container.
    • align-items: flex-end;: Aligns the flex items vertically at the bottom of the container.
    • justify-content: center;: Aligns the flex items horizontally at the center of the container.

    #controlls div: This selector targets <div> elements within the element with the ID "controlls". The following rules apply to these divs:

    • display: grid;: Sets the display property of the divs to grid.
    • grid-template-columns: 50px 50px 50px;: Sets the column sizes in the grid layout.
    • grid-template-rows: auto auto;: Sets the row sizes in the grid layout.
    • grid-column-gap: 10px;: Sets the gap between columns in the grid.
    • grid-row-gap: 10px;: Sets the gap between rows in the grid.
    • margin-bottom: 20px;: Sets a margin at the bottom of the divs.

    #controlls button: This selector targets <button> elements within the element with the ID "controlls". The following rules apply to these buttons:

    • width: 100%;: Sets the width of the buttons to 100% of their container.
    • background-color: white;: Sets the background color of the buttons to white.
    • border: 1px solid lightgray;: Sets a border around the buttons.

    #controlls button:first-of-type: This selector targets the first <button> element within the element with the ID "controlls". The following rules apply to this button:

    • grid-column: 1/-1;: Spans the button across all columns of the grid layout.

    This will give our Crossy Road game an upgraded presentation. Create a CSS file with the name of styles.css and paste the given codes into your CSS file. Remember that you must create a file with the .css extension.

  • @import url('https://fonts.googleapis.com/css?family=Press+Start+2P');
    
    body {
      margin: 0;
      font-family: 'Press Start 2P', cursive;
      font-size: 2em;
      color: white;
    }
    button {
      outline: none;
      cursor: pointer;
      border: none;
      box-shadow: 3px 5px 0px 0px rgba(0,0,0,0.75);
    }
    #counter {
      position: absolute;
      top: 20px;
      right: 20px;
    }
    #end {
      position: absolute;
      min-width: 100%;
      min-height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      visibility: hidden;
    }
    #end button {
      background-color: red;
      padding: 20px 50px 20px 50px;
      font-family: inherit;
      font-size: inherit;
    }
    #controlls {
      position: absolute;
      min-width: 100%;
      min-height: 100%;
      display: flex;
      align-items: flex-end;
      justify-content: center;
    }
    #controlls 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;
    }
    #controlls button {
      width: 100%;
      background-color: white;
      border: 1px solid lightgray;
    }
    #controlls button:first-of-type {
      grid-column: 1/-1;
    } 

    Step 3 (JavaScript Code):

    Finally, we use the Three.js library to create a Crossy Road game function in JavaScript.

    Let's go through the code step by step:

    The code starts by defining some constants and variables. counterDOM and endDOM are used to get DOM elements by their IDs. scene is an instance of THREE.Scene, which is used to store and manage all the objects in the game. distance is the distance between the camera and the scene. camera is an instance of THREE.OrthographicCamera that determines the view of the scene.

    The code sets the rotation and position of the camera based on the initial values. It calculates the initial camera position based on the camera rotation.

    Some more constants and variables are declared for various aspects of the game, such as zoom level, chicken size, lane and board dimensions, and time intervals.

    The code defines textures for different parts of the cars and trucks used in the game.

    The generateLanes function is defined. It creates and adds lanes to the scene based on the number of columns. Each lane is an instance of the Lane class, and its position is set based on the index. The function also filters out lanes with a negative index.

    The addLane function is defined. It adds a new lane to the scene based on the current number of lanes.

    An instance of the Chicken class is created and added to the scene.

    Instances of HemisphereLight, DirectionalLight, and DirectionalLight with shadows are created and added to the scene.

    The code initializes some arrays and variables and calls the generateLanes function to populate the initial lanes.

    An instance of THREE.WebGLRenderer is created and configured to enable shadow mapping. The renderer's size is set to match the window's inner size, and its DOM element is appended to the document body.

    The Texture function is defined, which creates a texture using the CanvasTexture class from Three.js. It takes a width, height, and an array of rectangles as parameters, and returns the resulting texture.

    The Wheel function is defined, which creates a wheel mesh using BoxBufferGeometry and MeshLambertMaterial. The wheel's position is set.

    The Car function is defined, which creates a car mesh by combining different geometries and materials. The car's position and appearance are set.

    The Truck function is defined, which creates a truck mesh by combining different geometries and materials. The truck's position and appearance are set.

    The Three function is defined, which creates a tree mesh by combining different geometries and materials. The tree's position and appearance are set.

    The Chicken function is defined, which creates a chicken mesh by combining different geometries and materials. The chicken's position and appearance are set.

    The Road function is defined, which creates a road mesh by combining different geometries and materials.

    The Grass function is defined, which creates a grass mesh by combining different geometries and materials.

    The Lane function is defined, which represents a lane in the game. It takes an index parameter to determine the position of the lane and the type of lane (field, forest, or car). Depending on the type of lane, it creates the appropriate mesh and adds it to the scene.

    Create a JavaScript file with the name of script.js and paste the given codes into your JavaScript file and make sure it's linked properly to your HTML document, so that the scripts are executed on the page. Remember, you’ve to create a file with .js extension.

    const counterDOM = document.getElementById('counter');  
    const endDOM = document.getElementById('end');  
    
    const scene = new THREE.Scene();
    
    const distance = 500;
    const camera = new THREE.OrthographicCamera( window.innerWidth/-2, window.innerWidth/2, window.innerHeight / 2, window.innerHeight / -2, 0.1, 10000 );
    
    camera.rotation.x = 50*Math.PI/180;
    camera.rotation.y = 20*Math.PI/180;
    camera.rotation.z = 10*Math.PI/180;
    
    const initialCameraPositionY = -Math.tan(camera.rotation.x)*distance;
    const initialCameraPositionX = Math.tan(camera.rotation.y)*Math.sqrt(distance**2 + initialCameraPositionY**2);
    camera.position.y = initialCameraPositionY;
    camera.position.x = initialCameraPositionX;
    camera.position.z = distance;
    
    const zoom = 2;
    
    const chickenSize = 15;
    
    const positionWidth = 42;
    const columns = 17;
    const boardWidth = positionWidth*columns;
    
    const stepTime = 200; // Miliseconds it takes for the chicken to take a step forward, backward, left or right
    
    let lanes;
    let currentLane;
    let currentColumn;
    
    let previousTimestamp;
    let startMoving;
    let moves;
    let stepStartTimestamp;
    
    const carFrontTexture = new Texture(40,80,[{x: 0, y: 10, w: 30, h: 60 }]);
    const carBackTexture = new Texture(40,80,[{x: 10, y: 10, w: 30, h: 60 }]);
    const carRightSideTexture = new Texture(110,40,[{x: 10, y: 0, w: 50, h: 30 }, {x: 70, y: 0, w: 30, h: 30 }]);
    const carLeftSideTexture = new Texture(110,40,[{x: 10, y: 10, w: 50, h: 30 }, {x: 70, y: 10, w: 30, h: 30 }]);
    
    const truckFrontTexture = new Texture(30,30,[{x: 15, y: 0, w: 10, h: 30 }]);
    const truckRightSideTexture = new Texture(25,30,[{x: 0, y: 15, w: 10, h: 10 }]);
    const truckLeftSideTexture = new Texture(25,30,[{x: 0, y: 5, w: 10, h: 10 }]);
    
    const generateLanes = () => [-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9].map((index) => {
      const lane = new Lane(index);
      lane.mesh.position.y = index*positionWidth*zoom;
      scene.add( lane.mesh );
      return lane;
    }).filter((lane) => lane.index >= 0);
    
    const addLane = () => {
      const index = lanes.length;
      const lane = new Lane(index);
      lane.mesh.position.y = index*positionWidth*zoom;
      scene.add(lane.mesh);
      lanes.push(lane);
    }
    
    const chicken = new Chicken();
    scene.add( chicken );
    
    hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
    scene.add(hemiLight)
    
    const initialDirLightPositionX = -100;
    const initialDirLightPositionY = -100;
    dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
    dirLight.position.set(initialDirLightPositionX, initialDirLightPositionY, 200);
    dirLight.castShadow = true;
    dirLight.target = chicken;
    scene.add(dirLight);
    
    dirLight.shadow.mapSize.width = 2048;
    dirLight.shadow.mapSize.height = 2048;
    var d = 500;
    dirLight.shadow.camera.left = - d;
    dirLight.shadow.camera.right = d;
    dirLight.shadow.camera.top = d;
    dirLight.shadow.camera.bottom = - d;
    
    // var helper = new THREE.CameraHelper( dirLight.shadow.camera );
    // var helper = new THREE.CameraHelper( camera );
    // scene.add(helper)
    
    backLight = new THREE.DirectionalLight(0x000000, .4);
    backLight.position.set(200, 200, 50);
    backLight.castShadow = true;
    scene.add(backLight)
    
    const laneTypes = ['car', 'truck', 'forest'];
    const laneSpeeds = [2, 2.5, 3];
    const vechicleColors = [0xa52523, 0xbdb638, 0x78b14b];
    const threeHeights = [20,45,60];
    
    const initaliseValues = () => {
      lanes = generateLanes()
    
      currentLane = 0;
      currentColumn = Math.floor(columns/2);
    
      previousTimestamp = null;
    
      startMoving = false;
      moves = [];
      stepStartTimestamp;
    
      chicken.position.x = 0;
      chicken.position.y = 0;
    
      camera.position.y = initialCameraPositionY;
      camera.position.x = initialCameraPositionX;
    
      dirLight.position.x = initialDirLightPositionX;
      dirLight.position.y = initialDirLightPositionY;
    }
    
    initaliseValues();
    
    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true
    });
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );
    
    function Texture(width, height, rects) {
      const canvas = document.createElement( "canvas" );
      canvas.width = width;
      canvas.height = height;
      const context = canvas.getContext( "2d" );
      context.fillStyle = "#ffffff";
      context.fillRect( 0, 0, width, height );
      context.fillStyle = "rgba(0,0,0,0.6)";  
      rects.forEach(rect => {
        context.fillRect(rect.x, rect.y, rect.w, rect.h);
      });
      return new THREE.CanvasTexture(canvas);
    }
    
    function Wheel() {
      const wheel = new THREE.Mesh( 
        new THREE.BoxBufferGeometry( 12*zoom, 33*zoom, 12*zoom ), 
        new THREE.MeshLambertMaterial( { color: 0x333333, flatShading: true } ) 
      );
      wheel.position.z = 6*zoom;
      return wheel;
    }
    
    function Car() {
      const car = new THREE.Group();
      const color = vechicleColors[Math.floor(Math.random() * vechicleColors.length)];
      
      const main = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 60*zoom, 30*zoom, 15*zoom ), 
        new THREE.MeshPhongMaterial( { color, flatShading: true } )
      );
      main.position.z = 12*zoom;
      main.castShadow = true;
      main.receiveShadow = true;
      car.add(main)
      
      const cabin = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 33*zoom, 24*zoom, 12*zoom ), 
        [
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carBackTexture } ),
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carFrontTexture } ),
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carRightSideTexture } ),
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carLeftSideTexture } ),
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true } ), // top
          new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true } ) // bottom
        ]
      );
      cabin.position.x = 6*zoom;
      cabin.position.z = 25.5*zoom;
      cabin.castShadow = true;
      cabin.receiveShadow = true;
      car.add( cabin );
      
      const frontWheel = new Wheel();
      frontWheel.position.x = -18*zoom;
      car.add( frontWheel );
    
      const backWheel = new Wheel();
      backWheel.position.x = 18*zoom;
      car.add( backWheel );
    
      car.castShadow = true;
      car.receiveShadow = false;
      
      return car;  
    }
    
    function Truck() {
      const truck = new THREE.Group();
      const color = vechicleColors[Math.floor(Math.random() * vechicleColors.length)];
    
    
      const base = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 100*zoom, 25*zoom, 5*zoom ), 
        new THREE.MeshLambertMaterial( { color: 0xb4c6fc, flatShading: true } )
      );
      base.position.z = 10*zoom;
      truck.add(base)
    
      const cargo = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 75*zoom, 35*zoom, 40*zoom ), 
        new THREE.MeshPhongMaterial( { color: 0xb4c6fc, flatShading: true } )
      );
      cargo.position.x = 15*zoom;
      cargo.position.z = 30*zoom;
      cargo.castShadow = true;
      cargo.receiveShadow = true;
      truck.add(cargo)
    
      const cabin = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 25*zoom, 30*zoom, 30*zoom ), 
        [
          new THREE.MeshPhongMaterial( { color, flatShading: true } ), // back
          new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckFrontTexture } ),
          new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckRightSideTexture } ),
          new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckLeftSideTexture } ),
          new THREE.MeshPhongMaterial( { color, flatShading: true } ), // top
          new THREE.MeshPhongMaterial( { color, flatShading: true } ) // bottom
        ]
      );
      cabin.position.x = -40*zoom;
      cabin.position.z = 20*zoom;
      cabin.castShadow = true;
      cabin.receiveShadow = true;
      truck.add( cabin );
    
      const frontWheel = new Wheel();
      frontWheel.position.x = -38*zoom;
      truck.add( frontWheel );
    
      const middleWheel = new Wheel();
      middleWheel.position.x = -10*zoom;
      truck.add( middleWheel );
    
      const backWheel = new Wheel();
      backWheel.position.x = 30*zoom;
      truck.add( backWheel );
    
      return truck;  
    }
    
    function Three() {
      const three = new THREE.Group();
    
      const trunk = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 15*zoom, 15*zoom, 20*zoom ), 
        new THREE.MeshPhongMaterial( { color: 0x4d2926, flatShading: true } )
      );
      trunk.position.z = 10*zoom;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      three.add(trunk);
    
      height = threeHeights[Math.floor(Math.random()*threeHeights.length)];
    
      const crown = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 30*zoom, 30*zoom, height*zoom ), 
        new THREE.MeshLambertMaterial( { color: 0x7aa21d, flatShading: true } )
      );
      crown.position.z = (height/2+20)*zoom;
      crown.castShadow = true;
      crown.receiveShadow = false;
      three.add(crown);
    
      return three;  
    }
    
    function Chicken() {
      const chicken = new THREE.Group();
    
      const body = new THREE.Mesh(
        new THREE.BoxBufferGeometry( chickenSize*zoom, chickenSize*zoom, 20*zoom ), 
        new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } )
      );
      body.position.z = 10*zoom;
      body.castShadow = true;
      body.receiveShadow = true;
      chicken.add(body);
    
      const rowel = new THREE.Mesh(
        new THREE.BoxBufferGeometry( 2*zoom, 4*zoom, 2*zoom ), 
        new THREE.MeshLambertMaterial( { color: 0xF0619A, flatShading: true } )
      );
      rowel.position.z = 21*zoom;
      rowel.castShadow = true;
      rowel.receiveShadow = false;
      chicken.add(rowel);
    
      return chicken;  
    }
    
    function Road() {
      const road = new THREE.Group();
    
      const createSection = color => new THREE.Mesh(
        new THREE.PlaneBufferGeometry( boardWidth*zoom, positionWidth*zoom ), 
        new THREE.MeshPhongMaterial( { color } )
      );
    
      const middle = createSection(0x454A59);
      middle.receiveShadow = true;
      road.add(middle);
    
      const left = createSection(0x393D49);
      left.position.x = - boardWidth*zoom;
      road.add(left);
    
      const right = createSection(0x393D49);
      right.position.x = boardWidth*zoom;
      road.add(right);
    
      return road;
    }
    
    function Grass() {
      const grass = new THREE.Group();
    
      const createSection = color => new THREE.Mesh(
        new THREE.BoxBufferGeometry( boardWidth*zoom, positionWidth*zoom, 3*zoom ), 
        new THREE.MeshPhongMaterial( { color } )
      );
    
      const middle = createSection(0xbaf455);
      middle.receiveShadow = true;
      grass.add(middle);
    
      const left = createSection(0x99C846);
      left.position.x = - boardWidth*zoom;
      grass.add(left);
    
      const right = createSection(0x99C846);
      right.position.x = boardWidth*zoom;
      grass.add(right);
    
      grass.position.z = 1.5*zoom;
      return grass;
    }
    
    function Lane(index) {
      this.index = index;
      this.type = index <= 0 ? 'field' : laneTypes[Math.floor(Math.random()*laneTypes.length)];
    
      switch(this.type) {
        case 'field': {
          this.type = 'field';
          this.mesh = new Grass();
          break;
        }
        case 'forest': {
          this.mesh = new Grass();
    
          this.occupiedPositions = new Set();
          this.threes = [1,2,3,4].map(() => {
            const three = new Three();
            let position;
            do {
              position = Math.floor(Math.random()*columns);
            }while(this.occupiedPositions.has(position))
              this.occupiedPositions.add(position);
            three.position.x = (position*positionWidth+positionWidth/2)*zoom-boardWidth*zoom/2;
            this.mesh.add( three );
            return three;
          })
          break;
        }
        case 'car' : {
          this.mesh = new Road();
          this.direction = Math.random() >= 0.5;
    
          const occupiedPositions = new Set();
          this.vechicles = [1,2,3].map(() => {
            const vechicle = new Car();
            let position;
            do {
              position = Math.floor(Math.random()*columns/2);
            }while(occupiedPositions.has(position))
              occupiedPositions.add(position);
            vechicle.position.x = (position*positionWidth*2+positionWidth/2)*zoom-boardWidth*zoom/2;
            if(!this.direction) vechicle.rotation.z = Math.PI;
            this.mesh.add( vechicle );
            return vechicle;
          })
    
          this.speed = laneSpeeds[Math.floor(Math.random()*laneSpeeds.length)];
          break;
        }
        case 'truck' : {
          this.mesh = new Road();
          this.direction = Math.random() >= 0.5;
    
          const occupiedPositions = new Set();
          this.vechicles = [1,2].map(() => {
            const vechicle = new Truck();
            let position;
            do {
              position = Math.floor(Math.random()*columns/3);
            }while(occupiedPositions.has(position))
              occupiedPositions.add(position);
            vechicle.position.x = (position*positionWidth*3+positionWidth/2)*zoom-boardWidth*zoom/2;
            if(!this.direction) vechicle.rotation.z = Math.PI;
            this.mesh.add( vechicle );
            return vechicle;
          })
    
          this.speed = laneSpeeds[Math.floor(Math.random()*laneSpeeds.length)];
          break;
        }
      }
    }
    
    document.querySelector("#retry").addEventListener("click", () => {
      lanes.forEach(lane => scene.remove( lane.mesh ));
      initaliseValues();
      endDOM.style.visibility = 'hidden';
    });
    
    document.getElementById('forward').addEventListener("click", () => move('forward'));
    
    document.getElementById('backward').addEventListener("click", () => move('backward'));
    
    document.getElementById('left').addEventListener("click", () => move('left'));
    
    document.getElementById('right').addEventListener("click", () => move('right'));
    
    window.addEventListener("keydown", event => {
      if (event.keyCode == '38') {
        // up arrow
        move('forward');
      }
      else if (event.keyCode == '40') {
        // down arrow
        move('backward');
      }
      else if (event.keyCode == '37') {
        // left arrow
        move('left');
      }
      else if (event.keyCode == '39') {
        // right arrow
        move('right');
      }
    });
    
    function move(direction) {
      const finalPositions = moves.reduce((position,move) => {
        if(move === 'forward') return {lane: position.lane+1, column: position.column};
        if(move === 'backward') return {lane: position.lane-1, column: position.column};
        if(move === 'left') return {lane: position.lane, column: position.column-1};
        if(move === 'right') return {lane: position.lane, column: position.column+1};
      }, {lane: currentLane, column: currentColumn})
    
      if (direction === 'forward') {
        if(lanes[finalPositions.lane+1].type === 'forest' && lanes[finalPositions.lane+1].occupiedPositions.has(finalPositions.column)) return;
        if(!stepStartTimestamp) startMoving = true;
        addLane();
      }
      else if (direction === 'backward') {
        if(finalPositions.lane === 0) return;
        if(lanes[finalPositions.lane-1].type === 'forest' && lanes[finalPositions.lane-1].occupiedPositions.has(finalPositions.column)) return;
        if(!stepStartTimestamp) startMoving = true;
      }
      else if (direction === 'left') {
        if(finalPositions.column === 0) return;
        if(lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column-1)) return;
        if(!stepStartTimestamp) startMoving = true;
      }
      else if (direction === 'right') {
        if(finalPositions.column === columns - 1 ) return;
        if(lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column+1)) return;
        if(!stepStartTimestamp) startMoving = true;
      }
      moves.push(direction);
    }
    
    function animate(timestamp) {
      requestAnimationFrame( animate );
    
      if(!previousTimestamp) previousTimestamp = timestamp;
      const delta = timestamp - previousTimestamp;
      previousTimestamp = timestamp;
    
      // Animate cars and trucks moving on the lane
      lanes.forEach(lane => {
        if(lane.type === 'car' || lane.type === 'truck') {
          const aBitBeforeTheBeginingOfLane = -boardWidth*zoom/2 - positionWidth*2*zoom;
          const aBitAfterTheEndOFLane = boardWidth*zoom/2 + positionWidth*2*zoom;
          lane.vechicles.forEach(vechicle => {
            if(lane.direction) {
              vechicle.position.x = vechicle.position.x < aBitBeforeTheBeginingOfLane ? aBitAfterTheEndOFLane : vechicle.position.x -= lane.speed/16*delta;
            }else{
              vechicle.position.x = vechicle.position.x > aBitAfterTheEndOFLane ? aBitBeforeTheBeginingOfLane : vechicle.position.x += lane.speed/16*delta;
            }
          });
        }
      });
    
      if(startMoving) {
        stepStartTimestamp = timestamp;
        startMoving = false;
      }
    
      if(stepStartTimestamp) {
        const moveDeltaTime = timestamp - stepStartTimestamp;
        const moveDeltaDistance = Math.min(moveDeltaTime/stepTime,1)*positionWidth*zoom;
        const jumpDeltaDistance = Math.sin(Math.min(moveDeltaTime/stepTime,1)*Math.PI)*8*zoom;
        switch(moves[0]) {
          case 'forward': {
            const positionY = currentLane*positionWidth*zoom + moveDeltaDistance;
            camera.position.y = initialCameraPositionY + positionY; 
            dirLight.position.y = initialDirLightPositionY + positionY; 
            chicken.position.y = positionY; // initial chicken position is 0
    
            chicken.position.z = jumpDeltaDistance;
            break;
          }
          case 'backward': {
            positionY = currentLane*positionWidth*zoom - moveDeltaDistance
            camera.position.y = initialCameraPositionY + positionY;
            dirLight.position.y = initialDirLightPositionY + positionY; 
            chicken.position.y = positionY;
    
            chicken.position.z = jumpDeltaDistance;
            break;
          }
          case 'left': {
            const positionX = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 - moveDeltaDistance;
            camera.position.x = initialCameraPositionX + positionX;     
            dirLight.position.x = initialDirLightPositionX + positionX; 
            chicken.position.x = positionX; // initial chicken position is 0
            chicken.position.z = jumpDeltaDistance;
            break;
          }
          case 'right': {
            const positionX = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 + moveDeltaDistance;
            camera.position.x = initialCameraPositionX + positionX;       
            dirLight.position.x = initialDirLightPositionX + positionX;
            chicken.position.x = positionX; 
    
            chicken.position.z = jumpDeltaDistance;
            break;
          }
        }
        // Once a step has ended
        if(moveDeltaTime > stepTime) {
          switch(moves[0]) {
            case 'forward': {
              currentLane++;
              counterDOM.innerHTML = currentLane;    
              break;
            }
            case 'backward': {
              currentLane--;
              counterDOM.innerHTML = currentLane;    
              break;
            }
            case 'left': {
              currentColumn--;
              break;
            }
            case 'right': {
              currentColumn++;
              break;
            }
          }
          moves.shift();
          // If more steps are to be taken then restart counter otherwise stop stepping
          stepStartTimestamp = moves.length === 0 ? null : timestamp;
        }
      }
    
      // Hit test
      if(lanes[currentLane].type === 'car' || lanes[currentLane].type === 'truck') {
        const chickenMinX = chicken.position.x - chickenSize*zoom/2;
        const chickenMaxX = chicken.position.x + chickenSize*zoom/2;
        const vechicleLength = { car: 60, truck: 105}[lanes[currentLane].type]; 
        lanes[currentLane].vechicles.forEach(vechicle => {
          const carMinX = vechicle.position.x - vechicleLength*zoom/2;
          const carMaxX = vechicle.position.x + vechicleLength*zoom/2;
          if(chickenMaxX > carMinX && chickenMinX < carMaxX) {
            endDOM.style.visibility = 'visible';
          }
        });
    
      }
      renderer.render( scene, camera );	
    }
    
    requestAnimationFrame( animate );

    Final Output:

    Crossy Road Game Clone HTML CSS JavaScript.gif

    Conclusion:

    Congratulations on completing the tutorial and successfully creating your own Crossy Road game clone using HTML, CSS, and JavaScript Three.js! You have gained valuable knowledge and practical experience in game development, and you should be proud of your accomplishment.

    Throughout this tutorial, we covered essential concepts and techniques for building a game from scratch. You learned how to set up the development environment, create the game canvas, design game elements, implement game logic and interactions, add visual effects and animations, and finally, test and deploy your game.

    By following the step-by-step instructions and utilizing the code snippets provided, you were able to create a fully functional game that captures the essence of Crossy Road. You implemented responsive controls, engaging gameplay mechanics, and captivating visual effects using Three.js, a powerful JavaScript library for 3D graphics.

    However, this tutorial is just the beginning of your game development journey. There are endless possibilities for further enhancements and customization. You can experiment with different game elements, such as adding new obstacles, power-ups, or even introducing different game modes. Let your creativity soar and make the game truly your own.

    Remember the importance of testing and debugging your game to ensure a smooth and enjoyable experience for the players. Test your game on different devices and browsers to ensure compatibility and optimize performance.

    Now that you have completed this tutorial, you have a solid foundation in game development using HTML, CSS, and JavaScript Three.js. This knowledge can be applied to future game projects or expanded upon to create even more complex and immersive games.

    We hope you enjoyed this tutorial and found it helpful in your game development endeavors. Game development is an exciting and ever-evolving field, so continue to explore, learn, and challenge yourself. Good luck with your future game development projects, and we can't wait to see the amazing games you create!

    Happy gaming and coding!

    That’s a wrap!

    I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.

    Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.

    And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

    Thanks!
    Faraz 😊

    End of the article

    Subscribe to my Newsletter

    Get the latest posts delivered right to your inbox


    Latest Post