Do you like my tutorials?

Then consider supporting me on Ko-fi

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

Welcome to the the 6th part of the Mini Archer tutorial series. This time I won’t be adding new features, splitting the existing code into classes.

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.

Was it necessary to split an already working code into classes? Of course!

Classes contribute to the readability and maintainability of your code. They provide a clear structure and hierarchy, making it easier for other programmers (including your future self) to understand the codebase, navigate through the project, and make changes without introducing errors.

As you can see, the game is working in the same way as previous step:

Tap to shoot the arrow and try to hit the target.

Here you have the completely commented source code.

We have one HTML file, one CSS file and ten TypeScript files.

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. I also grouped the variables to keep them more organized.

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

export const GameOptions = {

    terrain : {

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

        // vertical offset where to start placing stuff, in pixels
        stuffOffset : 38
    },

    // girl x position, in screen width ratio, where 0 = left, 1 = right
    girlPosition : 0.15,

    target : {

        // target position range, in screen width ratio, where 0 = left, 1 = right
        positionRange : {
            from : 0.6,
            to : 0.9
        },

        // target height range, in pixels
        heightRange : {
            from : 200,
            to : 450
        }
    },

    targetRings : {

        // number of rings
        amount : 5,

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

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

        // ring radii, from external to internal, in pixels
        radius : [50, 40, 40, 30, 20],

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

    rainbow : {

        // rainbow rings width, in pixels
        width : 5,

        // rainbow colors
        colors : [0xe8512e, 0xfbb904, 0xffef02, 0x65b33b, 0x00aae5, 0x3c4395, 0x6c4795]
    },

    arrow : {

        // arrow rotation speed, in degrees per second
        rotationSpeed : 180,
        
        // flying speed
        flyingSpeed: 1000
    }
}

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. Animations are also defined here.

// 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');
        this.load.image('cloud', 'assets/sprites/cloud.png');
        this.load.image('arrow', 'assets/sprites/arrow.png');
        this.load.image('mask', 'assets/sprites/mask.png');
        this.load.spritesheet('idlegirl', 'assets/sprites/idlegirl.png', {
            frameWidth : 119,
            frameHeight : 130
        });
        this.load.spritesheet('runninggirl', 'assets/sprites/runninggirl.png', {
            frameWidth : 119,
            frameHeight : 130
        });
    }
 
    // method to be called once the instance has been created
    create() : void {

        // define idle animation
        this.anims.create({
            key : 'idle',
            frames: this.anims.generateFrameNumbers('idlegirl', {
                start: 0,
                end: 15
            }),
            frameRate: 15,
            repeat: -1
        });

        // define running animation
        this.anims.create({
            key : 'run',
            frames: this.anims.generateFrameNumbers('runninggirl', {
                start: 0,
                end: 19
            }),
            frameRate: 15,
            repeat: -1
        });

 
        // 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';
import { Rainbow } from './rainbow';
import { Arrow } from './arrow';
import { Girl } from './girl';
import { Target } from './target';
import { Terrain } from './terrain';

// game states
enum GameState {
    Idle,
    Aiming,
    Firing
}

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

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

    // current game state
    gameState : GameState;

    // the girl
    girl : Girl;

    // the terrain
    terrain : Terrain;

    // the rainbow
    rainbow : Rainbow;

    // the arrow
    arrow : Arrow;

    // the target
    target : Target;

    // array to hold all stuff to be scrolled
    stuffToScroll : any[];

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

        // at the beginning, the game is in idle state
        this.gameState = GameState.Idle;

        // add the terrain
        this.terrain = new Terrain(this);
       
        // add the target
        this.target = new Target(this);

        // add rainbow
        this.rainbow = new Rainbow(this);

        // add the arrow
        this.arrow = new Arrow(this);

        // add clouds, after the arrow for a matter of z-indexing
        this.rainbow.addClouds();

        // add the girl
        this.girl = new Girl(this);
         
        // tween the target to a random position
        this.tweenTarget(this.getRandomPosition());

        // populate the array with all stuff to be scrolled
        this.stuffToScroll = this.arrow.getChildren();
        this.stuffToScroll = this.stuffToScroll.concat(this.target.getChildren());
        this.stuffToScroll = this.stuffToScroll.concat(this.terrain.getChildren());

        // listeners
        this.input.on('pointerdown', this.handlePointer, this);    
        this.rainbow.on('appeared', this.rainbowAppeared, this); 
        this.arrow.on('flown', this.arrowFlown, this);
    }

    // method to handle pointer
    handlePointer() : void {

        // is the girl aiming?
        if (this.gameState == GameState.Aiming) {

            // now the girl is firing
            this.gameState = GameState.Firing;

            // default distance the arrow will travel
            let distance : number = this.game.config.width as number * 2;

            // if the arrow hits the target...
            if (this.target.hitByArrow(this.rainbow.center.x, this.rainbow.center.y, this.arrow.arrow.rotation)) {

                // adjust the distance to make the arrow stop in the center of the target
                distance = (this.target.rings[0].x - this.rainbow.center.x - this.arrow.arrow.displayWidth) / Math.cos(this.arrow.arrow.rotation);                
                
                // place the mask behind target horizontal center and make it big enough
                this.arrow.arrowMask.x = this.target.rings[0].x;
                this.arrow.arrowMask.y = this.target.rings[0].y;
                
            }

            // shoot the arrow!
            this.arrow.shoot(distance);
        }
    }

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

    // method to draw the rainbow
    drawRainbow() : void {
        this.rainbow.appear(this.girl.body.x, this.girl.body.getBounds().centerY, this.rainbow.clouds[1].x, this.rainbow.clouds[1].posY);
    }

    // method to be called once the rainbow appeared
    rainbowAppeared() : void {
        
        // the girl is aiming
        this.gameState = GameState.Aiming;

        // aim the arrow
        this.arrow.prepareToAim(this.rainbow);
    }

    // method to be called once the arrow flown
    arrowFlown() : void {

        // make rainbow disappear
        this.rainbow.disappear();

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

    // method to tween the target to posX
    // posX : the x position
    tweenTarget(posX : number) : void {
        
        // 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;

        // move rainbow cloud
        this.rainbow.moveCloud(this.girl.body.x - 50, this.girl.body.getBounds().top, deltaX * 3);

        // play girl's "run" animation
        this.girl.body.anims.play('run');

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

            // 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
                this.stuffToScroll.forEach((item : any) => {
                    item.x -= delta * deltaX;
                });

                // adjust terrain position
                this.terrain.adjustPosition();

                // adjust target position
                this.target.adjustPosition(totalTravelled);  
            },

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

                // play girl's "idle" animation
                this.girl.body.anims.play('idle');

                // draw the rainbow
                this.drawRainbow();
            }
        })
    }

    // method to be executed at each frame
    // time : time passed since the beginning
    // deltaTime : time passed since last frame
    update(time : number, deltaTime : number) : void {

        // is the player aiming?
        if (this.gameState == GameState.Aiming) {

            // let the arrow aim
            this.arrow.aim(deltaTime, this.rainbow);
       }
    }
}

arrow.ts

The arrow, a Group with two Sprite objects, one for the arrow itself and one for the mask to simulate the arrow to stick into the target.

// ARROW CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameObjects } from 'phaser';
import { GameOptions } from './gameOptions';
import { Rainbow } from './rainbow';

export class Arrow extends Phaser.GameObjects.Group {

    // a simple multiplier to make the arrow move clockwise or counter clockwise
    mult : number;

    // the arrow itself
    arrow : Phaser.GameObjects.Sprite;

    // arrow mask to simulate the arrow to stick in the target
    arrowMask : GameObjects.Sprite;

    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);

        // add the arrow
        this.arrow = scene.add.sprite(0, 0, 'arrow');
        this.arrow.setOrigin(0, 0.5);
        this.arrow.setVisible(false);
        this.mult = 1;

        // add arrow to the group
        this.add(this.arrow);

        // create a mask to simulate arrow sticking in the target
        this.arrowMask = scene.add.sprite(0, 0, 'mask');
        this.arrowMask.setOrigin(0, 0.5);
        this.arrowMask.setVisible(false);
        this.arrowMask.setDisplaySize(512, 512);

        // add arrow mask to the group
        this.add(this.arrowMask);

        // set the mask as a bitmap mask
        let bitmapMask : Phaser.Display.Masks.BitmapMask = this.arrowMask.createBitmapMask();
        bitmapMask.invertAlpha = true;
        this.arrow.setMask(bitmapMask);
    }

    // method to prepare to aim
    // rainbow : the rainbow
    prepareToAim(rainbow : Rainbow) {

        // place the arrow mask outside the screen
        this.arrowMask.setX(this.scene.game.config.width as number);
     
        // set the arrow visible
        this.arrow.setVisible(true);

        // place the arrow according to rainbow radius
        this.arrow.setPosition(rainbow.center.x + rainbow.radius - 30, rainbow.center.y);
        
        // rotate the arrow according to rainbow radius
        this.arrow.setAngle(Phaser.Math.RadToDeg(rainbow.startAngle));
    }

    // method to aim
    // deltaTime : time passed since previous frame, in milliseconds
    // rainbow : the raimbow
    aim(deltaTime : number, rainbow : Rainbow) : void {
        
        // rotate the arrow according to arrow speed
        this.arrow.angle += GameOptions.arrow.rotationSpeed * deltaTime / 1000 * this.mult;
            
        // did the arrow reach the end of the rainbow?
        if (this.arrow.angle > Phaser.Math.RadToDeg(rainbow.startAngle + rainbow.length)) {

            // don't let it go further
            this.arrow.angle = Phaser.Math.RadToDeg(rainbow.startAngle + rainbow.length);

            // invert arrow rotation direction
            this.mult *= -1;
        }

        // did the arrow reach the beginning of the rainbow?
        if (this.arrow.angle < Phaser.Math.RadToDeg(rainbow.startAngle)) {

            // don't let the arrow go further
            this.arrow.angle = Phaser.Math.RadToDeg(rainbow.startAngle);

            // invert arrow rotation direction
            this.mult *= -1;
        }

        // set arrow position according to its rotation
        this.arrow.setPosition(rainbow.center.x + (rainbow.radius - 30) * Math.cos(this.arrow.rotation), rainbow.center.y + (rainbow.radius - 30) * Math.sin(this.arrow.rotation));
    }

    // method to shoot
    // distance : distance the arrow must travel
    shoot(distance : number) : void {
        
        // add the tween to shoot the arrow
        this.scene.tweens.add({

            // target: the arrow
            targets: this.arrow,

            // arrow destination, determine with trigonometry
            x : this.arrow.x + distance * Math.cos(this.arrow.rotation),
            y : this.arrow.y + distance * Math.sin(this.arrow.rotation),

            // tween duration, according to distance
            duration : distance / GameOptions.arrow.flyingSpeed * 1000,

            // tween callback scope
            callbackScope : this,

            // function to execute when the tween is complete
            onComplete : () => {

                // add a timer event to wait one second
                this.scene.time.addEvent({

                    // amount of milliseconds to wait
                    delay : 1000,

                    // timer callback scope
                    callbackScope : this,

                    // function to execute when the timer is complete
                    callback : () => {

                        // emit "flown" event
                        this.emit('flown');               
                    }
                })
            }
        })
    }
}

