Build a 3D HTML5 game like “Stairs” using Phaser and Three.js – Step 2: adding the bouncing ball just using trigonometry

Read all posts about "" game

Here we go with the second step of HTML5 “Stairs” prototype built with Phaser and Three.js.

In first step we built an endless staircase with randomly positioned spikes, now it’s time to add the bouncing ball.

How can we add a bouncing ball? It’s a Three SphereGeometry controlled by Phaser, but the creation is easy. The hard part comes when you have to make it jump over steps.

As explained in previous step, there isn’t any endless staircase, just a few steps moving towards the camera, leaving the screen from the bottom just to appear once again from the top.

So, if steps are moving, the ball shouldn’t move along z axis, but only up and down along y axis.

The hard part was syncronizing ball jump with steps movement, since there isn’t any physics engine or tween system to manage animations, and everything is made just using the delta time between two frames.

All I needed to do is calculate the time needed for a step to take the place of previous step, and make the ball move up and down in that time.

I used a sine curve to determine ball height, because at a high speed it simulates quite good the movement of a jumping ball.

Look at the result:

I also modified the size of the steps and added some more lights and shadows to emphasize the 3D effect.

All this stuff in one HTML file, one CSS file and five 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 recycled
    stepsAmount : 18,
 
    // staircase speed, in pixels per second
    staircaseSpeed : 80,

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

    // ball diameter, in pixels
    ballDiameter : 60,

    // ball starting step
    ballStartingStep : 2,

    // ball jump height, in pixels
    jumpHeight : 100
}

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';
import { ThreeBall } from './threeBall';

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

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

    // the ball
    ball : ThreeBall;

    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));
        }    

        // add the ball
        this.ball = new ThreeBall(this, threeScene)
    }

    // 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 directional light
        const directionalLight : THREE.DirectionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
        directionalLight.castShadow = true;
        directionalLight.position.set(270, 200, 0);
        directionalLight.target.position.set(270, 100, -1000);
        threeScene.add(directionalLight);
        threeScene.add(directionalLight.target)
       
        // add a spotlight
        const spotLight : THREE.SpotLight = new THREE.SpotLight(0xffffff, 0.2, 0, 0.4, 0.5, 0.1);
        spotLight.position.set(270, 1000, 0);
        spotLight.castShadow = true;
        spotLight.shadow.mapSize.width = 1024;
        spotLight.shadow.mapSize.height = 1024;
        spotLight.shadow.camera.near = 1;
        spotLight.shadow.camera.far = 10000;
        spotLight.shadow.camera.fov = 80;
        spotLight.target.position.set(270, 0, -320);
        threeScene.add(spotLight);
        threeScene.add(spotLight.target);

        // 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 {

        // update ball position
        this.ball.updateBallPosition(deltaTime);

        // 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 : 0x09c4fe
        });
        const step : THREE.Mesh = new THREE.Mesh(stepGeometry, stepMaterial);
        step.receiveShadow = true;

        // build the spike
        const spikeGeometry = new THREE.ConeGeometry(25, 40, 32);
        const spikeMaterial = new THREE.MeshStandardMaterial({
            color: 0x444444
        });
        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 * -GameOptions.stepSize.z);

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

threeBall.ts

Custom class extending THREE.Mesh to define the ball.

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

// class to define the ball, extends THREE.Mesh
export class ThreeBall extends THREE.Mesh {

    // amount of time the ball is in play, useful to determine its position 
    ballTime : number;

    // time needed for the ball to jump over next step
    jumpTime : number;

    // ball starting y position
    startY : number;
   
    constructor(scene : Phaser.Scene, threeScene : THREE.Scene) {

        // build the ball
        const SphereGeometry : THREE.SphereGeometry = new THREE.SphereGeometry(GameOptions.ballDiameter / 2, 32, 32);
        const sphereMaterial : THREE.MeshStandardMaterial = new THREE.MeshStandardMaterial({
            color : 0x444444
        });
        super(SphereGeometry, sphereMaterial);
        
        // ball casts shadows
        this.castShadow = true;

        // ball is in time for zero milliseconds at the moment
        this.ballTime = 0;

        // jump time, in seconds, is determined by y step size divided by staircase speed
        this.jumpTime = GameOptions.stepSize.y / GameOptions.staircaseSpeed * 1000;

        // determine ball starting y position according to step size, ball diameter, and ball starting step
        this.startY = (GameOptions.ballStartingStep + 0.5) * GameOptions.stepSize.y + GameOptions.ballDiameter / 2

        // position the ball properly
        this.position.set(scene.game.config.width as number / 2, this.startY , GameOptions.ballStartingStep * -GameOptions.stepSize.z);

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

    // method to update ball position according to the time the ball is in play
    updateBallPosition(delta : number) : void {

        // determine ball time, being sure it will never be greater than the time required to jump on next step
        this.ballTime = (this.ballTime += delta) % this.jumpTime;

        // ratio is the amount of time passed divided by the time required to jump on next step
        let ratio : number = this.ballTime / this.jumpTime;

        // set ball y position using a sine curve
        this.position.setY(this.startY + Math.sin(ratio * Math.PI) * GameOptions.jumpHeight);
    }
}

In next step we’ll see how to control the ball, meanwhile download the source code.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers