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.