Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Circular endless runner game, Game development, HTML5, Javascript, Phaser and TypeScript.

In my opinion the circular endless runner prototype has potential, so I decided to update it to latest Phaser version, rewrite it in TypeScript and add some optimization.

No physics engines have been used, everything is controlled by trigonometry and geometry.

Look at the result:

Tap or click to jump and avoid the spikes. You can perform double and even triple jump.

All spikes are recycled and once they disappear from one quadrant of the circle, they appear on the next quadrant.

And here it is the completely commented source code, which consists in one HTML file, one CSS file and six TypeScript files:

index.html

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

HTML
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, 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.

CSS
/* remove margin and padding from all elements */
* {
    padding : 0;
    margin : 0;
}

/* set body background color */
body {
    background-color : #000000;    
}

/* Disable browser handling of all panning and zooming gestures. */
canvas {
    touch-action : none;
}

gameOptions.ts

Configurable game options.

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

export const GameOptions : any = {

    gameSize : {
        width               : 800,              // width of the game, in pixels
        height              : 800               // height of the game, in pixels
    },

    gameBackgroundColor     : 0x222222,         // game background color
    bigCircleRadius         : 250,              // radius of the big circle - the "planet" - in pixels
    playerRadius            : 25,               // radius of the small circle, in pixels
    playerSpeed             : 60,               // player speed, in degrees per second
    worldGravity            : 2500,             // world gravity
    jumpForce               : [600, 500, 400],  // jump force. First element is the first jump, second element - if any -  the double jump, third element - if any - the triple jump and so on
    spikeSize               : 50,               // size of the square where the spike, which is an isosceles triangle, is inscribed
    spikeHeightRange        : [4, 40],          // spikes can have different height, which range is set in this array
    closeToSpike            : 20,               // distance needed to consider the player close to a spike, in degrees. Useful for colliision detection
    farFromSpike            : 35,               // distance needed to consider the player far from a spike, in degrees. Useful for colliision detection
    spikesPerQuadrant       : 4                 // amount of spikes for each quadrant
}

main.ts

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

TypeScript
// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';                            // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame';           // playGame scene
import { GameOptions } from './gameOptions';            // game options

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode        : Phaser.Scale.FIT,                     // adjust size to automatically fit in the window
    autoCenter  : Phaser.Scale.CENTER_BOTH,             // center the game horizontally and vertically
    parent      : 'thegame',                            // DOM id where to render the game
    width       : GameOptions.gameSize.width,           // game width, in pixels
    height      : GameOptions.gameSize.height           // game height, in pixels
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = { 
    type            : Phaser.AUTO,                      // game renderer
    backgroundColor : GameOptions.gameBackgroundColor,  // game background color
    scale           : scaleObject,                      // scale settings
    scene           : [                                 // array with game scenes
        PreloadAssets,                                  // PreloadAssets scene
        PlayGame                                        // PlayGame scene
    ]
}

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

scenes > preloadAssets.ts

Here we preload all assets to be used in the game.

TypeScript
// CLASS TO PRELOAD ASSETS

// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
  
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
  
    // method to be called during class preloading
    preload() : void {
 
        // load images
        this.load.image('bigcircle', 'assets/sprites/bigcircle.png');   // the big circle, aka the planet
        this.load.image('player', 'assets/sprites/player.png');         // the player
        this.load.image('spike', 'assets/sprites/spike.png');           // the spike
        this.load.image('particle', 'assets/sprites/particle.png');     // a small circle used for particle effects        
    }
  
    // method to be executed when the scene is created
    create() : void {

        // start PlayGame scene
        this.scene.start('PlayGame');
    }
}

scenes > playGame.ts

Main game file, all game logic is stored here.

TypeScript
// THE GAME ITSELF