cloud.ts

The cloud, a Sprite.

// CLOUD CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE

export class Cloud extends Phaser.GameObjects.Sprite {

    // we need to keep track of y position to allow floating movement
    posY : number;

    constructor(scene : Phaser.Scene) {
        super(scene, 0, 0, 'cloud');  
        scene.add.existing(this);

        // add a tween from 0 to 1
        this.scene.tweens.addCounter({

            // start value
            from : 0,

            // end value
            to : 1,

            // tween duration, in milliseconds
            duration : 1000,

            // callback scope
            callbackScope : this,

            // function to be executed at each update
            onUpdate : (tween : Phaser.Tweens.Tween) => {

                // make cloud float using a cosine function
                this.setY(this.posY + 5 * Math.cos(Math.PI * tween.getValue()));  
            },

            // run the tween in reverse as well
            yoyo : true,

            // execute the tween forever
            repeat : -1
        });
    }

    // method to move a cloud along a path
    // posX : new x position
    // posY : new y position
    // duration : duration, in milliseconds, of the movement
    moveAlongPath(posX : number, posY : number, duration : number) : void {

        // object which will follow a path
        let follower : any = {
            t: 0,
            vec: new Phaser.Math.Vector2()
        };

        // define cloud movement line
        let movementLine : Phaser.Curves.Line = new Phaser.Curves.Line([this.x, this.posY, posX, posY]);

        // add a path
        var path : Phaser.Curves.Path = this.scene.add.path(0, 0);

        // add movement line to path
        path.add(movementLine);

        // add a tween
        this.scene.tweens.add({

            // tween target
            targets : follower,

            // bring t property of the target to 1
            t : 1,
            
            // duration, in milliseconds
            duration : duration,

            // callback scope
            callbackScope : this,

            // function to be executed at each update
            onUpdate : () => {

                // get the point along the path at time t, where 0 = the beginning, 1 = the end
                var point = path.getPoint(follower.t, follower.vec)

                // set new cloud x position
                this.setX(point.x)

                // set new cloud Y property
                this.posY = point.y;
            }
        });        
    }
}

girl.ts

The running girl, a Group with Sprite objects representing the girl herself and her shadow.

// GIRL CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameOptions } from './gameOptions';

export class Girl extends Phaser.GameObjects.Group {

    // the girl itself
    body : Phaser.GameObjects.Sprite;

    // girl shadow
    shadow : Phaser.GameObjects.Sprite;
    
    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);

        // determine girl x and y position
        let girlPositionY : number = this.scene.game.config.height as number * GameOptions.terrain.start + GameOptions.terrain.stuffOffset;
        let girlPositionX : number = this.scene.game.config.width as number * GameOptions.girlPosition;
        
        // add girl shadow
        this.shadow = this.scene.add.sprite(girlPositionX + 5, girlPositionY, 'circle');
        this.shadow.setTint(0x000000);
        this.shadow.setAlpha(0.2);
        this.shadow.setDisplaySize(60, 20); 

        // add shadow to group
        this.add(this.shadow);
        
        // add girl
        this.body = this.scene.add.sprite(girlPositionX, girlPositionY, 'girl');
        this.body.setOrigin(0.5, 1);
        this.body.anims.play('idle'); 

        // add girl to group
        this.add(this.body);
    }
}

rainbow.ts

The rainbow, a Group with a Graphics object to draw the rainbow itself and two Cloud objects.

// RAINBOW CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameOptions } from './gameOptions';
import {Cloud} from './cloud';

// rainbow ends
enum RainbowEnd {
    Lower,
    Upper
}

export class Rainbow extends Phaser.GameObjects.Group {

    // the Graphics ojbect representing the rainbow itself
    graphics : Phaser.GameObjects.Graphics;
    
    // rainbow start angle
    startAngle : number;

    // rainbow radius
    radius : number;

    // rainbow length, in radians
    length : number;

    // rainbow center
    center : Phaser.Geom.Point;

