Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Stairs game, 3D, Game development, HTML5, Javascript, Phaser and TypeScript.

If you are following my tutorial series about the making of “Stairs” game, you already saw the first part built with Godot Engine: I was able to create an endless staircase in a few lines of code, reciclying steps to save resources.

All posts in this tutorial series:

Step 1: Creation of an endless staircase with random spikes.

Step 2: Adding a bouncing ball without using any physics engine.

Step 3: Controlling the bouncing ball and adding more spikes.

Step 4: Collision detection without using physics, improved controls with virtual trackpad and level progression.

Step 5: Using latest Three.js version, moving steps, adding bonuses to collect and displaying score.

Step 6: Final touches and settings to automatically increase difficulty.

But Godot is not the only free tool capable of building games like Stairs, so it’s time to see how to create a 3D endless stair with Phaser and Three.js.

Unfortunately it’s not easy to make Phaser work with the latest version of Three.js, as you can see in my Twitter’s posts #1#2 and #3, but this did not discourage me and I was able to fix things building a version of Bouncy Light with Phaser and Three.js, I just had to use an older Three.js version: Three.js v 0.109 which is three years old but at least it works without workarounds.

So now I am able to show you the endless Phaser + Three staircase, and I also added random spikes on each step, as well as a fog effect:

As in most endless runners, remember the camera is not moving along the stairs, but steps are moving towards the camera.

When a step leaves camera view from the bottom, it returns to the top of the staircase.

All this stuff in one HTML file, one CSS file and four TypeScript files, let’s have a look at them:

index.html

The web page which hosts the game, to be run inside thegame element.

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></script> 
    </head>
    <body>   
        <div id = "thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

body {
    background-color: #011025;    
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

gameOptions.ts

Configurable game options. It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game.

// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions = {

    // amount of steps to be created and recicled
    stepsAmount : 18,
 
    // staircase speed
    staircaseSpeed : 50,

    // step size: x, y, z
    stepSize : new Phaser.Math.Vector3(400, 40, 80)
}

main.ts

This is where the game is created, with all Phaser related options.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 540,
    height : 960
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.WEBGL,
    transparent: true,
    backgroundColor : 0x011025,
    scale : scaleObject,
    scene : [PlayGame]
}

// the game itself
new Phaser.Game(configObject);

playGame.ts

Main game file, all game logic and Three integration is stored here.

// THE GAME ITSELF

import * as THREE from 'three';
import { GameOptions } from './gameOptions';
import { ThreeStep } from './threeStep';

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    // step to build the staircase
    steps : ThreeStep[];

    constructor() {
        super({
            key: 'PlayGame'
        });
    }

    // method to be executed when the scene has been created
    create() : void {

        // creation of the 3D scene
        const threeScene : THREE.Scene = this.create3DWorld();

        // add steps to the 3D scene and into steps array
        this.steps = [];
        for (let i = 0; i < GameOptions.stepsAmount; i ++) {
            this.steps.push(new ThreeStep(this, threeScene, i));
        }    
    }

    // method to build the 3D scene
    create3DWorld() : THREE.Scene {

        // variables to store canvas width and height
        const width : number = this.game.config.width as number;
        const height : number = this.game.config.height as number;

        // create a new THREE scene
        const threeScene : THREE.Scene = new THREE.Scene();

        // create the renderer
        const renderer : THREE.WebGLRenderer = new THREE.WebGLRenderer({
            canvas: this.sys.game.canvas,
            context: this.sys.game.context as WebGLRenderingContext,
            antialias: true
        });
        renderer.autoClear = false;
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;

        // add a camera
        const camera  = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
        camera.position.set(width / 2, 720, 640);
        camera.lookAt(width / 2 , 560, 320);

        // add an ambient light
        const ambientLight : THREE.AmbientLight = new THREE.AmbientLight(0xffffff, 1);
        threeScene.add(ambientLight);
        
        // add a spotlight
        const spotLight : THREE.SpotLight = new THREE.SpotLight(0xffffff, 1, 0, 0.4, 0.5, 0.1);
        spotLight.position.set(0, height * 3, 0);
        spotLight.castShadow = true;
        spotLight.shadow.mapSize.width = 512;
        spotLight.shadow.mapSize.height = 512;
        spotLight.shadow.camera.near = 1;
        spotLight.shadow.camera.far = 10000;
        threeScene.add(spotLight);

        // add a fog effect
        const fog : THREE.Fog = new THREE.Fog(0x011025, 500, 2000);
        threeScene.fog = fog;

        // create an Extern Phaser game object
        const view : Phaser.GameObjects.Extern = this.add.extern();

        
        // custom renderer
        // next line is needed to avoid TypeScript errors
        // @ts-expect-error
        view.render = () => {
            renderer.state.reset();
            renderer.render(threeScene, camera);
        };        
        return threeScene;
    }

    // method to be executed at each frame 
    update(time : number, deltaTime : number) : void {

        // loop through steps array
        this.steps.forEach((step : ThreeStep) => {

            // adjust step position according to speed, delta time and step size
            step.position.y -= deltaTime / 1000 * GameOptions.staircaseSpeed;
            step.position.z += deltaTime / 1000 * GameOptions.staircaseSpeed * GameOptions.stepSize.z / GameOptions.stepSize.y;
           
            // if the step is leaving the game from the bottom...
            if (step.position.y < - 40) {

                // ...place it on top of the staircase
                step.position.y += GameOptions.stepsAmount * GameOptions.stepSize.y;
                step.position.z -= GameOptions.stepsAmount * GameOptions.stepSize.z;

                // change spike position
                step.children[1].position.x = Phaser.Math.Between(-150, 150)
            }
        })
    }
}

threeStep.ts

Custom class extending THREE.Group to define the step, which is made of two Three meshes: one with a box geometry for the step itself, and one with a cone geometry for the spike.

import { GameOptions } from './gameOptions';
import * as THREE from 'three';

// class to define the step, extends THREE.Group
export class ThreeStep extends THREE.Group {
   
    constructor(scene : Phaser.Scene, threeScene : THREE.Scene, stepNumber : number) {
        super();
        
        // build the step
        const stepGeometry : THREE.BoxGeometry = new THREE.BoxGeometry(GameOptions.stepSize.x, GameOptions.stepSize.y, GameOptions.stepSize.z);
        const stepMaterial : THREE.MeshStandardMaterial = new THREE.MeshStandardMaterial({
            color : 0x00ff00
        });
        const step : THREE.Mesh = new THREE.Mesh(stepGeometry, stepMaterial);
        step.receiveShadow = true;

        // build the spike
        const spikeGeometry = new THREE.ConeGeometry(15, 50, 32);
        const spikeMaterial = new THREE.MeshStandardMaterial({
            color: 0xff0000
        });
        let spike = new THREE.Mesh(spikeGeometry, spikeMaterial);
        spike.position.set(Phaser.Math.Between(-GameOptions.stepSize.x / 2 + 50, GameOptions.stepSize.x / 2 - 50), GameOptions.stepSize.y - 5, 0);
        spike.castShadow = true;

        // add step and spike to the group
        this.add(step, spike);

        // position the group properly
        this.position.set(scene.game.config.width as number / 2, stepNumber * GameOptions.stepSize.y, stepNumber * -80);

        // add the group to the scene
        threeScene.add(this);
    }
}

And with these few lines, we got our endless staircase. Next time, we’ll add the player, meanwhile download the source code.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.