Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Mini Archer game, Game development, HTML5, Javascript, Phaser and TypeScript.

Mini Archer is a simple little mobile game by Nanovation Labs available on Apple App Store and it’s a good game to port to HTML5 using Phaser.

All posts in this tutorial series:

Step 1: Creation of an endless terrain with infinite randonly generated targets.

Step 2: Adding a running character with more animations.

Step 3: Adding a bow using a Graphics GameObject.

Step 4: Adding the arrow.

Step 5: Firing the arrow.

Step 6: Splitting the code into classes.

You simply have to shoot arrows and hit the target with an endless runner gameplay.

In this first part of the tutorial, I am going to create an endless terrain with randomly generated infinite targets.

As in most endless runners, I am not moving the camera. It’s the whole environment which moves, and I just recycle assets when they leave the canvas from the left.

The assets used in this prototype are taken from this free collection by Bayat Games.

Look at the result:

Terrian is continuoulsy scrolling using a tween, and infinite targets appear.

Let’s have a look at the commented source code: we have one HTML file, one CSS file and four TypeScript files. Phaser version used here is 3.60, beta 13.

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

    // terrain start, in screen height ratio, where 0 = top, 1 = bottom
    terrainStart : 0.6,

    // target position range, in screen width ratio, where 0 = left, 1 = tight
    targetPositionRange : {
        from : 0.5,
        to : 0.9
    },

    // target height range, in pixels
    targetHeightRange : {
        from : 150,
        to : 350
    },

    // number of rings
    rings : 5,

    // ring ratio, to make target look oval, this is the ratio of width compared to height
    ringRatio : 0.8,

    // ring colors, from external to internal
    ringColor : [0xffffff, 0x5cb6f8, 0xe34d46, 0xf2aa3c, 0x95a53c],

    // ring radii, from external to internal, in pixels
    ringRadius : [45, 35, 35, 25, 15],

    // tolerance of ring radius, can be up to this ratio bigger or smaller 
    ringRadiusTolerance : 0.5

}

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 { PreloadAssets } from './preloadAssets';
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.AUTO,
    backgroundColor : 0x5df4f0,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

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

preloadAssets.ts

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