    // array where to store the clouds
    clouds : Cloud[];

    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);

        // add a new graphics        
        this.graphics = this.scene.add.graphics();

        // add the graphics to the group
        this.add(this.graphics);
        
    }

    // method to add the clouds
    addClouds() : void {

        // populate clouds array
        this.clouds = [new Cloud(this.scene), new Cloud(this.scene)];
        this.add(this.clouds[RainbowEnd.Lower]);
        this.add(this.clouds[RainbowEnd.Upper]);
    }

    // method to make the rainbow appear
    // centerX : x coordinate of rainbow center
    // centerY : y coordinate of rainbow center
    // arcX : x coordinate of the beginning of the arc
    // arcY : y coordinate of the beginning of the arc
    appear(centerX : number, centerY : number, arcX : number, arcY : number) : void {

        // make lower cloud visible
        this.clouds[RainbowEnd.Lower].setVisible(true);

        // set rainbow center
        this.center = new Phaser.Geom.Point(centerX, centerY);
        
        // make a line representing rainbow radius
        let rainbowRadius : Phaser.Geom.Line = new Phaser.Geom.Line(centerX, centerY, arcX, arcY);
        
        // get radius length
        this.radius = Phaser.Geom.Line.Length(rainbowRadius) - GameOptions.rainbow.colors.length / 2 * GameOptions.rainbow.width;
        
        // get radius angle, which is rainbow start angle
        this.startAngle = Phaser.Geom.Line.Angle(rainbowRadius);

        // get a random rainbow arc length
        this.length = Math.PI / 4 * 3 + Phaser.Math.FloatBetween(0, Math.PI / 4);

        // generic tween of a value from 0 to 1, to make rainbow appear
        this.scene.tweens.addCounter({
            from : 0,
            to : 1,
            
            // tween duration
            duration : 200,

            // tween callback scope
            callbackScope : this,

            // method to be called at each tween update
            onUpdate : (tween : Phaser.Tweens.Tween) => {

                // draw the rainbow
                this.drawRainbow(this.length * tween.getValue());

            },

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

                // emit "appeared" event
                this.emit('appeared');
            }
        })  
    }

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

        // generic tween of a value from 0 to 1, to make rainbow disappear
        this.scene.tweens.addCounter({
            from : 0,
            to : 1,
                        
            // tween duration
            duration : 200,
            
            // tween callback scope
            callbackScope : this,
            
            // function to be called at each tween update
            onUpdate : (tween : Phaser.Tweens.Tween) => {

                // draw the rainbow
                this.drawRainbow(this.length - this.length * tween.getValue());
                         
            },

            // function to be called when the tween ends 
            onComplete : () => {

                // make lower cloud disappear
                this.clouds[RainbowEnd.Lower].setVisible(false)
            }
        })

    }

    // method to draw the rainbow
    // angle : rainbow angle
    drawRainbow(angle : number) : void {

        // clear rainbow graphics
        this.graphics.clear();

        // loop through all rainbow colors
        GameOptions.rainbow.colors.forEach((item : number, index : number) => {

            // set line style
            this.graphics.lineStyle(GameOptions.rainbow.width, item, 1);

            // draw the arc
            this.graphics.beginPath();
            this.graphics.arc(this.center.x, this.center.y, this.radius + index * GameOptions.rainbow.width, this.startAngle, this.startAngle + angle, false);
            this.graphics.strokePath();

            // set lower cloud posY attribute
            this.clouds[RainbowEnd.Lower].posY = this.center.y + (this.radius + (index * GameOptions.rainbow.width) / 2) * Math.sin(this.startAngle + angle);
               
            // set lower cloud x position
            this.clouds[RainbowEnd.Lower].setX(this.center.x + (this.radius + (index * GameOptions.rainbow.width) / 2) *  Math.cos(this.startAngle + angle));
        });

    }

    // method to move the cloud
    // startX : cloud x position
    // startY : cloud y position
    // duration : movement duration, in milliseconds.
    moveCloud(startX : number, startY : number, duration : number) : void {

        // next cloud X position
        let nextCloudX : number = startX + Phaser.Math.Between(0, 100); 
      
        // next cloud y position
        let nextCloudY : number = startY - Phaser.Math.Between(50, 100); 

        // move the cloud along the path
        this.clouds[RainbowEnd.Upper].moveAlongPath(nextCloudX, nextCloudY, duration); 
    }
}

target.ts

The target, a Group with some Sprite objects representing the circles building the target itself, the pole, the pole top, the pole shadow and the target shadow.

// TARGET CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameOptions } from './gameOptions';

export class Target extends Phaser.GameObjects.Group {

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

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

    // the pole
    pole : Phaser.GameObjects.TileSprite;

    // pole top
    poleTop : Phaser.GameObjects.Sprite;

    // array with all rings
    rings : Phaser.GameObjects.Sprite[];
    
