Talking about Game development, HTML5, Javascript and Phaser.
There are great news for all HTML5 game developers and Phaser enthusiasts: the long awaited 3.16 version has been released and brings a complete scale manager and also a full screen feature. If you used to code with Phaser 2, probably the feature you missed the most once you moved on to Phaser 3 was the scale manager. You had to move and resize the canvas on your own, using your JavaScript and CSS to sort the things out. For this reason I published the post How to scale your HTML5 games if your framework does not feature a scale manager – or if you do not use any framework. Finally, the scale manager is back in Phaser 3, and it also comes with a full screen feature. Look at the endless runner game I am blogging about during these days: Click or tap to jump or double jump. Once the focus is on the iframe hosting the game, pressing SPACEBAR will make the game run in full screen. What changed since previous version? First, the code in the page hosting the game has no CSS, switching from this code:
<!DOCTYPE html>
<html>
<head>
<style type = "text/css">
body{
background: #000000;
padding: 0px;
margin: 0px;
}
canvas{
display:block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<script src = "phaser.min.js"></script>
<script src = "game.js"></script>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<style type = "text/css">
body{
background: #000000;
padding: 0px;
margin: 0px;
}
</style>
<script src = "phaser.min.js"></script>
<script src = "game.js"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
div
where to place the game itself
What about the game script? When you define the object with game configuration, you will be able to define the scale mode, like in this case:
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 1334,
height: 750
}
thegame
, with a 1334×750 size.
Full screen management is even easier:
if(!this.scale.isFullscreen){
this.scale.startFullscreen();
}
let game;
// global game options
let gameOptions = {
// platform speed range, in pixels per second
platformSpeedRange: [300, 300],
// mountain speed, in pixels per second
mountainSpeed: 80,
// 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,
// % of probability a fire appears on the platform
firePercent: 25
}
window.onload = function() {
// object containing configuration options
let gameConfig = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 1334,
height: 750
},
scene: [preloadGame, playGame],
backgroundColor: 0x0c88c7,
// physics settings
physics: {
default: "arcade"
}
}
game = new Phaser.Game(gameConfig);
window.focus();
}
// 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
});
// the firecamp is a sprite sheet made by 32x58 pixels
this.load.spritesheet("fire", "fire.png", {
frameWidth: 40,
frameHeight: 70
});
// mountains are a sprite sheet made by 512x512 pixels
this.load.spritesheet("mountain", "mountain.png", {
frameWidth: 512,
frameHeight: 512
});
}
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
});
// setting fire animation
this.anims.create({
key: "burn",
frames: this.anims.generateFrameNumbers("fire", {
start: 0,
end: 4
}),
frameRate: 15,
repeat: -1
});
this.scene.start("PlayGame");
}
}
// playGame scene
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
create(){
// group with all active mountains.
this.mountainGroup = this.add.group();
// 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)
}
});
// group with all active firecamps.
this.fireGroup = this.add.group({
// once a firecamp is removed, it's added to the pool
removeCallback: function(fire){
fire.scene.firePool.add(fire)
}
});
// fire pool
this.firePool = this.add.group({
// once a fire is removed from the pool, it's added to the active fire group
removeCallback: function(fire){
fire.scene.fireGroup.add(fire)
}
});
// adding a mountain
this.addMountains()
// keeping track of added platforms
this.addedPlatforms = 0;
// 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);
this.player.setDepth(2);
// the player is not dying
this.dying = false;
// setting collisions between the player and the platform group
this.platformCollider = 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);
// setting collisions between the player and the fire group
this.physics.add.overlap(this.player, this.fireGroup, function(player, fire){
this.dying = true;
this.player.anims.stop();
this.player.setFrame(2);
this.player.body.setVelocityY(-200);
this.physics.world.removeCollider(this.platformCollider);
}, null, this);
// checking for input
this.input.on("pointerdown", this.jump, this);
this.add.text(20, 0, "Press SPACEBAR to enter fullscreen", {
fontFamily: "Arial",
fontSize: 64,
color: "#ffffff"
});
this.input.keyboard.on("keydown_SPACE", function(){
if(!this.scale.isFullscreen){
this.scale.startFullscreen();
}
}, this);
}
// adding mountains
addMountains(){
let rightmostMountain = this.getRightmostMountain();
if(rightmostMountain < game.config.width * 2){
let mountain = this.physics.add.sprite(rightmostMountain + Phaser.Math.Between(100, 350), game.config.height + Phaser.Math.Between(0, 100), "mountain");
mountain.setOrigin(0.5, 1);
mountain.body.setVelocityX(gameOptions.mountainSpeed * -1)
this.mountainGroup.add(mountain);
if(Phaser.Math.Between(0, 1)){
mountain.setDepth(1);
}
mountain.setFrame(Phaser.Math.Between(0, 3))
this.addMountains()
}
}
// getting rightmost mountain x position
getRightmostMountain(){
let rightmostMountain = -200;
this.mountainGroup.getChildren().forEach(function(mountain){
rightmostMountain = Math.max(rightmostMountain, mountain.x);
})
return rightmostMountain;
}
// 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);
platform.setDepth(2);
this.platformGroup.add(platform);
}
this.nextPlatformDistance = Phaser.Math.Between(gameOptions.spawnRange[0], gameOptions.spawnRange[1]);
// if this is not the starting platform...
if(this.addedPlatforms > 1){
// is there a coin over the platform?
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");
coin.setDepth(2);
this.coinGroup.add(coin);
}
}
// is there a fire over the platform?
if(Phaser.Math.Between(1, 100) <= gameOptions.firePercent){
if(this.firePool.getLength()){
let fire = this.firePool.getFirst();
fire.x = posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth);
fire.y = posY - 46;
fire.alpha = 1;
fire.active = true;
fire.visible = true;
this.firePool.remove(fire);
}
else{
let fire = this.physics.add.sprite(posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth), posY - 46, "fire");
fire.setImmovable(true);
fire.setVelocityX(platform.body.velocity.x);
fire.setSize(8, 2, true)
fire.anims.play("burn");
fire.setDepth(2);
this.fireGroup.add(fire);
}
}
}
}
// 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
// and obviously if the player is not dying
jump(){
if((!this.dying) && (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);
// recycling fire
this.fireGroup.getChildren().forEach(function(fire){
if(fire.x < - fire.displayWidth / 2){
this.fireGroup.killAndHide(fire);
this.fireGroup.remove(fire);
}
}, this);
// recycling mountains
this.mountainGroup.getChildren().forEach(function(mountain){
if(mountain.x < - mountain.displayWidth){
let rightmostMountain = this.getRightmostMountain();
mountain.x = rightmostMountain + Phaser.Math.Between(100, 350);
mountain.y = game.config.height + Phaser.Math.Between(0, 100);
mountain.setFrame(Phaser.Math.Between(0, 3))
if(Phaser.Math.Between(0, 1)){
mountain.setDepth(1);
}
}
}, 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);
}
}
};
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.