HTML5 endless runner built with Phaser and Arcade physics step 3: adding textures to platforms and coins to collect
Here we are with the 3rd part of the HTML5 endless runner prototype powered by Phaser and Arcade physics.
In first step, I was able to set up a basic endless runner with double jump and room for customization using object pooling to save resources, but it was made only by a white rectangle jumping over white platforms, so in second step I added even more room for customization and – most important – a player animation.
Now I added texture to platforms – no more white boxes – and coins to collect. Coins have their own animation and once collected they disappear in a way similar to the one you used to see in Super Mario series.
Have a look at the game:
Click or tap to jump or double jump, try to collect coins.
Coin and platform graphics are taken from the awesome Kenney site.
What about animations? I found it better to declare them in a preload scene, at lines 84-107 so they are only created once and do not throw a warning when the game restarts.
As for the platforms, they are tile sprites. A TileSprite is a sprite that has a repeating texture which can be scrolled and scaled independently of the TileSprite itself.
The scaling feature comes in hand when you retrieve a platform from the pool and need to change its width by resizing it but you want the texture not to be resized along with the platform.
Have a look at the source code, fairly commented:
let game;
// global game options
let gameOptions = {
// platform speed range, in pixels per second
platformSpeedRange: [300, 300],
// spawn range, how far should be the rightmost platform from the right edge
// before next platform spawns, in pixels
spawnRange: [80, 300],
// platform width range, in pixels
platformSizeRange: [90, 300],
// a height range between rightmost platform and next platform to be spawned
platformHeightRange: [-5, 5],
// a scale to be multiplied by platformHeightRange
platformHeighScale: 20,
// platform max and min height, as screen height ratio
platformVerticalLimit: [0.4, 0.8],
// player gravity
playerGravity: 900,
// player jump force
jumpForce: 400,
// player starting X position
playerStartPosition: 200,
// consecutive jumps allowed
jumps: 2,
// % of probability a coin appears on the platform
coinPercent: 25
}
window.onload = function() {
// object containing configuration options
let gameConfig = {
type: Phaser.AUTO,
width: 1334,
height: 750,
scene: [preloadGame, playGame],
backgroundColor: 0x0c88c7,
// physics settings
physics: {
default: "arcade"
}
}
game = new Phaser.Game(gameConfig);
window.focus();
resize();
window.addEventListener("resize", resize, false);
}
// preloadGame scene
class preloadGame extends Phaser.Scene{
constructor(){
super("PreloadGame");
}
preload(){
this.load.image("platform", "platform.png");
// player is a sprite sheet made by 24x48 pixels
this.load.spritesheet("player", "player.png", {
frameWidth: 24,
frameHeight: 48
});
// the coin is a sprite sheet made by 20x20 pixels
this.load.spritesheet("coin", "coin.png", {
frameWidth: 20,
frameHeight: 20
});
}
create(){
// setting player animation
this.anims.create({
key: "run",
frames: this.anims.generateFrameNumbers("player", {
start: 0,
end: 1
}),
frameRate: 8,
repeat: -1
});
// setting coin animation
this.anims.create({
key: "rotate",
frames: this.anims.generateFrameNumbers("coin", {
start: 0,
end: 5
}),
frameRate: 15,
yoyo: true,
repeat: -1
});
this.scene.start("PlayGame");
}
}
// playGame scene
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
create(){
// keeping track of added platforms
this.addedPlatforms = 0;
// group with all active platforms.
this.platformGroup = this.add.group({
// once a platform is removed, it's added to the pool
removeCallback: function(platform){
platform.scene.platformPool.add(platform)
}
});
// platform pool
this.platformPool = this.add.group({
// once a platform is removed from the pool, it's added to the active platforms group
removeCallback: function(platform){
platform.scene.platformGroup.add(platform)
}
});
// group with all active coins.
this.coinGroup = this.add.group({
// once a coin is removed, it's added to the pool
removeCallback: function(coin){
coin.scene.coinPool.add(coin)
}
});
// coin pool
this.coinPool = this.add.group({
// once a coin is removed from the pool, it's added to the active coins group
removeCallback: function(coin){
coin.scene.coinGroup.add(coin)
}
});
// number of consecutive jumps made by the player so far
this.playerJumps = 0;
// adding a platform to the game, the arguments are platform width, x position and y position
this.addPlatform(game.config.width, game.config.width / 2, game.config.height * gameOptions.platformVerticalLimit[1]);
// adding the player;
this.player = this.physics.add.sprite(gameOptions.playerStartPosition, game.config.height * 0.7, "player");
this.player.setGravityY(gameOptions.playerGravity);
// setting collisions between the player and the platform group
this.physics.add.collider(this.player, this.platformGroup, function(){
// play "run" animation if the player is on a platform
if(!this.player.anims.isPlaying){
this.player.anims.play("run");
}
}, null, this);
// setting collisions between the player and the coin group
this.physics.add.overlap(this.player, this.coinGroup, function(player, coin){
this.tweens.add({
targets: coin,
y: coin.y - 100,
alpha: 0,
duration: 800,
ease: "Cubic.easeOut",
callbackScope: this,
onComplete: function(){
this.coinGroup.killAndHide(coin);
this.coinGroup.remove(coin);
}
});
}, null, this);
// checking for input
this.input.on("pointerdown", this.jump, this);
}
// the core of the script: platform are added from the pool or created on the fly
addPlatform(platformWidth, posX, posY){
this.addedPlatforms ++;
let platform;
if(this.platformPool.getLength()){
platform = this.platformPool.getFirst();
platform.x = posX;
platform.y = posY;
platform.active = true;
platform.visible = true;
this.platformPool.remove(platform);
let newRatio = platformWidth / platform.displayWidth;
platform.displayWidth = platformWidth;
platform.tileScaleX = 1 / platform.scaleX;
}
else{
platform = this.add.tileSprite(posX, posY, platformWidth, 32, "platform");
this.physics.add.existing(platform);
platform.body.setImmovable(true);
platform.body.setVelocityX(Phaser.Math.Between(gameOptions.platformSpeedRange[0], gameOptions.platformSpeedRange[1]) * -1);
this.platformGroup.add(platform);
}
this.nextPlatformDistance = Phaser.Math.Between(gameOptions.spawnRange[0], gameOptions.spawnRange[1]);
// is there a coin over the platform?
if(this.addedPlatforms > 1){
if(Phaser.Math.Between(1, 100) <= gameOptions.coinPercent){
if(this.coinPool.getLength()){
let coin = this.coinPool.getFirst();
coin.x = posX;
coin.y = posY - 96;
coin.alpha = 1;
coin.active = true;
coin.visible = true;
this.coinPool.remove(coin);
}
else{
let coin = this.physics.add.sprite(posX, posY - 96, "coin");
coin.setImmovable(true);
coin.setVelocityX(platform.body.velocity.x);
coin.anims.play("rotate");
this.coinGroup.add(coin);
}
}
}
}
// the player jumps when on the ground, or once in the air as long as there are jumps left and the first jump was on the ground
jump(){
if(this.player.body.touching.down || (this.playerJumps > 0 && this.playerJumps < gameOptions.jumps)){
if(this.player.body.touching.down){
this.playerJumps = 0;
}
this.player.setVelocityY(gameOptions.jumpForce * -1);
this.playerJumps ++;
// stops animation
this.player.anims.stop();
}
}
update(){
// game over
if(this.player.y > game.config.height){
this.scene.start("PlayGame");
}
this.player.x = gameOptions.playerStartPosition;
// recycling platforms
let minDistance = game.config.width;
let rightmostPlatformHeight = 0;
this.platformGroup.getChildren().forEach(function(platform){
let platformDistance = game.config.width - platform.x - platform.displayWidth / 2;
if(platformDistance < minDistance){
minDistance = platformDistance;
rightmostPlatformHeight = platform.y;
}
if(platform.x < - platform.displayWidth / 2){
this.platformGroup.killAndHide(platform);
this.platformGroup.remove(platform);
}
}, this);
// recycling coins
this.coinGroup.getChildren().forEach(function(coin){
if(coin.x < - coin.displayWidth / 2){
this.coinGroup.killAndHide(coin);
this.coinGroup.remove(coin);
}
}, this);
// adding new platforms
if(minDistance > this.nextPlatformDistance){
let nextPlatformWidth = Phaser.Math.Between(gameOptions.platformSizeRange[0], gameOptions.platformSizeRange[1]);
let platformRandomHeight = gameOptions.platformHeighScale * Phaser.Math.Between(gameOptions.platformHeightRange[0], gameOptions.platformHeightRange[1]);
let nextPlatformGap = rightmostPlatformHeight + platformRandomHeight;
let minPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[0];
let maxPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[1];
let nextPlatformHeight = Phaser.Math.Clamp(nextPlatformGap, minPlatformHeight, maxPlatformHeight);
this.addPlatform(nextPlatformWidth, game.config.width + nextPlatformWidth / 2, nextPlatformHeight);
}
}
};
function resize(){
let canvas = document.querySelector("canvas");
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let windowRatio = windowWidth / windowHeight;
let gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
During next steps I’ll add more comments, enemies and a scrolling background, meanwhile download the source code