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 5th part of the Mini Archer tutorial series. This is quite a big update since I am going to show you how to fire an arrow and make it stick in the target just using trigonometry, with no physics engines.

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.

If shooting the arrow was rather easy, once we simplified the concept and decided that the arrow will always fly in a straight line, how do we simulate the sticking of the arrow in the target?

Of course with a mask, hiding the part of the arrow Sprite which is on the right side of the target.

Look at the result, built with Phaser 3.60:

Try to shoot the arrow to hit the target.

In previous step I promised to add some custom classes to make the source code more readable, but I preferred to complete the shooting feature first.

But the source code is completely commented and it should be easy for you to see how I built this prototype:

We have one HTML file, one CSS file and four 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 start, in screen height ratio, where 0 = top, 1 = bottom
    terrainStart : 0.6,

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

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

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

    // 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[];

    // girl
    girl : Phaser.GameObjects.Sprite;

    // rainbow
    rainbow : Phaser.GameObjects.Graphics;

    // clouds
    clouds : Phaser.GameObjects.Sprite[];

    // arrow
    arrow : Phaser.GameObjects.Sprite;

    // array with all rings radii
    ringRadii : number[];

    // mask to simulate arrow sticking in the target
    mask : Phaser.GameObjects.Sprite;

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

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

        // 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.targetRings.amount; i ++) {  
            this.targetRings[i] = this.add.sprite(0, 0, 'circle');
        }

        // girl start position
        let girlXPos : number = this.game.config.width as number * GameOptions.girlPosition;

        // add girl shadow
        let girlShadow : Phaser.GameObjects.Sprite = this.add.sprite(girlXPos + 5, poleYPos, 'circle');
        girlShadow.setTint(0x000000);
        girlShadow.setAlpha(0.2);
        girlShadow.setDisplaySize(60, 20); 

        // add rainbow
        this.rainbow = this.add.graphics();

        // add the arrow
        this.arrow = this.add.sprite(this.game.config.width as number * 2, 0, 'arrow');
        this.arrow.setOrigin(0, 0.5);

         // add girl
         this.girl = this.add.sprite(girlXPos, poleYPos, 'girl');
         this.girl.setOrigin(0.5, 1);
         this.girl.anims.play('idle'); 
       
        // add clouds
        this.clouds = [
            this.add.sprite(0, 0, 'cloud'),
            this.add.sprite(0, 0, 'cloud')    
        ];

        // set a custom property to top cloud
        this.clouds[1].setData('posY', this.girl.getBounds().top - 50)
      
        // add a tween to move a bit clouds up and down
        this.tweens.addCounter({
            from : 0,
            to : 1,
            duration : 1000,
            callbackScope : this,
            onUpdate : (tween : Phaser.Tweens.Tween) => {
                this.clouds[1].y =  this.clouds[1].getData('posY') + 5 * Math.cos(Math.PI * tween.getValue())
                this.clouds[0].y =  this.clouds[0].getData('posY') + 5 * Math.cos(Math.PI * tween.getValue())
            },
            yoyo : true,
            repeat : -1
        })

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

        // create a mask to simulate arrow sticking in the target
        this.mask = this.add.sprite(0, 0, 'mask');
        this.mask.setOrigin(0, 0.5)
        this.mask.setVisible(false)
        let bitmapMask : Phaser.Display.Masks.BitmapMask = this.mask.createBitmapMask();
        bitmapMask.invertAlpha = true;
        this.arrow.setMask(bitmapMask);
        
        // tween the target to a random position
        this.tweenTarget(this.getRandomPosition());

        // waiting for player input
        this.input.on('pointerdown', this.handlePointer, 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;

            // 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(this.girl.x, this.girl.getBounds().centerY, this.targetRings[0].x, this.targetRings[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(this.girl.x, this.girl.getBounds().centerY, this.targetRings[0].x, this.targetRings[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(this.arrow.rotation);

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

            // if arrow angle is between top and bottom target angle, then arrow hits the target
            if (arrowNormalizedAngle >= topNormalizedAngle && arrowNormalizedAngle <= bottomNormalizedAngle) {

                // adjusting the distance to make the arrow stop in the horizontal center of the target
                distance = (this.targetRings[0].x - this.girl.x) / Math.cos(this.arrow.rotation) - this.arrow.getData('radius') - this.arrow.displayWidth * 0.7;                
                
                // place the mask behind target horizontal center and make it big enough
                this.mask.x = this.targetRings[0].x;
                this.mask.y = this.targetRings[0].y;
                this.mask.setDisplaySize(radiiSum, radiiSum * 2)
            }

            // add the tween to shoot the arrow
            this.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.time.addEvent({

                        // amount of milliseconds to wait
                        delay : 1000,

                        // timer callback scope
                        callbackScope : this,

                        // function to execute when the timer is complete
                        callback : () => {
                   
                            // generic tween of a value from 0 to 1, to make rainbow disappear
                            this.tweens.addCounter({
                                from : 0,
                                to : 1,
                                            
                                // tween duration according to deltaX
                                duration : 200,
                                
                                // tween callback scope
                                callbackScope : this,
                                
                                // function to be called at each tween update
                                onUpdate : (tween : Phaser.Tweens.Tween) => {
                                            
                                    // get current angle according to rainbow length and tween value
                                    let angle : number = this.rainbow.getData('rainbowlength') - this.rainbow.getData('rainbowlength') * tween.getValue();

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

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

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

                                        // draw the arc
                                        this.rainbow.beginPath();
                                        this.rainbow.arc(this.girl.x, this.girl.getBounds().centerY, this.rainbow.getData('radiuslength') + index * GameOptions.rainbow.width, this.rainbow.getData('angle'), this.rainbow.getData('angle') + angle, false);
                                        this.rainbow.strokePath();
                                    });

                                    // set posY property of lower cloud 
                                    this.clouds[0].setData('posY', this.girl.getBounds().centerY + this.rainbow.getData('radiuslength') * Math.sin(this.rainbow.getData('angle') + angle) + GameOptions.rainbow.colors.length / 2 * GameOptions.rainbow.width)
                                                
                                    // set x position of lower cloud
                                    this.clouds[0].setX(this.girl.x + this.rainbow.getData('radiuslength') * Math.cos(this.rainbow.getData('angle') + angle)) ;      
                                },

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

                                    // hide the cloud
                                    this.clouds[0].setVisible(false)
                                }
                            })

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

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

        // make a line representing rainbow radius
        let rainbowRadius : Phaser.Geom.Line = new Phaser.Geom.Line(this.girl.x, this.girl.getBounds().centerY, this.clouds[1].x, this.clouds[1].getData('posY'));
        
        // get radius length
        let rainbowRadiusLength : number = Phaser.Geom.Line.Length(rainbowRadius) - GameOptions.rainbow.colors.length / 2 * GameOptions.rainbow.width;
        
        // get radius angle, which is random start angle
        let rainbowStartAngle : number = Phaser.Geom.Line.Angle(rainbowRadius);

        this.rainbow.setData('angle', rainbowStartAngle);

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

        this.rainbow.setData('radiuslength', rainbowRadiusLength)
        this.rainbow.setData('rainbowlength', rainbowLength);
        
        // hide the lower cloud
        this.clouds[0].setVisible(true);      
        
        // generic tween of a value from 0 to 1, to make rainbow appear
        this.tweens.addCounter({
            from : 0,
            to : 1,
            
            // tween duration according to deltaX
            duration : 200,

            // tween callback scope
            callbackScope : this,

            // method to be called at each tween update
            onUpdate : (tween : Phaser.Tweens.Tween) => {
               
                // get current angle according to rainbow length and tween value
                let angle : number = rainbowLength * tween.getValue();

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

                // loop through all rainbow colors
                GameOptions.rainbow.colors.forEach((item : number, index : number) => {
                    
                    // set line style
                    this.rainbow.lineStyle(GameOptions.rainbow.width, item, 1);
                    
                    // draw the arc
                    this.rainbow.beginPath();
                    this.rainbow.arc(this.girl.x, this.girl.getBounds().centerY, rainbowRadiusLength + index * GameOptions.rainbow.width, rainbowStartAngle, rainbowStartAngle + angle, false);
                    this.rainbow.strokePath();
                });

                // set posY property of lower cloud 
                this.clouds[0].setData('posY', this.girl.getBounds().centerY + rainbowRadiusLength * Math.sin(rainbowStartAngle + angle) + GameOptions.rainbow.colors.length / 2 * GameOptions.rainbow.width);
                
                // set x position of lower cloud
                this.clouds[0].setX(this.girl.x + rainbowRadiusLength * Math.cos(rainbowStartAngle + angle)) ;
            },

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

                // the girl is aiming
                this.gameState = GameState.Aiming;

                // place the arrow according to rainbow radius
                this.arrow.setPosition(this.girl.x + rainbowRadiusLength - 30, this.girl.getBounds().centerY);
                
                // set some arrow data
                this.arrow.setData('radius', rainbowRadiusLength - 30);
                this.arrow.setData('start', Phaser.Math.RadToDeg(rainbowStartAngle));
                this.arrow.setData('end', Phaser.Math.RadToDeg(rainbowStartAngle + rainbowLength));
                this.arrow.setData('mult', 1);

                // rotate the arrow according to rainbow radius
                this.arrow.setAngle(Phaser.Math.RadToDeg(rainbowStartAngle));
                
            }
        })        
    }

    // method to place the target at (posX, posY)
    placeTarget(posX : number, posY : 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 = posY - 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 = 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.targetRings.ratio, radiiSum);

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

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

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

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

            // decrease radiiSum to get the radius of next ring
            radiiSum -= this.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, this.mask, this.arrow].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;

        // next cloud X position
        let nextCloudX : number = this.girl.x - 50 + Phaser.Math.Between(0, 100); 
      
        // next cloud y position
        let nextCloudY : number = this.girl.getBounds().top - Phaser.Math.Between(50, 100); 
     
        // 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.clouds[1].x, this.clouds[1].getData('posY'), nextCloudX, nextCloudY]);
        
        // add a path
        var path : Phaser.Curves.Path = this.add.path(0, 0);
        
        // add movement line to path
        path.add(movementLine);

        // move the cloud along the path
        this.tweens.add({
            targets: follower,
            t: 1,
            ease: 'Linear',
            duration : deltaX * 3,
            callbackScope : this,
            onUpdate : () => {
                var point = path.getPoint(follower.t, follower.vec)
                this.clouds[1].setX(point.x)
                this.clouds[1].setData('posY', point.y);
            }
        });

        // play girl's "run" animation
        this.girl.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
                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 : () => {

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

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

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

        // is the arrow visible?
        if (this.arrow.visible == true && this.gameState != GameState.Firing) {

            // rotate the arrow according to arrow speed
            this.arrow.angle += GameOptions.arrow.rotationSpeed * deltatime / 1000 * this.arrow.getData('mult');
            
            // did the arrow reach the end of the rainbow?
            if (this.arrow.angle > this.arrow.getData('end')) {

                // don't let it go further
                this.arrow.angle = this.arrow.getData('end');

                // invert arrow rotation direction
                this.arrow.setData('mult', this.arrow.getData('mult') * -1);
            }

            // did the arrow reach the beginning of the rainbow?
            if (this.arrow.angle < this.arrow.getData('start')) {

                // don't let the arrow go further
                this.arrow.angle = this.arrow.getData('start');

                // invert arrow rotation direction
                this.arrow.setData('mult', this.arrow.getData('mult') * -1);
            }

            // set arrow position according to its rotation
            this.arrow.setPosition(this.girl.x + this.arrow.getData('radius') * Math.cos(this.arrow.rotation), this.girl.getBounds().centerY  + this.arrow.getData('radius') * Math.sin(this.arrow.rotation))
        }
    }
}

And finally the running girl is able to shoot arrows to an infinite sequence of randomly generated targets running along an endless terrain. 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.