    // array with all rings radii
    ringRadii : number[];

    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);

        // x position where to start placing stuff
        let stuffStartX : number = this.scene.game.config.height as number * GameOptions.terrain.start + GameOptions.terrain.stuffOffset;

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

        // add the shadow to the group
        this.add(this.shadow);

        // add a circle representing pole shadow
        this.poleShadow = this.scene.add.sprite(0, stuffStartX, 'circle');
        this.poleShadow.setTint(0x000000);
        this.poleShadow.setAlpha(0.2);
        this.poleShadow.setDisplaySize(90, 20);

        // add pole shadow to the group
        this.add(this.poleShadow);

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

        // add the pole to the group
        this.add(this.pole);

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

        // add pole top to the group
        this.add(this.poleTop);

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

        // place the target
        this.place(this.scene.game.config.width as number * 2);
    }

    // method to place the garget
    // posX : x position
    place(posX : number) : void {
        
        // array where to store radii values
        this.ringRadii = [];

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

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

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

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

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

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

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

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

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

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

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

    // method to check if the target has been hit by the arrow
    // startX : x position of rainbow center
    // startY : y position of rainbow center
    // arrowAngle : arrow angle
    hitByArrow(startX : number, startY : number, arrowAngle : number) : boolean {

        // get radii sum, which is the height of the target
        let radiiSum : number = this.ringRadii.reduce((sum, value) => sum + value, 0);

        // we define a target line going from the center of the rainbow to the top edge of the target
        let topTargetLine : Phaser.Geom.Line = new Phaser.Geom.Line(startX, startY, this.rings[0].x, this.rings[0].y - radiiSum / 2);

        // we define a target line going from the center of the rainbow to the bottom edge of the target
        let bottomTargetLine : Phaser.Geom.Line = new Phaser.Geom.Line(startX, startY, this.rings[0].x, this.rings[0].y + radiiSum / 2);

        // get the angle of the top target line and normalize it
        let topAngle : number = Phaser.Geom.Line.Angle(topTargetLine);
        let topNormalizedAngle : number = Phaser.Math.Angle.Normalize(topAngle);

        // get the angle of the bottom target line and normalize it
        let bottomAngle : number = Phaser.Geom.Line.Angle(bottomTargetLine);
        let bottomNormalizedAngle : number = Phaser.Math.Angle.Normalize(bottomAngle);

        // get the normalized arrow angle
        let arrowNormalizedAngle : number = Phaser.Math.Angle.Normalize(arrowAngle);   
        
        // return true if arrow angle is between top and bottom target angle
        return arrowNormalizedAngle >= topNormalizedAngle && arrowNormalizedAngle <= bottomNormalizedAngle;
    }

    // adjust target position
    // deltaX : distance alreay travelled by the target
    adjustPosition(deltaX : number) : void {
        
        // if the target left the canvas from the left side...
        if (this.shadow.getBounds().right < 0) {

            // reposition it on the right side
            this.place(this.scene.game.config.width as number * 2 - deltaX);     
        }
    }
}

terrain.ts

The scrolling terrain, a Group with two TileSprite objects, one for the grass and one for the dirt.

// TERRAIN CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameOptions } from './gameOptions';

export class Terrain extends Phaser.GameObjects.Group {

    // the grass, the upper part of the terrain
    grass : Phaser.GameObjects.TileSprite;

    // the dirt, the lower part of the terrain
    dirt : Phaser.GameObjects.TileSprite;

    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);

        // determine terrain starting y position
        let terrainStartY : number = this.scene.game.config.height as number * GameOptions.terrain.start

        // add grass tilesprite
        this.grass = this.scene.add.tileSprite(0, terrainStartY, this.scene.game.config.width as number + 256, 256, 'grasstile');
        this.grass.setOrigin(0, 0);

        // add the grass to group
        this.add(this.grass)

        // determine dirt starting y position
        let dirtStartY : number = terrainStartY + 256; 

        // add dirt tilesprite
        this.dirt = this.scene.add.tileSprite(0, dirtStartY, this.grass.width, this.scene.game.config.height as number - dirtStartY, 'dirttile');
        this.dirt.setOrigin(0, 0);

        // add the dirt to the group
        this.add(this.dirt)
    }

    // method to adjust terrain position
    adjustPosition() : void {
        
        // adjust the seamless grass when it goes too much outside the screen
        if (this.grass.x < -256) {
            this.grass.x += 256;
        }

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

And that’s it! Now your code is more readable and more reusable. Download the source code of the entire project.

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