// modules to import
import { GameOptions } from '../gameOptions';       // game options   
import { PhysicsCircle } from '../physicsCircle';   // physics circle
import { PhysicsSpike } from '../physicsSpike';     // physics spike

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

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

    player          : PhysicsCircle;                                // the player
    spikeArray      : PhysicsSpike[];                               // group with all spikes
    trailEmitter    : Phaser.GameObjects.Particles.ParticleEmitter  // trail particle emitter

    // method to be called once the instance has been created
    create() : void {

        // set some custom data
        this.data.set({
            gameOver    : false,                                // is the game over?
            centerX     : this.game.config.width as number / 2, // horizontal center of the game
            centerY     : this.game.config.height as number / 2 // vertical center of the game
        })

        // place and resize big circle
        const bigCircle : Phaser.GameObjects.Sprite = this.add.sprite(this.data.get('centerX'), this.data.get('centerY'), 'bigcircle');
        bigCircle.displayWidth = GameOptions.bigCircleRadius * 2;
        bigCircle.displayHeight = GameOptions.bigCircleRadius * 2;

        // initialize spikeArray vector
        this.spikeArray = [];

        // place spikes
        for (let i : number = 0; i < 3 * GameOptions.spikesPerQuadrant; i ++) {
 
            // create a spike
            const spike : PhysicsSpike = new PhysicsSpike(this);
 
            // addi the spike to spike array
            this.spikeArray.push(spike);

            // determine the quadrant where to place the spike
            // 0 : bottom-right
            // 1 : bottom-left
            // 2 : top-left
            // 3 : top-right
            const quadrant : number = Math.floor(i % 3);

            // place the spike in the given quadrant.
            // I don't want spikes in the quadrant 2 at the beginning, because it's the quadrant where the player spawns
            spike.place(quadrant != 2 ? quadrant : 3);  
        }
 
        // create the player
        this.player = new PhysicsCircle(this);

        // create the particle trail emitter and make it follow the player
        this.trailEmitter = this.add.particles(this.player.x, this.player.y, 'particle', {
            
            lifespan    : 900,  // particle life span, in mmilliseconds
            alpha : {
                start   : 1,    // alpha start value, 0 = transparent; 1 = opaque
                end     : 0     // alpha end value
            },
            scale : {
                start   : 1,    // scale start value
                end     : 0.8   // scale end value
            },
            quantity    : 1,    // amount of particle to be fired
            frequency   : 150   // particle frequency, in milliseconds
        }).startFollow(this.player);
      
        // handle player input on touch or clicl
        this.input.on('pointerdown', () => {

            // make player jump
            this.player.jump();
        });
    }

    // method to be called when the game is over
    gameOver() : void {

        // set gameOver data to true
        this.data.set('gameOver', true);

        // shake the camera
        this.cameras.main.shake(800, 0.01);
                   
        // hide the player
        this.player.setVisible(false);

        // stop trail emitter
        this.trailEmitter.stop();

        // add a particle explosion effect
        this.add.particles(this.player.x, this.player.y, 'particle', {
            lifespan    : 900,  // particle life span, in milliseconds
            alpha : {
                start   : 1,    // alpha start value, 0 = transparent; 1 = opaque
                end     : 0     // alpha end value
            },
            scale : {
                start   : 0.6,  // scale start value
                end     : 0.02  // scale end value
            },
            speed: {
                min     : -150, // minimum speed, in pixels per second
                max     : 150   // maximum speed, in pixels per second
            },
        }).explode(100);

        // add a timer event
        this.time.addEvent({
            delay       : 2000,     // delay, in milliseconds
            callback    : () => {   // callback function
                
                // start PlayGame scene
                this.scene.start('PlayGame');
            }
        });
    }

    // metod to be called at each frame
    // time : time passed since the beginning, in milliseconds
    // deltaTime : time passed since last frame, in milliseconds
    update(time : number, deltaTime : number) {

        // if the game is over, do nothing
        if (this.data.get('gameOver')) {
            return;
        }

        // move the player, according to deltaTime
        this.player.move(deltaTime / 1000);

        // loop through all spikes
        this.spikeArray.forEach((spike : PhysicsSpike) => {

            // get angle difference between spike and player
            const angleDifference : number = Math.abs(Phaser.Math.Angle.ShortestBetween(spike.angle, this.player.currentAngle));
            
            // if the player is not approaching the spike and it's close enough...
            if (!spike.approaching && angleDifference < GameOptions.closeToSpike) {

                // player is now approaching the spike
                spike.approaching = true;     
            }

            // if the player is approaching the spike...
            if (spike.approaching) {

                // if spike triangle shape and player circle shape intersect...
                if (Phaser.Geom.Intersects.TriangleToCircle(spike.triangle, this.player.circle)) {
                    
                    // the game is over!
                    this.gameOver();
                }
                
                // if we are getting too far from the spike...
                if (angleDifference > GameOptions.farFromSpike) {

                    // recycle the spike making it disappear
                    spike.disappear();
                }
            }
        });
    }
}

