Build a HTML5 game like mobile smashing hit “Spinny Gun” with no physics engines using Phaser 3 and paths

Read all posts about "" game

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.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers