Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Spinny Gun game, Game development, HTML5, Javascript and Phaser.

Ketchapp did it again. Spinny Gun available for both iOS and Android is the 10,000th hyper-casual game made by this studio which becomes a smashing hit.

«Shoot as many targets as you can in this new addictive game!» the description says, and actually that’s all! There is a spinning gun and a series of targets moving around a circular path.

So we have objects moving along a path and rotating accordingly, bullets fired from a spinning gun and a collision routine checking for bullets trajectory. Which physics engine should we choose? No one.

Thanks to Phaser 3 paths, we can draw a path just like we are used to draw on graphic canvas with lines, curves and arcs, then make sprites follow such path tweening their positions with all the kind of controls we expect to be capable of when tweening a sprite: easing, speed, repeating, yoyo effects and more.

As for the collision system, we are only using trigonometry to determine the line of fire and check for intersection with targets bounding box.

Have a look at the example:

Tap or click to fire, try to hit the targets.

The source code, completely commented, allows a lot of customization thanks to gameOptions object.

let game;
let gameOptions = {

    // width of the path, in pixels
    pathWidth: 500,

    // height of the path, in pixels
    pathHeight: 800,

    // radius of path curves, in pixels
    curveRadius: 50,

    // amount of targets in game
    targets: 5,

    // min and max milliseconds needed by the targets
    // to run all the way around the path
    targetSpeed: {
        min: 6000,
        max: 10000
    },

    // min and max target size, in pixels
    targetSize: {
        min: 100,
        max: 200
    },

    // milliseconds needed by the gun to rotate by 360 degrees
    gunSpeed: 5000,

    // multiplier to be applied to gun rotation speed each time
    // the gun fires
    gunThrust: 2,

    // maximum gun speed multiplier.
    // If gunSpeed is 5000 and maxGunSpeedMultiplier is 15,
    // maximum gun rotation will allow to rotate by 360 degrees
    // in 5000/15 seconds
    maxGunSpeedMultiplier: 15,

    // gunFriction will reduce gun rotating speed each time the gun
    // completes a 360 degrees rotation
    gunFriction: 0.9
}
window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor: 0x222222,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 750,
            height: 1334
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("tile", "tile.png");
        this.load.image("gun", "gun.png");
        this.load.image("fireline", "fireline.png");
    }
    create(){
        // determine the offset to make path always stand in the center of the stage
        let offset = new Phaser.Math.Vector2((game.config.width - gameOptions.pathWidth) / 2, (game.config.height - gameOptions.pathHeight) / 2);

        // building a path using lines and ellipses. Ellipses are used to create
        // circle arcs and build the curves
        this.path = new Phaser.Curves.Path(offset.x + gameOptions.curveRadius, offset.y);
        this.path.lineTo(offset.x + gameOptions.pathWidth - gameOptions.curveRadius, offset.y);
        this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 90, 180, false, 0);
        this.path.lineTo(offset.x + gameOptions.pathWidth, offset.y + gameOptions.pathHeight - gameOptions.curveRadius);
        this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 180, 270, false, 0);
        this.path.lineTo(offset.x + gameOptions.curveRadius, offset.y + gameOptions.pathHeight);
        this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 270, 0, false, 0);
        this.path.lineTo(offset.x, offset.y + gameOptions.curveRadius);
        this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 0, 90, false, 0);

        // drawing the path
        this.graphics = this.add.graphics();
        this.graphics.lineStyle(4, 0xffff00, 1);
        this.path.draw(this.graphics);

        // fireLine is the bullet trajectory
        this.fireLine = this.add.sprite(game.config.width / 2, game.config.height / 2, "fireline");
        this.fireLine.setOrigin(0, 0.5);
        this.fireLine.displayWidth = 700;
        this.fireLine.displayHeight = 8;
        this.fireLine.visible = false;

        // the rotating gun
        this.gun = this.add.sprite(game.config.width / 2, game.config.height / 2, "gun");

        // the group of targets
        this.targets = this.add.group();
        for(let i = 0; i < gameOptions.targets; i++){

            // target aren't sprites but followers!!!!
            let target = this.add.follower(this.path, offset.x + gameOptions.curveRadius, offset.y, "tile");
            target.alpha = 0.8;
            target.displayWidth = Phaser.Math.RND.between(gameOptions.targetSize.min, gameOptions.targetSize.max)
            this.targets.add(target);

            // the core of the script: targets run along the path starting from a random position
            target.startFollow({
                duration: Phaser.Math.RND.between(gameOptions.targetSpeed.min, gameOptions.targetSpeed.max),
                repeat: -1,
                rotateToPath: true,
                verticalAdjust: true,
                startAt: Phaser.Math.RND.frac()
            });
        }

        // tween to rotate the gun
        this.gunTween = this.tweens.add({
            targets: [this.gun],
            angle: 360,
            duration: gameOptions.gunSpeed,
            repeat: -1,
            callbackScope: this,
            onRepeat: function(){

                // each round, gun angular speed decreases
                this.gunTween.timeScale = Math.max(1, this.gunTween.timeScale * gameOptions.gunFriction);
            }
        });

        // waiting for user input
        this.input.on("pointerdown", function(pointer){

            // we say we can fire when the fire line is not visible
            if(!this.fireLine.visible){
                this.fireLine.visible = true;
                this.fireLine.angle = this.gun.angle;

                // gun angular speed increases
                this.gunTween.timeScale = Math.min(gameOptions.maxGunSpeedMultiplier, this.gunTween.timeScale * gameOptions.gunThrust);

                // fire line disappears after 50 milliseconds
                this.time.addEvent({
                    delay: 50,
                    callbackScope: this,
                    callback: function(){
                        this.fireLine.visible = false;
                    }
                });

                // calculate the line of fire starting from sprite angle
                let radians = Phaser.Math.DegToRad(this.fireLine.angle);
                let fireStartX = game.config.width / 2;
                let fireStartY = game.config.height / 2;
                let fireEndX = fireStartX + game.config.height / 2 * Math.cos(radians);
                let fireEndY = fireStartY + game.config.height / 2 * Math.sin(radians);
                let lineOfFire = new Phaser.Geom.Line(fireStartX, fireStartY, fireEndX, fireEndY);

                // loop through all targets
                this.targets.getChildren().forEach(function(target){
                    if(target.visible){

                        // get target bounding box
                        let bounds = target.getBounds();

                        // check if the line intersect the bounding box
                        if(Phaser.Geom.Intersects.LineToRectangle(lineOfFire, bounds)){

                            // target HIT!!!! hide it for 3 seconds
                            target.visible = false;
                            this.time.addEvent({
                                delay: 3000,
                                callback: function(){
                                    target.visible = true;
                                }
                            });
                        }
                    }
                }.bind(this))
            }
        }, this);
    }
};

We were able, once more, to build a fully featured prototype of a hyper casual game in less than 100 lines of code thanks to Phaser. Download the source code.

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