physicsCircle.ts

Custom class for the physics circle.

TypeScript
// THE PHYSICS CIRCLE

// modules to import
import { GameOptions } from './gameOptions';    // game options

// PhysicsCircle class extends Phaser.GameObjects.Sprite
export class PhysicsCircle extends Phaser.GameObjects.Sprite {
    
    currentAngle    : number;               // current angle around the planet
    jumpHeight      : number;               // height reached while jumping
    jumps           : number;               // amount of consecutive jumps already performed
    jumpForce       : number;               // jump force to be applied at each frame
    circle          : Phaser.Geom.Circle;   // geometric circle representing the sprite, useful for collision detection   

    constructor(scene : Phaser.Scene) {
        
        // create and add the instance
        super(scene, 0, 0, 'player');
        scene.add.existing(this);

        // adjust display size according to game options
        this.displayWidth = GameOptions.playerRadius * 2;
        this.displayHeight = GameOptions.playerRadius * 2;

        // player starts from -150 degrees
        this.currentAngle = -150;

        // jump height, force and amount of jumps start at zero
        this.jumpHeight = 0;
        this.jumps = 0;
        this.jumpForce = 0;

        // geometric circle definition
        this.circle = new Phaser.Geom.Circle(0, 0, GameOptions.playerRadius);

        // set circle depth
        this.setDepth(2);
    }

    // method to be called to make the circle jump
    jump() : void {

        // can the player jump?
        if (this.jumps < GameOptions.jumpForce.length) {
 
            // player is jumping once more
            this.jumps ++; 

            // adding the proper jump force to player's jumpForce property
            // is jumpForce greater than zero? That is, is the player still gaining height?
            if (this.jumpForce > 0) {

                // then add the proper jump force to current force to make the player jump higher
                this.jumpForce += GameOptions.jumpForce[this.jumps - 1];
            }

            // in this case the player is falling...
            else {

                // so the jump force is set to make player gain height once more
                this.jumpForce = GameOptions.jumpForce[this.jumps - 1];    
            }   
        }
    }

    // method to move the player
    // s : seconds passed since last frame 
    move(s : number) : void {

        // is the player jumping?
        if (this.jumps > 0) {
 
            // adjust player jump height
            this.jumpHeight += this.jumpForce * s;

            // decrease jump force due to gravity
            this.jumpForce -= GameOptions.worldGravity * s;

            // if jumpHeight is less than zero, it means the player touched the ground
            if (this.jumpHeight < 0) {

                // setting jump height to zero
                this.jumpHeight = 0;

                // player is not jumping anymore
                this.jumps = 0;

                // there is no jump force
                this.jumpForce = 0;
            }
        }

        // deltaAnge is the amount in degrees gained in "s" seconds
        const deltaAngle : number = GameOptions.playerSpeed * s;

        // adjust current angle according to delta angle
        this.currentAngle = Phaser.Math.Angle.WrapDegrees(this.currentAngle + deltaAngle);  

        // also get the current angle in radians
        const radians : number = Phaser.Math.DegToRad(this.currentAngle); 
        
        // get the distance from center according  to big circle radius, player radius and jump height
        const distanceFromCenter : number = GameOptions.bigCircleRadius + GameOptions.playerRadius + this.jumpHeight;
 
        // adjust player position
        this.setPosition(this.scene.data.get('centerX') + distanceFromCenter * Math.cos(radians), this.scene.data.get('centerY') + distanceFromCenter * Math.sin(radians));
        
        // also adjust geometric circle position
        this.circle.setPosition(this.x, this.y);

        // determine player revolution around the big circle
        const revolutions : number = GameOptions.bigCircleRadius / GameOptions.playerRadius + 1;

        // adjust player rotation
        this.setAngle(this.currentAngle * revolutions);  
    }

}

physicsSpike.ts

Custom class for the physics spike.

TypeScript
// THE PHYSICS SPIKE

// modules to import
import { GameOptions } from './gameOptions';    // game options

// PhysicsSpike class extends Phaser.GameObjects.Sprite
export class PhysicsSpike extends Phaser.GameObjects.Sprite {

    quadrant    : number;                   // spike's quadrant
    triangle    : Phaser.Geom.Triangle;     // geometric triangle representing the spike
    approaching : boolean;                  // is the spike being approached by the player?