// CLASS TO PRELOAD ASSETS
 
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
 
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
 
    // method to be execute during class preloading
    preload() : void {

        // this is how we preload a bitmap font
        this.load.image('circle', 'assets/sprites/circle.png');
        this.load.image('grasstile', 'assets/sprites/grasstile.png');
        this.load.image('dirttile', 'assets/sprites/dirttile.png');
        this.load.image('pole', 'assets/sprites/pole.png');
        this.load.image('poletop', 'assets/sprites/poletop.png');
    }
 
    // method to be called once the instance has been created
    create() : void {
 
        // call PlayGame class
        this.scene.start('PlayGame');
    }
}

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import { GameOptions } from './gameOptions';

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

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

    // terrain
    terrain : Phaser.GameObjects.TileSprite;

    // dirt below the terrain
    dirt : Phaser.GameObjects.TileSprite;

    // pole
    pole : Phaser.GameObjects.TileSprite;

    // topmost part of the pole
    poleTop : Phaser.GameObjects.Sprite;

    // pole shadow
    poleShadow : Phaser.GameObjects.Sprite;

    // target shadow
    targetShadow : Phaser.GameObjects.Sprite;

    // target rigns
    targetRings : Phaser.GameObjects.Sprite[];

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

        // add terrain
        let terrainStartY : number = this.game.config.height as number * GameOptions.terrainStart;
        this.terrain = this.add.tileSprite(0, terrainStartY, this.game.config.width as number + 256, 256, 'grasstile');
        this.terrain.setOrigin(0, 0);

        // add dirt, the graphics below the terrain
        let dirtStartY : number = terrainStartY + 256; 
        this.dirt = this.add.tileSprite(0, dirtStartY, this.terrain.width, this.game.config.height as number - dirtStartY, 'dirttile');
        this.dirt.setOrigin(0, 0);

        // add a circle which represents target shadow
        this.targetShadow = this.add.sprite(0, 0, 'circle');
        this.targetShadow.setTint(0x676767);

        // add pole shadow
        let poleYPos : number = terrainStartY + 38;
        this.poleShadow = this.add.sprite(0, poleYPos, 'circle');
        this.poleShadow.setTint(0x000000);
        this.poleShadow.setAlpha(0.2);
        this.poleShadow.setDisplaySize(90, 20);

        // add pole
        this.pole = this.add.tileSprite(0, poleYPos, 32, 0, 'pole');
        this.pole.setOrigin(0.5, 1); 

        // add pole top
        this.poleTop = this.add.sprite(0, 0, 'poletop');
        this.poleTop.setOrigin(0.5, 1);

        // add circles which represent the various target circles
        this.targetRings = [];
        for (let i : number = 0; i < GameOptions.rings; i ++) {  
            this.targetRings[i] = this.add.sprite(0, 0, 'circle');
        }

        // place a random target at current position
        this.placeTarget(this.game.config.width as number * 2, this.pole.y);

        // tween the target to a random position
        this.tweenTarget(this.getRandomPosition());
    }

    // simple metod to get a random target position
    getRandomPosition() : number {
        return Math.round(Phaser.Math.FloatBetween(GameOptions.targetPositionRange.from, GameOptions.targetPositionRange.to) * (this.game.config.width as number));
    }

    // method to place the target at (posX, posY)
    placeTarget(posX : number, posY : number) : void {

        // array where to store radii values
        let ringRadii : number[] = [];

        // determine radii values according to default radius size and tolerance
        for (let i : number = 0; i < GameOptions.rings; i ++) {
            ringRadii[i] = Math.round(GameOptions.ringRadius[i] + (GameOptions.ringRadius[i] * Phaser.Math.FloatBetween(0, GameOptions.ringRadiusTolerance) * Phaser.Math.RND.sign()));
        }

        // get the sum of all radii, this will be the size of the target
        let radiiSum : number = ringRadii.reduce((sum, value) => sum + value, 0);

        // determine target height
        let targetHeight : number = posY - Phaser.Math.Between(GameOptions.targetHeightRange.from, GameOptions.targetHeightRange.to) 
        
        // set pole shadow x poisition
        this.poleShadow.setX(posX);

        // set pole x position
        this.pole.setX(posX);

        // set pole height
        this.pole.height = posY - targetHeight;

        // set pole top position
        this.poleTop.setPosition(posX, this.pole.y - this.pole.displayHeight - radiiSum / 2 + 10);
       
        // set shadow size
        this.targetShadow.setDisplaySize(radiiSum * GameOptions.ringRatio, radiiSum);

        // set target shadow position
        this.targetShadow.setPosition(posX + 5, targetHeight);

        // loop through all rings
        for (let i : number = 0; i < GameOptions.rings; i ++) {  
            
            // set ring position
            this.targetRings[i].setPosition(posX, targetHeight);

            // set ring tint
            this.targetRings[i].setTint(GameOptions.ringColor[i]);

            // set ring diplay size
            this.targetRings[i].setDisplaySize(radiiSum * GameOptions.ringRatio, radiiSum);

            // decrease radiiSum to get the radius of next ring
            radiiSum -= ringRadii[i];
        }    
    }

    // method to tween the target to posX
    tweenTarget(posX : number) : void {

        // array with all target related stuff to move
        let stuffToMove : any[] = [this.pole, this.poleTop, this.poleShadow, this.targetShadow, this.terrain, this.dirt].concat(this.targetRings);
        
        // delta X between current target position and destination position
        let deltaX : number = this.game.config.width as number * 2 - posX;

        // variable to save previous value
        let previousValue : number = 0;

        // variable to save the amount of pixels already travelled
        let totalTravelled : number = 0;

        // tween a number from 0 to 1
        this.tweens.addCounter({
            from : 0,
            to : 1,
            
            // tween duration according to deltaX
            duration : deltaX * 3,

            // tween easing
            ease : Phaser.Math.Easing.Cubic.InOut,

            // tween callback scope
            callbackScope : this,

            // method to be called at each tween update
            onUpdate : (tween : Phaser.Tweens.Tween) => {
                
                // delta between previous and current value
                let delta : number = tween.getValue() - previousValue;

                // update previous value to current value
                previousValue = tween.getValue();

                // determine the amount of pixels travelled
                totalTravelled += delta * deltaX;

                // move all stuff
                stuffToMove.forEach((item : any) => {
                    item.x -= delta * deltaX;
                })

                // adjust the seamless terrain when it goes too much outside the screen
                if (this.terrain.x < -256) {
                    this.terrain.x += 256;
                }

                // adjust the seamless dirt when it goes too much outside the screen
                if (this.dirt.x < -256) {
                    this.dirt.x += 256;
                }

                // if the target left the canvas from the left side...
                if (this.targetShadow.getBounds().right < 0) {

                    // reposition it on the right side
                    this.placeTarget(this.game.config.width as number * 2 - totalTravelled, this.pole.y);     
                }
            },

            // method to be called when the tween completes
            onComplete : () => {

                // add a time event
                this.time.addEvent({

                    // wait 1 second
                    delay : 1000,

                    // tween callback scope
                    callbackScope : this,

                    // callback function
                    callback : () => {

                        // tween the new target
                        this.tweenTarget(this.getRandomPosition());
                    }
                });
            }
        })
    }
}

And we got an endless terrain with just a few lines. Next time I am going to place the archer, meanwhile download the full source code of the project.

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