Talking about Circular endless runner game, Game development, HTML5, Javascript, Phaser and TypeScript.
In my opinion the circular endless runner prototype has potential, so I decided to update it to latest Phaser version, rewrite it in TypeScript and add some optimization.
No physics engines have been used, everything is controlled by trigonometry and geometry.
Look at the result:
Tap or click to jump and avoid the spikes. You can perform double and even triple jump.
All spikes are recycled and once they disappear from one quadrant of the circle, they appear on the next quadrant.
And here it is the completely commented source code, which consists in one HTML file, one CSS file and six TypeScript files:
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, 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.
/* remove margin and padding from all elements */
* {
padding : 0;
margin : 0;
}
/* set body background color */
body {
background-color : #000000;
}
/* Disable browser handling of all panning and zooming gestures. */
canvas {
touch-action : none;
}
gameOptions.ts
Configurable game options.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions : any = {
gameSize : {
width : 800, // width of the game, in pixels
height : 800 // height of the game, in pixels
},
gameBackgroundColor : 0x222222, // game background color
bigCircleRadius : 250, // radius of the big circle - the "planet" - in pixels
playerRadius : 25, // radius of the small circle, in pixels
playerSpeed : 60, // player speed, in degrees per second
worldGravity : 2500, // world gravity
jumpForce : [600, 500, 400], // jump force. First element is the first jump, second element - if any - the double jump, third element - if any - the triple jump and so on
spikeSize : 50, // size of the square where the spike, which is an isosceles triangle, is inscribed
spikeHeightRange : [4, 40], // spikes can have different height, which range is set in this array
closeToSpike : 20, // distance needed to consider the player close to a spike, in degrees. Useful for colliision detection
farFromSpike : 35, // distance needed to consider the player far from a spike, in degrees. Useful for colliision detection
spikesPerQuadrant : 4 // amount of spikes for each quadrant
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser'; // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame'; // playGame scene
import { GameOptions } from './gameOptions'; // game options
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT, // adjust size to automatically fit in the window
autoCenter : Phaser.Scale.CENTER_BOTH, // center the game horizontally and vertically
parent : 'thegame', // DOM id where to render the game
width : GameOptions.gameSize.width, // game width, in pixels
height : GameOptions.gameSize.height // game height, in pixels
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO, // game renderer
backgroundColor : GameOptions.gameBackgroundColor, // game background color
scale : scaleObject, // scale settings
scene : [ // array with game scenes
PreloadAssets, // PreloadAssets scene
PlayGame // PlayGame scene
]
}
// the game itself
new Phaser.Game(configObject);
scenes > preloadAssets.ts
Here we preload all assets to be used in the game.
// CLASS TO PRELOAD ASSETS
// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be called during class preloading
preload() : void {
// load images
this.load.image('bigcircle', 'assets/sprites/bigcircle.png'); // the big circle, aka the planet
this.load.image('player', 'assets/sprites/player.png'); // the player
this.load.image('spike', 'assets/sprites/spike.png'); // the spike
this.load.image('particle', 'assets/sprites/particle.png'); // a small circle used for particle effects
}
// method to be executed when the scene is created
create() : void {
// start PlayGame scene
this.scene.start('PlayGame');
}
}
scenes > playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
// modules to import
import { GameOptions } from '../gameOptions'; // game options
import { PhysicsCircle } from '../physicsCircle'; // physics circle
import { PhysicsSpike } from '../physicsSpike'; // physics spike
// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
player : PhysicsCircle; // the player
spikeArray : PhysicsSpike[]; // group with all spikes
trailEmitter : Phaser.GameObjects.Particles.ParticleEmitter // trail particle emitter
// method to be called once the instance has been created
create() : void {
// set some custom data
this.data.set({
gameOver : false, // is the game over?
centerX : this.game.config.width as number / 2, // horizontal center of the game
centerY : this.game.config.height as number / 2 // vertical center of the game
})
// place and resize big circle
const bigCircle : Phaser.GameObjects.Sprite = this.add.sprite(this.data.get('centerX'), this.data.get('centerY'), 'bigcircle');
bigCircle.displayWidth = GameOptions.bigCircleRadius * 2;
bigCircle.displayHeight = GameOptions.bigCircleRadius * 2;
// initialize spikeArray vector
this.spikeArray = [];
// place spikes
for (let i : number = 0; i < 3 * GameOptions.spikesPerQuadrant; i ++) {
// create a spike
const spike : PhysicsSpike = new PhysicsSpike(this);
// addi the spike to spike array
this.spikeArray.push(spike);
// determine the quadrant where to place the spike
// 0 : bottom-right
// 1 : bottom-left
// 2 : top-left
// 3 : top-right
const quadrant : number = Math.floor(i % 3);
// place the spike in the given quadrant.
// I don't want spikes in the quadrant 2 at the beginning, because it's the quadrant where the player spawns
spike.place(quadrant != 2 ? quadrant : 3);
}
// create the player
this.player = new PhysicsCircle(this);
// create the particle trail emitter and make it follow the player
this.trailEmitter = this.add.particles(this.player.x, this.player.y, 'particle', {
lifespan : 900, // particle life span, in mmilliseconds
alpha : {
start : 1, // alpha start value, 0 = transparent; 1 = opaque
end : 0 // alpha end value
},
scale : {
start : 1, // scale start value
end : 0.8 // scale end value
},
quantity : 1, // amount of particle to be fired
frequency : 150 // particle frequency, in milliseconds
}).startFollow(this.player);
// handle player input on touch or clicl
this.input.on('pointerdown', () => {
// make player jump
this.player.jump();
});
}
// method to be called when the game is over
gameOver() : void {
// set gameOver data to true
this.data.set('gameOver', true);
// shake the camera
this.cameras.main.shake(800, 0.01);
// hide the player
this.player.setVisible(false);
// stop trail emitter
this.trailEmitter.stop();
// add a particle explosion effect
this.add.particles(this.player.x, this.player.y, 'particle', {
lifespan : 900, // particle life span, in milliseconds
alpha : {
start : 1, // alpha start value, 0 = transparent; 1 = opaque
end : 0 // alpha end value
},
scale : {
start : 0.6, // scale start value
end : 0.02 // scale end value
},
speed: {
min : -150, // minimum speed, in pixels per second
max : 150 // maximum speed, in pixels per second
},
}).explode(100);
// add a timer event
this.time.addEvent({
delay : 2000, // delay, in milliseconds
callback : () => { // callback function
// start PlayGame scene
this.scene.start('PlayGame');
}
});
}
// metod to be called at each frame
// time : time passed since the beginning, in milliseconds
// deltaTime : time passed since last frame, in milliseconds
update(time : number, deltaTime : number) {
// if the game is over, do nothing
if (this.data.get('gameOver')) {
return;
}
// move the player, according to deltaTime
this.player.move(deltaTime / 1000);
// loop through all spikes
this.spikeArray.forEach((spike : PhysicsSpike) => {
// get angle difference between spike and player
const angleDifference : number = Math.abs(Phaser.Math.Angle.ShortestBetween(spike.angle, this.player.currentAngle));
// if the player is not approaching the spike and it's close enough...
if (!spike.approaching && angleDifference < GameOptions.closeToSpike) {
// player is now approaching the spike
spike.approaching = true;
}
// if the player is approaching the spike...
if (spike.approaching) {
// if spike triangle shape and player circle shape intersect...
if (Phaser.Geom.Intersects.TriangleToCircle(spike.triangle, this.player.circle)) {
// the game is over!
this.gameOver();
}
// if we are getting too far from the spike...
if (angleDifference > GameOptions.farFromSpike) {
// recycle the spike making it disappear
spike.disappear();
}
}
});
}
}
physicsCircle.ts
Custom class for the physics circle.
// THE PHYSICS CIRCLE
// modules to import
import { GameOptions } from './gameOptions'; // game options
// PhysicsCircle class extends Phaser.GameObjects.Sprite
export class PhysicsCircle extends Phaser.GameObjects.Sprite {
currentAngle : number; // current angle around the planet
jumpHeight : number; // height reached while jumping
jumps : number; // amount of consecutive jumps already performed
jumpForce : number; // jump force to be applied at each frame
circle : Phaser.Geom.Circle; // geometric circle representing the sprite, useful for collision detection
constructor(scene : Phaser.Scene) {
// create and add the instance
super(scene, 0, 0, 'player');
scene.add.existing(this);
// adjust display size according to game options
this.displayWidth = GameOptions.playerRadius * 2;
this.displayHeight = GameOptions.playerRadius * 2;
// player starts from -150 degrees
this.currentAngle = -150;
// jump height, force and amount of jumps start at zero
this.jumpHeight = 0;
this.jumps = 0;
this.jumpForce = 0;
// geometric circle definition
this.circle = new Phaser.Geom.Circle(0, 0, GameOptions.playerRadius);
// set circle depth
this.setDepth(2);
}
// method to be called to make the circle jump
jump() : void {
// can the player jump?
if (this.jumps < GameOptions.jumpForce.length) {
// player is jumping once more
this.jumps ++;
// adding the proper jump force to player's jumpForce property
// is jumpForce greater than zero? That is, is the player still gaining height?
if (this.jumpForce > 0) {
// then add the proper jump force to current force to make the player jump higher
this.jumpForce += GameOptions.jumpForce[this.jumps - 1];
}
// in this case the player is falling...
else {
// so the jump force is set to make player gain height once more
this.jumpForce = GameOptions.jumpForce[this.jumps - 1];
}
}
}
// method to move the player
// s : seconds passed since last frame
move(s : number) : void {
// is the player jumping?
if (this.jumps > 0) {
// adjust player jump height
this.jumpHeight += this.jumpForce * s;
// decrease jump force due to gravity
this.jumpForce -= GameOptions.worldGravity * s;
// if jumpHeight is less than zero, it means the player touched the ground
if (this.jumpHeight < 0) {
// setting jump height to zero
this.jumpHeight = 0;
// player is not jumping anymore
this.jumps = 0;
// there is no jump force
this.jumpForce = 0;
}
}
// deltaAnge is the amount in degrees gained in "s" seconds
const deltaAngle : number = GameOptions.playerSpeed * s;
// adjust current angle according to delta angle
this.currentAngle = Phaser.Math.Angle.WrapDegrees(this.currentAngle + deltaAngle);
// also get the current angle in radians
const radians : number = Phaser.Math.DegToRad(this.currentAngle);
// get the distance from center according to big circle radius, player radius and jump height
const distanceFromCenter : number = GameOptions.bigCircleRadius + GameOptions.playerRadius + this.jumpHeight;
// adjust player position
this.setPosition(this.scene.data.get('centerX') + distanceFromCenter * Math.cos(radians), this.scene.data.get('centerY') + distanceFromCenter * Math.sin(radians));
// also adjust geometric circle position
this.circle.setPosition(this.x, this.y);
// determine player revolution around the big circle
const revolutions : number = GameOptions.bigCircleRadius / GameOptions.playerRadius + 1;
// adjust player rotation
this.setAngle(this.currentAngle * revolutions);
}
}
physicsSpike.ts
Custom class for the physics spike.
// THE PHYSICS SPIKE
// modules to import
import { GameOptions } from './gameOptions'; // game options
// PhysicsSpike class extends Phaser.GameObjects.Sprite
export class PhysicsSpike extends Phaser.GameObjects.Sprite {
quadrant : number; // spike's quadrant
triangle : Phaser.Geom.Triangle; // geometric triangle representing the spike
approaching : boolean; // is the spike being approached by the player?
constructor(scene : Phaser.Scene) {
// create and add the instance
super(scene, 0, 0, 'spike');
scene.add.existing(this);
// set registration point to left, vertical center
this.setOrigin(0, 0.5);
}
// method to place a spike
// quadrant : quadrant where to place the spike
// 0 : bottom-right
// 1 : bottom-left
// 2 : top-left
// 3 : top-right
place(quadrant : number) : void {
// choose a random angle in the proper quadrant
const randomAngle : number = Phaser.Math.Angle.WrapDegrees(Phaser.Math.Between(quadrant * 90, (quadrant + 1) * 90));
// this is the same random angle converted in radians
const randomAngleRadians : number = Phaser.Math.DegToRad(randomAngle);
// set spike start position, completely inside the big circle
const spikeStartX : number = this.scene.data.get('centerX') + (GameOptions.bigCircleRadius - this.displayWidth) * Math.cos(randomAngleRadians);
const spikeStartY : number = this.scene.data.get('centerY') + (GameOptions.bigCircleRadius - this.displayWidth) * Math.sin(randomAngleRadians);
// place the spike in proper position
this.setPosition(spikeStartX, spikeStartY);
// determine spike position final according to its angle
var spikeX = this.scene.data.get('centerX') + (GameOptions.bigCircleRadius - Phaser.Math.Between(GameOptions.spikeHeightRange[0], GameOptions.spikeHeightRange[1])) * Math.cos(randomAngleRadians);
var spikeY = this.scene.data.get('centerY') + (GameOptions.bigCircleRadius - Phaser.Math.Between(GameOptions.spikeHeightRange[0], GameOptions.spikeHeightRange[1])) * Math.sin(randomAngleRadians);
// add a tween to make spike appear
this.scene.tweens.add({
targets : this, // tween target, the spike itself
x : spikeX, // final x position
y : spikeY, // final y position
duration : 500, // tween duration in milliseconds
ease : Phaser.Math.Easing.Cubic.Out // tween ease
})
// save spike's quadrant in a custom property
this.quadrant = quadrant;
// set spike angke
this.angle = randomAngle;
// build the geometric triangle which will be used for collision
this.triangle = new Phaser.Geom.Triangle(
spikeX + GameOptions.spikeSize * Math.cos(randomAngleRadians),
spikeY + GameOptions.spikeSize * Math.sin(randomAngleRadians),
spikeX + GameOptions.spikeSize / 2 * Math.cos(randomAngleRadians + Math.PI / 2),
spikeY + GameOptions.spikeSize / 2 * Math.sin(randomAngleRadians + Math.PI / 2),
spikeX + GameOptions.spikeSize / 2 * Math.cos(randomAngleRadians - Math.PI / 2),
spikeY + GameOptions.spikeSize / 2 * Math.sin(randomAngleRadians - Math.PI / 2));
// is the player approaching to the spike?
this.approaching = false;
}
// method to make the spike disappear
disappear() : void {
// the spike is not being approached by player
this.approaching = false;
// set spike start position, completely inside the big circle
const spikeStartX : number = this.scene.game.config.width as number / 2 + (GameOptions.bigCircleRadius - this.displayWidth) * Math.cos(this.rotation);
const spikeStartY : number = this.scene.game.config.height as number / 2 + (GameOptions.bigCircleRadius - this.displayWidth) * Math.sin(this.rotation);
// add a tween to make spike disappear into its starting position
this.scene.tweens.add({
targets : this, // tween target, the spike itself
x : spikeStartX, // final x position
y : spikeStartY, // final y position
duration : 500, // tween duration, in milliseconds
ease : Phaser.Math.Easing.Cubic.In, // tween ease
onComplete : () => { // function to be called once the tween is complete
// place the spike in the previous quadrant
this.place((this.quadrant + 3) % 4);
}
})
}
}
And now you have a fully functional circular endless runner engine. Download the source code along with the entire wepack project.
Don’t know where to start developing with Phaser and TypeScript? I’ll explain it to you step by step in this free minibook.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.