Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Run Around game, Game development, HTML5, Javascript and Phaser.

Did you play Run Around?

In the original game you can find both for iOS and Android, you play as a stickman as you run around the circle and jump over obstacles. Complete the circle to pass levels.

This particular kind of game can be described as a circular endless runner, and I already developed a circular endless runner prototype, the only difference is this time the player is running inside the circle, painting it, and we also need to keep track of the amount of circumference the player already painted.

Look at this prototype. I kept the feature to make the player jump and double jump:

Tap to jump and double jump and see how the player paints the big circle.

The game features no physics, every circle movement has been done using trigonometry.

The trickiest part was to draw the painted path, but I just acted like it was a straight line, keeping track of the painted arcs/segments and merging them into bigger arcs/segments with an external script, because I did not want to reinvent the wheel.

Have a look at the commented source code:

// the game itself
let game;

// global object with configuration options
let gameOptions = {

    // radius of the big circle, in pixels
    bigCircleRadius: 300,

    // thickness of the big circle, in pixels
    bigCircleThickness: 20,

    // radius of the player, in pixels
    playerRadius: 25,

    // player speed, in degrees per frame
    playerSpeed: 0.6,

    // world gravity
    worldGravity: 0.8,

    // jump force of the single and double jump
    jumpForce: [12, 8]
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.CANVAS,
        backgroundColor: 0x444444,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 800,
            height: 800
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("player", "player.png");
    }
    create(){

        // array to store all painted arcs
        this.paintedArcs = [];

        // calculate the distance from the center of the canvas and the big circle
        this.distanceFromCenter = gameOptions.bigCircleRadius - gameOptions.playerRadius - gameOptions.bigCircleThickness / 2;

        // draw the big circle
        this.bigCircle = this.add.graphics();
        this.bigCircle.lineStyle(gameOptions.bigCircleThickness, 0xffffff);
        this.bigCircle.strokeCircle(game.config.width / 2, game.config.height / 2, gameOptions.bigCircleRadius);

        // graphics object where to draw the highlight circle
        this.highlightCircle = this.add.graphics();

        // add player sprite
        this.player = this.add.sprite(game.config.width / 2, game.config.height / 2 - this.distanceFromCenter, "player");
        this.player.displayWidth = gameOptions.playerRadius * 2;
        this.player.displayHeight = gameOptions.playerRadius * 2;

        // player current angle, on top of the big circle
        this.player.currentAngle = -90;

        // player previous angle, at the moment same value of current angle
        this.player.previousAngle = this.player.currentAngle;

        // jump offset, the distance from the ground and player position during jumps
        this.player.jumpOffset = 0;

        // counter to keep track of player jumps
        this.player.jumps = 0;

        // current jump force
        this.player.jumpForce = 0;

        // input listener
        this.input.on("pointerdown", function(){

            // if the player jumped less than 2 times...
            if(this.player.jumps < 2){

                // one more jump
                this.player.jumps ++;

                // add to player jump force the proper force according to the number of jumps performed
                this.player.jumpForce = gameOptions.jumpForce[this.player.jumps - 1];
            }
        }, this);

        // text to display player progress
        this.levelText = this.add.text(game.config.width / 2, game.config.height / 2, "", {
            fontFamily: "Arial",
            fontSize: 96,
            color: "#00ff00"
        });
        this.levelText.setOrigin(0.5);
    }

    // method to be executed at each frame
    update(){

        // if the player is jumping...
        if(this.player.jumps > 0){

            // increase player jump offset according to jump force
            this.player.jumpOffset += this.player.jumpForce;

            // decrease jump force ti simulate gravity
            this.player.jumpForce -= gameOptions.worldGravity;

            // if jump offset is zero or less than zero, that is the player is on the ground...
            if(this.player.jumpOffset < 0){

                // set jump offset to zero
                this.player.jumpOffset = 0;

                // player is not jumping
                this.player.jumps = 0;

                // player has no jump force
                this.player.jumpForce = 0;
            }
        }

        // update previous angle to current angle
        this.player.previousAngle = this.player.currentAngle;

        // update current angle adding player speed
        this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + gameOptions.playerSpeed);

        // if player is not jumping...
        if(this.player.jumpOffset == 0){

            // set painted ratio to zero
            this.paintedRatio = 0;

            // convert Phaser angles to a more readable angles where zero is on top, 90 is right, 180 down, 270 left
            let currentAngle = this.getGameAngle(this.player.currentAngle);
            let previousAngle = this.getGameAngle(this.player.previousAngle);

            // if current angle is greater than previous angle...
            if(currentAngle >= previousAngle){

                // put in paintedArcs array a new arc
                this.paintedArcs.push([previousAngle, currentAngle]);
            }
            else{

                // this is the case player passed from a value less than 360 to a value greater than 360, which is zero
                // we manage this case as a couple of arcs
                this.paintedArcs.push([previousAngle, 360]);
                this.paintedArcs.push([0, currentAngle]);
            }

            // prepare highlightCircle graphic object to draw
            this.highlightCircle.clear();
            this.highlightCircle.lineStyle(gameOptions.bigCircleThickness, 0xff00ff);

            // merge small arcs into bigger arcs, if possible
            this.paintedArcs = this.mergeIntervals(this.paintedArcs);

            // loop through all arcs
            this.paintedArcs.forEach(function(i){

                // increase painted ratio value with arc length
                this.paintedRatio += (i[1] - i[0]);

                // draw the arc
                this.highlightCircle.beginPath();
                this.highlightCircle.arc(game.config.width / 2, game.config.height / 2, gameOptions.bigCircleRadius, Phaser.Math.DegToRad(i[0] - 90), Phaser.Math.DegToRad(i[1] - 90), false);
                this.highlightCircle.strokePath();
            }.bind(this));

            // convert the sum of all arcs lenght into a 0 -> 100 value
            this.paintedRatio = Math.round(this.paintedRatio * 100 / 360);

            // update player progress text
            this.levelText.setText(this.paintedRatio + "%");

            // if the player painted the whole circle...
            if(this.paintedRatio == 100){

                // ... restart the game in two seconds
                this.time.addEvent({
                    delay: 2000,
                    callbackScope: this,
                    callback: function(){
                        this.scene.start("PlayGame");
                    }
                });
            }
        }

        // transform degrees to radians
        let radians = Phaser.Math.DegToRad(this.player.currentAngle);

        // determine distance from center according to jump offset
        let distanceFromCenter = this.distanceFromCenter - this.player.jumpOffset;

        // position player using trigonometry
        this.player.x = game.config.width / 2 + distanceFromCenter * Math.cos(radians);
        this.player.y = game.config.height / 2 + distanceFromCenter * Math.sin(radians);

        // rotate player using trigonometry
        let revolutions = gameOptions.bigCircleRadius / gameOptions.playerRadius + 1;
        this.player.angle = -this.player.currentAngle * revolutions;
    }

    // method to convert Phaser angles to a more readable angles
    getGameAngle(angle){
        let gameAngle = angle + 90;
        if(gameAngle < 0){
            gameAngle = 360 + gameAngle
        }
        return gameAngle;
    }

    // method to merge intervals, found at
    // https://gist.github.com/vrachieru/5649bce26004d8a4682b
    mergeIntervals(intervals){
        if(intervals.length <= 1){
            return intervals;
        }
        let stack = [];
        let top = null;
        intervals = intervals.sort(function(a, b){
            return a[0] - b[0]
        });
        stack.push(intervals[0]);
        for(let i = 1; i < intervals.length; i++){
            top = stack[stack.length - 1];
            if(top[1] < intervals[i][0]){
                stack.push(intervals[i]);
            }
            else{
                if (top[1] < intervals[i][1]){
                    top[1] = intervals[i][1];
                    stack.pop();
                    stack.push(top);
                }
            }
        }
        return stack;
    }
}

Next time we’ll add enemies, meanwhile 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.