    constructor(scene : Phaser.Scene) {
        
        // create and add the instance
        super(scene, 0, 0, 'spike');
        scene.add.existing(this);

        // set registration point to left, vertical center
        this.setOrigin(0, 0.5);   
    }

    // method to place a spike
    // quadrant : quadrant where to place the spike
    // 0 : bottom-right
    // 1 : bottom-left
    // 2 : top-left
    // 3 : top-right
    place(quadrant : number) : void {
        
        // choose a random angle in the proper quadrant
        const randomAngle : number = Phaser.Math.Angle.WrapDegrees(Phaser.Math.Between(quadrant * 90, (quadrant + 1) * 90));
 
        // this is the same random angle converted in radians
        const randomAngleRadians : number = Phaser.Math.DegToRad(randomAngle);

        // set spike start position, completely inside the big circle
        const spikeStartX : number = this.scene.data.get('centerX') + (GameOptions.bigCircleRadius - this.displayWidth) * Math.cos(randomAngleRadians);
        const spikeStartY : number = this.scene.data.get('centerY') + (GameOptions.bigCircleRadius - this.displayWidth) * Math.sin(randomAngleRadians);

        // place the spike in proper position
        this.setPosition(spikeStartX, spikeStartY);
 
        // determine spike position final according to its angle
        var spikeX = this.scene.data.get('centerX') + (GameOptions.bigCircleRadius - Phaser.Math.Between(GameOptions.spikeHeightRange[0], GameOptions.spikeHeightRange[1])) * Math.cos(randomAngleRadians);
        var spikeY = this.scene.data.get('centerY') + (GameOptions.bigCircleRadius - Phaser.Math.Between(GameOptions.spikeHeightRange[0], GameOptions.spikeHeightRange[1])) * Math.sin(randomAngleRadians);

        // add a tween to make spike appear
        this.scene.tweens.add({
            targets     : this,                         // tween target, the spike itself
            x           : spikeX,                       // final x position
            y           : spikeY,                       // final y position
            duration    : 500,                          // tween duration in milliseconds
            ease        : Phaser.Math.Easing.Cubic.Out  // tween ease
        })

        // save spike's quadrant in a custom property
        this.quadrant = quadrant;
 
        // set spike angke
        this.angle = randomAngle;

        // build the geometric triangle which will be used for collision
        this.triangle = new Phaser.Geom.Triangle(
            spikeX + GameOptions.spikeSize * Math.cos(randomAngleRadians),
            spikeY + GameOptions.spikeSize * Math.sin(randomAngleRadians),
            spikeX + GameOptions.spikeSize / 2 * Math.cos(randomAngleRadians + Math.PI / 2),
            spikeY + GameOptions.spikeSize / 2 * Math.sin(randomAngleRadians + Math.PI / 2),
            spikeX + GameOptions.spikeSize / 2 * Math.cos(randomAngleRadians - Math.PI / 2),
            spikeY + GameOptions.spikeSize / 2 * Math.sin(randomAngleRadians - Math.PI / 2));

        // is the player approaching to the spike?
        this.approaching = false;
    }

    // method to make the spike disappear
    disappear() : void {

        // the spike is not being approached by player
        this.approaching = false;

        // set spike start position, completely inside the big circle
        const spikeStartX : number = this.scene.game.config.width as number / 2 + (GameOptions.bigCircleRadius - this.displayWidth) * Math.cos(this.rotation);
        const spikeStartY : number = this.scene.game.config.height as number / 2 + (GameOptions.bigCircleRadius - this.displayWidth) * Math.sin(this.rotation);
        
        // add a tween to make spike disappear into its starting position
        this.scene.tweens.add({
            targets     : this,                         // tween target, the spike itself
            x           : spikeStartX,                  // final x position
            y           : spikeStartY,                  // final y position
            duration    : 500,                          // tween duration, in milliseconds
            ease        : Phaser.Math.Easing.Cubic.In,  // tween ease
            onComplete  : () => {                       // function to be called once the tween is complete
                
                // place the spike in the previous quadrant
                this.place((this.quadrant + 3) % 4);
            }
        })
    }
}

And now you have a fully functional circular endless runner engine. Download the source code along with the entire wepack project.

Don’t know where to start developing with Phaser and TypeScript? I’ll explain it to you step by step in this free minibook.

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