Talking about Endless Runner game, Game development, HTML5, Javascript and Phaser.
When you work with HTML5 mobile games, one of the biggest headaches comes when the game runs well on YOUR phone, and you realize it won’t look as good when loaded on any other device.
On the web there are lots of tips and suggestions about scaling your HTML5 games to make them look good on any device, but I am afraid they all are too much generic.
The secret about scaling a game is: there isn’t a best way to scale an HTML5 game. You have to scale each game in a different way according to game genre.
This quick tip covers endless runner games, mostly built to be played on landscape orientation.
Since all these games have a fixed height and a (fake) endless width, we should first calculate screen width/height ratio, then adjusting game width according to such ratio while keeping the original height.
This way, wider screens will see a longer part of the endless width, but all kind of screens will be perfectly filled by the game.
Looks at these three examples of the same game, taken from the Spring Ninja tutorial (you’ll probably need to click on each example to make it start):
You can also check it on your mobile device on this link.
Although the background of the body is red, as you can see it’s perfectly filled by the game. Obviously, the wider the screen, the larger the land you will see.
Here it is the source code, with highlighted lines to let you see what changed from the original code:
window.onload = function() { var innerWidth = window.innerWidth; var innerHeight = window.innerHeight; var gameRatio = innerWidth/innerHeight; var game = new Phaser.Game(Math.ceil(480*gameRatio), 480, Phaser.CANVAS); var ninja; var ninjaGravity = 800; var ninjaJumpPower; var score=0; var scoreText; var topScore; var powerBar; var powerTween; var placedPoles; var poleGroup; var minPoleGap = 100; var maxPoleGap = 300; var ninjaJumping; var ninjaFallingDown; var play = function(game){} play.prototype = { preload:function(){ game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; game.scale.setScreenSize(true); game.load.image("ninja", "ninja.png"); game.load.image("pole", "pole.png"); game.load.image("powerbar", "powerbar.png"); }, create:function(){ ninjaJumping = false; ninjaFallingDown = false; score = 0; placedPoles = 0; poleGroup = game.add.group(); topScore = localStorage.getItem("topFlappyScore")==null?0:localStorage.getItem("topFlappyScore"); scoreText = game.add.text(10,10,"-",{ font:"bold 16px Arial" }); updateScore(); game.stage.backgroundColor = "#87CEEB"; game.physics.startSystem(Phaser.Physics.ARCADE); ninja = game.add.sprite(80,0,"ninja"); ninja.anchor.set(0.5); ninja.lastPole = 1; game.physics.arcade.enable(ninja); ninja.body.gravity.y = ninjaGravity; game.input.onDown.add(prepareToJump, this); addPole(80); }, update:function(){ game.physics.arcade.collide(ninja, poleGroup, checkLanding); if(ninja.y>game.height){ die(); } } } game.state.add("Play",play); game.state.start("Play"); function updateScore(){ scoreText.text = "Score: "+score+"\nBest: "+topScore; } function prepareToJump(){ if(ninja.body.velocity.y==0){ powerBar = game.add.sprite(ninja.x,ninja.y-50,"powerbar"); powerBar.width = 0; powerTween = game.add.tween(powerBar).to({ width:100 }, 1000, "Linear",true); game.input.onDown.remove(prepareToJump, this); game.input.onUp.add(jump, this); } } function jump(){ ninjaJumpPower= -powerBar.width*3-100 powerBar.destroy(); game.tweens.removeAll(); ninja.body.velocity.y = ninjaJumpPower*2; ninjaJumping = true; powerTween.stop(); game.input.onUp.remove(jump, this); } function addNewPoles(){ var maxPoleX = 0; poleGroup.forEach(function(item) { maxPoleX = Math.max(item.x,maxPoleX) }); var nextPolePosition = maxPoleX + game.rnd.between(minPoleGap,maxPoleGap); addPole(nextPolePosition); } function addPole(poleX){ if(poleX<game.width*2){ placedPoles++; var pole = new Pole(game,poleX,game.rnd.between(250,380)); game.add.existing(pole); pole.anchor.set(0.5,0); poleGroup.add(pole); var nextPolePosition = poleX + game.rnd.between(minPoleGap,maxPoleGap); addPole(nextPolePosition); } } function die(){ localStorage.setItem("topFlappyScore",Math.max(score,topScore)); game.state.start("Play"); } function checkLanding(n,p){ if(p.y>=n.y+n.height/2){ var border = n.x-p.x if(Math.abs(border)>20){ n.body.velocity.x=border*2; n.body.velocity.y=-200; } var poleDiff = p.poleNumber-n.lastPole; if(poleDiff>0){ score+= Math.pow(2,poleDiff); updateScore(); n.lastPole= p.poleNumber; } if(ninjaJumping){ ninjaJumping = false; game.input.onDown.add(prepareToJump, this); } } else{ ninjaFallingDown = true; poleGroup.forEach(function(item) { item.body.velocity.x = 0; }); } } Pole = function (game, x, y) { Phaser.Sprite.call(this, game, x, y, "pole"); game.physics.enable(this, Phaser.Physics.ARCADE); this.body.immovable = true; this.poleNumber = placedPoles; }; Pole.prototype = Object.create(Phaser.Sprite.prototype); Pole.prototype.constructor = Pole; Pole.prototype.update = function() { if(ninjaJumping && !ninjaFallingDown){ this.body.velocity.x = ninjaJumpPower; } else{ this.body.velocity.x = 0 } if(this.x<-this.width){ this.destroy(); addNewPoles(); } } }
And that’s it, during next days I will publish other kinds of endless runner games to let you see how well this method works. Meanwhile download the source code, it runs with the new Phaser 2.3 release candidate.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.