Talking about Game development, HTML5, Javascript and Phaser.
Today is the blue monday, reported to be the most depressing day of the year. But we don’t want depression to be among us, so my take on making depression go away and letting happiness come in is to give you the full commented source code of “Just Jump” game, ready to be published on iOS and Android devices as a landscape game. What’s the difference between the game I am going to release today and the original version? Easy, this version is optimize to run on any mobile device in landscape mode, and it will be published on iOS and Android app stores to show you the entire process. The game will have a fixed width which covers the entire width of the device, while its height is stretched to cover the entire height of the devices, then floors height is calculated accordingly. Look, this is how it would look on a iPhone 6: And this is how it would look on an iPad: As you can see, width remain intact while overall game height and floor height changes to cover the full screen area. And here is the gift to wash away the blue monday, the complete and commented source code:
// the game itself
var game;
// global object with all game options
var gameOptions = {
// game width
gameWidth: 840,
// height of each floor
floorHeight: 20,
// array with vertical floors potision
floorY: [],
// number of floors
floorsAmount: 4,
// number of spikes per floor
spikesAmount: 4,
// height of each spike
spikeHeight: 40,
// size of the hero
squareSize: 16,
// horizontal speed of the hero
squareSpeed: 170,
// game gravity
squareGravity: 450,
// force to be applied at each jump
jumpForce: -210,
// jump tween length, in milliseconds
jumpTime: 300,
// colors used in the game
levelColors: [0xe81d62, 0x9b26af, 0x2095f2, 0x4bae4f, 0xfeea3a, 0x795548, 0x5f7c8a],
// local storage name, it's the variable we will be using to save game information such as best score
localStorageName: "justjumpgame",
// just a string with version number to be displayed
version: "1.1m"
}
// when the window loads
window.onload = function() {
// determining window width/height ratio
var windowRatio = window.innerWidth / window.innerHeight;
// we already have in mind to use 100% of window width with game canvas, so let's
// calculate game height to cover the entire height of the window
var gameHeight = gameOptions.gameWidth / windowRatio;
// now we prepare to split the screen into floorsAmount floors, and each floor will take
// 1 / floorsAmount of game height. We use these values to populate floorY array
for(var i = 1; i <= gameOptions.floorsAmount; i++){
gameOptions.floorY.push(gameHeight / gameOptions.floorsAmount * i - gameOptions.floorHeight);
}
// game creation
game = new Phaser.Game(gameOptions.gameWidth, gameHeight);
// adding game state
game.state.add("TheGame", TheGame);
// starting game state
game.state.start("TheGame");
}
var TheGame = function(){};
TheGame.prototype = {
// when the state preloads
preload: function(){
// setting the game on maximum scale mode to cover the entire screen
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
game.scale.pageAlignHorizontally = true;
game.scale.pageAlignVertically = true;
// the game will keep running even when it loses the focus
game.stage.disableVisibilityChange = true;
// preloading the only game assets, a tile which will be used both for the square and the floor
game.load.image("tile", "assets/sprites/tile.png");
// preloading the bitmap font, generated with Littera bitmap font generator
game.load.bitmapFont("font", "assets/fonts/font.png", "assets/fonts/font.fnt");
// preloading the two audio files used in the game
game.load.audio("jump", "assets/sounds/jump.mp3");
game.load.audio("explosion", "assets/sounds/explosion.mp3");
},
// once the state is ready
create: function(){
// handling local storage to retrieve the previously saved high score or to create a new local storage object with a zero score
this.savedData = localStorage.getItem(gameOptions.localStorageName) == null ? {score : 0} : JSON.parse(localStorage.getItem(gameOptions.localStorageName));
// assigning the two sounds to variables to be called later
this.jumpSound = game.add.audio("jump");
this.explosionSound = game.add.audio("explosion");
// variable to tell us if we are in demo mode, that is when the square jumps automatically
this.demo = true;
// variable to tell us if it's game over
this.gameOver = false;
// in this array we will store floor colors
this.floorColors = [];
// in this array we will store floor spikes (the obstacles)
this.floorSpikes = [];
// in this array we will store the bitmap texts showing the scores at each jump
this.floorScores = [];
// creating a copy of levelColors array
var colorsArray = gameOptions.levelColors.slice();
// bakckround group
this.bgGroup = game.add.group();
// creation of a group where we will place all floors
this.groundGroup = game.add.group();
// creation of a group where we will place all spikes, or obstacles, or whatever you will name them
this.spikeGroup = game.add.group();
// creation of a group where we will place all bitmap texts showing the scores
this.scoreGroup = game.add.group();
// we start on the first floor
this.levelFloor = 0;
// adding the hero
this.theSquare = game.add.sprite(0, 0, "tile");
// setting hero registration point
this.theSquare.anchor.set(0.5);
// setting hero width and height
this.theSquare.width = gameOptions.squareSize;
this.theSquare.height = gameOptions.squareSize;
// chen the hero jump?
this.theSquare.canJump = true;
// enabling ARCADE physics on the hero
game.physics.enable(this.theSquare, Phaser.Physics.ARCADE);
// setting hero horizontal velocity
this.theSquare.body.velocity.x = gameOptions.squareSpeed;
// gravity applied to the square
this.theSquare.body.gravity.y = gameOptions.squareGravity;
// a custom attribute to tell the player which color we are going to use at each floor
this.theSquare.squareColor = [];
// determining level height. Each "level" is a floor, but I am calling the variable levelHeight rather than
// floorHeight not to be confused with gameOptions.floorHeight
var levelHeight = game.height / gameOptions.floorsAmount;
// time to create the floors
for(var i = 0; i < gameOptions.floorY.length; i++){
// each floorSpikes item is an array
this.floorSpikes[i] = [];
// each floorScores item is an array
this.floorScores[i] = [];
// creation of the background of each floor
var bg = game.add.sprite(0, gameOptions.floorY[i] - levelHeight + gameOptions.floorHeight, "tile");
// assigning background a width and a height
bg.width = game.width;
bg.height = levelHeight;
// tintColor is a random colorsArray item. Then such item is removed from colorsArray so that
// each floor will have a unique color
var tintColor = Phaser.ArrayUtils.removeRandomItem(colorsArray)
// choosing and removing a random tint color
this.floorColors.push(tintColor);
// applying tint color to background
bg.tint = tintColor;
// setting background semi-transparent to make it look darker thanks to black canvas color
bg.alpha = 0.5;
// adding the background to its proper group
this.bgGroup.add(bg);
// creation of the floor pavement
var floor = game.add.sprite(0, gameOptions.floorY[i], "tile");
// assigning floor pavement a width and a height
floor.width = game.width
floor.height = gameOptions.floorHeight;
// applying tint color to floor
floor.tint = tintColor;
// setting floor semi-transparent to make it look darker thanks to black canvas color
floor.alpha = 0.5;
// let's enable ARCADE physics on floors too
game.physics.enable(floor, Phaser.Physics.ARCADE);
// floors can't move
floor.body.immovable = true;
// adding the floor to ground group
this.groundGroup.add(floor);
// this method will place the spikes
this.placeSpikes(i)
}
// placing a particle emitter at coordinates 0, 0 (we'll place it in its proper position later) which can fire up to 30 particles
this.emitter = game.add.emitter(0, 0, 30);
// our particle is the same old "tile" image
this.emitter.makeParticles("tile");
// setting a gravity for each particle
this.emitter.gravity = 200;
// particles would be too big, so let's set their min and max scale.
this.emitter.maxParticleScale = 0.1;
this.emitter.minParticleScale = 0.05;
// finally placing the hero
this.placeSquare();
// creation of a new group which will contain all demo elements
this.demoGroup = game.add.group();
// we start with an overlay covering the entire game area
var blackOverlay = game.add.sprite(0, 0, "tile");
blackOverlay.width = game.width;
blackOverlay.height = game.height;
// tinting the overlay with black
blackOverlay.tint = 0x000000;
// setting the overlay 70% opaque
blackOverlay.alpha = 0.7;
// adding blackOverlay to demoGroup group
this.demoGroup.add(blackOverlay);
// adding a bitmap text with game title
var titleText = game.add.bitmapText(game.width / 2, game.height / 5, "font", "Just Jump", 48);
// setting titleText anchor point to 0.5 (the centre)
titleText.anchor.set(0.5);
// adding titleText to demoGroup group
this.demoGroup.add(titleText);
// same thing goes with infoText
var infoText = game.add.bitmapText(game.width / 2, game.height / 5 * 2, "font", "Tap / Click to jump", 24);
infoText.anchor.set(0.5, 0.5);
this.demoGroup.add(infoText);
// if you still haven't played the game, set score variable to zero
if(!this.score){
this.score = 0;
}
// now same concept we saw before now applies with scoresText, we are printing both the latest score and the top score
var scoresText = game.add.bitmapText(game.width / 2, game.height / 5 * 4, "font", "Latest score\n" + this.score.toString() + "\n\nBest score\n" + this.savedData.score.toString(), 24);
scoresText.anchor.set(0.5, 0.5);
scoresText.align = "center";
this.demoGroup.add(scoresText);
// last but not least, let's add version text
var versionText = game.add.bitmapText(game.width, game.height, "font", "v" + gameOptions.version, 24);
versionText.anchor.set(1, 1);
this.demoGroup.add(versionText);
// waiting for player input, then call squareJump function
game.input.onDown.add(this.squareJump, this);
},
// function to be executed at each frame
update: function(){
// is the game over?
if(!this.gameOver){
// making the hero collide with floors so it won't fallo down
game.physics.arcade.collide(this.theSquare, this.groundGroup);
// checking if the hero overlaps with anything in spike group
game.physics.arcade.overlap(this.theSquare, this.spikeGroup, function(){
// placing the emitter over the player
this.emitter.x = this.theSquare.x;
this.emitter.y = this.theSquare.y;
// firing 10 particles at once with a 1000 milliseconds lifespan
this.emitter.start(true, 1000, null, 10);
// tinting particles with the same player color
this.emitter.forEach(function(particle) {
particle.tint = this.theSquare.tint;
}, this);
// do not wait any longer for input
game.input.onDown.remove(this.squareJump, this);
// play explosion sound
this.explosionSound.play();
// game over, man!!
this.gameOver = true;
// updating localstorage setting score as the max value between current score and saved score
localStorage.setItem(gameOptions.localStorageName,JSON.stringify({
score: Math.max(this.score, this.savedData.score)
}));
// wait two seconds before restarting the game
game.time.events.add(Phaser.Timer.SECOND * 2, function(){
game.state.start("TheGame");
}, this);
}, null, this);
// if the hero leaves the floor to the right or to the left...
if((this.theSquare.x > game.width && this.levelFloor % 2 == 0) || (this.theSquare.x < 0 && this.levelFloor % 2 == 1)){
// calling moveSpikes method to update spikes position and width
this.moveSpikes(this.levelFloor);
// increasing floor number or setting it back to zero
this.levelFloor = (this.levelFloor + 1) % gameOptions.floorY.length;
// placing player at the beginning of the floor
this.placeSquare();
}
// we'll enter this block if the hero just landed, that is it's touching its bottom side and it can't jump yet
if(!this.theSquare.canJump && this.theSquare.body.touching.down){
// now the hero can jump again
this.theSquare.canJump = true;
// this statement checks if we jumped a spike:
// * we jumped more than zero times
// * we jumped less than or equal to gameOptions.spikesAmount
// * it's not a demo
if(this.jumps > 0 && this.jumps <= gameOptions.spikesAmount && !this.demo){
// now jumpLen takes the value of the square when it was in the middle of the jump
this.jumpLen = this.jumpLen + (this.theSquare.x - this.jumpLen) / 2;
// precision is the horizontal distance from the square in the middle of the jump and spike position
// the smaller its value, the more perfect the jump
var precision = Math.round(Math.abs(this.jumpLen - this.floorSpikes[this.levelFloor][this.jumps - 1].x));
// showing the bitmap text above the spike we just jumped
this.floorScores[this.levelFloor][this.jumps - 1].visible = true;
// if precision is less than 10 (a good jump)
if(precision < 10){
// increase the score
this.score += (10 - precision) * 10;
// show jump score
this.floorScores[this.levelFloor][this.jumps - 1].text = (10 - precision) * 10;
}
// otherwise tell the player it was a bad jump
else{
this.floorScores[this.levelFloor][this.jumps - 1].text = "BAD";
}
}
}
// if this is the demo...
if(this.demo){
// if there are spikes ahead the hero and we are close enough to next spike to allow hero to jump it...
if((this.jumps < gameOptions.spikesAmount) && (Math.abs(this.floorSpikes[this.levelFloor][this.jumps].x - this.theSquare.x) < 88)){
// se the hero jump!!
this.squareJump();
}
}
}
},
// when the player starts running on a floor
placeSquare: function(){
// at the beginning of the floor, the player jumped zero times
this.jumps = 0;
// properly tint the square according to floor number
this.theSquare.tint = this.floorColors[this.levelFloor];
// adjusting hero speed according to floor number: from left to right on even floors, from right to left on odd floors
this.theSquare.body.velocity.x = (this.levelFloor % 2 == 0) ? gameOptions.squareSpeed : -gameOptions.squareSpeed;
// no vertical velocity
this.theSquare.body.velocity.y = 0;
// the hero can jump again
this.theSquare.canjump = true;
// adjusting hero vertical and horizontal position
this.theSquare.y = gameOptions.floorY[this.levelFloor] - gameOptions.squareSize / 2;
this.theSquare.x = (this.levelFloor % 2 == 0) ? 0 : game.width;
// stopping the jump tween if running
if(this.jumpTween && this.jumpTween.isRunning){
this.jumpTween.stop();
this.theSquare.angle = 0;
}
},
placeSpikes: function(floor){
// time to place the spikes
// let's loop through this level spikes
for(var i = 1; i <= gameOptions.spikesAmount; i++){
// creating the spike as a tileSprite
var spike = game.add.sprite((floor % 2 == 0) ? game.width : 0, gameOptions.floorY[floor], "tile");
spike.height = gameOptions.spikeHeight;
// applying spikes the same tint color used for the ground
spike.tint = this.floorColors[floor];
// setting spike semi-transparent to make it look darker thanks to black canvas color
spike.alpha = 0.5;
// setting spike anchor point
spike.anchor.set(0.5, 1);
// enabling ARCADE physics to the spike
game.physics.enable(spike, Phaser.Physics.ARCADE);
// spikes can't move
spike.body.immovable = true;
// adding the spike to spike group
this.spikeGroup.add(spike);
// adding the spike to floorSpikes array
this.floorSpikes[floor].push(spike);
// adding a bitmap text above each spike
var scoreText = game.add.bitmapText(0, gameOptions.floorY[floor] - 60, "font", "100", 24);
// set text registration point
scoreText.anchor.set(0.5);
// set text aligment
scoreText.align = "center";
// do not show the text at the moment
scoreText.visible = false;
// adding the score bitmap text to scoreGroup group
this.scoreGroup.add(scoreText);
// adding the score to floorScores array
this.floorScores[floor].push(scoreText);
// now let's move the spikes of this floor
this.moveSpikes(floor);
}
},
moveSpikes: function(floor){
// the first obstacle will be placed at 180 if we are on aneven floor, game.width - 180 if we are on an odd floor
var obstacleX = (floor % 2 == 0) ? 180 : game.width - 180;
// looping through all obstacles in this floor
for(var i = 0; i < this.floorSpikes[floor].length; i++){
// assigning a new width to the obstacle
var newWidth = game.rnd.integerInRange(1, 16) * 2;
// adjusting spike physical body size
this.floorSpikes[floor][i].body.setSize(newWidth, this.floorSpikes[floor][i].height);
// re-positioning score text accordingly
this.floorScores[floor][i].x = obstacleX;
this.floorScores[floor][i].visible = false;
// placing next obstacle between 150 and 200 pixels
var obstacleGap = game.rnd.integerInRange(150, 200);
// moving and resizing the obstacle with a tween
var obstacleTween = game.add.tween(this.floorSpikes[floor][i]).to({
x: obstacleX,
width: newWidth
}, 250, Phaser.Easing.Linear.None, true)
// determining next obstacle position
obstacleX += (floor % 2 == 0) ? obstacleGap : - obstacleGap;
}
},
// when the player jumps
squareJump: function(e){
// we want e not to be undefined and demo to be true to say the player touched the screen
// or clicked to mouse to start playinh
if(e != undefined && this.demo){
// not a demo anymore
this.demo = false;
// destroying demoGroup and its content, removing titles, overlay, and everything not
// strictly related to the game
this.demoGroup.destroy();
// starting from first floor
this.levelFloor = 0;
// resetting the score
this.score = 0;
// placing the square
this.placeSquare();
// no more else to do
return;
}
// if the hero can jump...
if(this.theSquare.canJump){
// we save the position the square started jumping
this.jumpLen = this.theSquare.x;
// increasing the number of jumps
this.jumps ++;
// preventing it to jump while in the air
this.theSquare.canJump = false;
// adding a vertical force to the player
this.theSquare.body.velocity.y = gameOptions.jumpForce;
// setting a jump rotation angle just to make the square rotate
var jumpAngle = this.levelFloor % 2 == 0 ? 180 : -180;
// using a tween to rotate the player
this.jumpTween = game.add.tween(this.theSquare).to({
angle: this.theSquare.angle + jumpAngle
}, gameOptions.jumpTime, Phaser.Easing.Linear.None, true);
// if this is not a demo...
if(!this.demo){
// playing jump sound
this.jumpSound.play();
}
}
}
}
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.