Pixel Perfect scaling a Phaser game

gameboy25

With GBJam3 just started today I tend to get asked this a lot on twitter: “How do I scale my game and keep it crisp?

This is a perfectly valid question and is essential for games that rely on pixel art. And the answer is that there is no 100% fully cross-browser compatible solution. There are various CSS hacks and vendor prefixes you can try, but they won’t work on everything.

However, if that was my final answer there would be no point in this blog post, right? When we created our lowrez jam game, which was a game running at a 32×32 resolution, we came up with the following approach that works much more reliably than any CSS hack. Here’s how to get it working:

For this example I’ll assume you are making a GBJam game, so you’ve a restriction of 160 x 144 pixels. The same as the original Gameboy resolution. First create your Phaser game object:

The important things to note here are:

  1. Use the un-scaled resolution
  2. Always use Phaser.CANVAS as the render method
  3. Give an empty string as the DOM parent (the 4th parameter)

Once the game object is created we use a new object to hold our scaled canvas:

Here we’ve got a scale property set to 4, which will give us a x4 scaled game. The other properties are populated by the init function:

The comments in the above function should be self-explanatory, but essentially what it does is hide the un-scaled game canvas and create a brand new one at the size we require. It then keeps references to it in the pixel object.

For this simple example we just load and display a single image:

Of course this is where you’d set-up your game properly and start things off, but it does what we need to prove this works. Finally the last thing to do is create our render function. This is called every frame and is called after the game has rendered everything, think of it as a post-render hook. So it’s the perfect place to copy our un-scaled original canvas up to the scaled one:

Here we use the unscaled game as our source canvas and draw it at the scaled resolution to the displayed canvas. Because we disabled smoothing earlier it performs a clean non-aliased draw. The end result is your game properly scaled:

kof-scaled

Drawbacks

There are two drawbacks to this method:

  1. There is a potential performance issue here, as it has to render the whole game (to the hidden canvas) and then copy it again to the displayed canvas. In practise we never saw this create an actual issue, because you’re drawing so few pixels at the unscaled stage that it does it incredibly fast anyway, and the upscaling is just one single draw call. Even so, it’s something to consider.
  2. The biggest drawback is that you can’t use this method and also use mouse or touch input. The current version of Phaser works by checking for input on the canvas object itself, but as that is hidden nothing will be processed. And even if it wasn’t the scales would be wrong anyway. Keyboard and Gamepad input works flawlessly though. There are ways to get around the input issue, but Phaser won’t handle it natively (although it’s something we have added to the roadmap for a release later this year). Even so, we feel the benefits outweigh the cons here, but do take this point into consideration before using this method.

Source Code / Example

You can view the full code and see an example running.

So have fun and if you make a GBJam3 entry using Phaser, or just any game using Phaser, please don’t forget to tell us about it. Either on the Phaser forums or you can send me a tweet @photonstorm :) Oh and the awesome picture at the top of the post is from the OCRemix Gameboy 25 Album!

Posted on August 1st 2014 at 8:13 pm by .
View more posts in Phaser. Follow responses via the RSS 2.0 feed.


20 Responses

Leave a comment
  • August 2nd 2014 at 5:53 am

    Hi Rich, cool post as always.

    What about the following idea:

    Upscale all graphic assets after loading is finished and use their upscaled version during the game. This way you could get around the drawbacks?

    Best,
    benny!

  • August 2nd 2014 at 9:50 am

    You could yes but actually that causes quite a headache in your game code, because the game itself is now x4 and all your tweens etc will run off that size, so you’d have to keep remembering to multiply and round coordinates in steps of 4 etc. just a bit of a ball ache if you ask me :) easier to work at native resolution. Also the mouse event downside is really easy to fix, I just haven’t had time yet – but all it needs is the ability to change where the event listeners are created and it’d work fine!

  • cdrch
    August 3rd 2014 at 5:18 am

    I’m glad to have found this while searching on this topic for the GBJam3.

    Quick question: Would this still work with a game broken up across several files or states (similar to the basic game template in the Phaser resources)? And how would you modify it to do so? I’ve been trying to do that all day, but I can’t quite figure out how to get it to work.

  • August 3rd 2014 at 9:22 am

    Yes it would. All you’d do is the init stuff once in your Boot state and make the pixel object a property of your main game object. Then all you have to do is have the render code in every State.

  • cdrch
    August 3rd 2014 at 8:22 pm

    Thanks! Unfortunately, I can’t quite get it to work. It switches from displaying my game at the top-middle of my screen to displaying a black box at the top left (which I suppose means that the init is working). I suspect it has something to do with my render function, but I have no idea what it might be since the one line in render() is so simple. Think you could take a look at it?

    https://github.com/cdrch/gbjam3-game

  • August 4th 2014 at 12:16 am

    Make sure you’re not using any of the game scaling features, or game centering (horizontally or vertically). If you’re using one of the project templates it will have it enabled by default – remove it all.

  • cdrch
    August 4th 2014 at 12:43 am

    I went through my code and commented out everything related to scaling and centering (including all anchor.setTo calls), but there’s no change in the appearance. Would using tilemaps affect this at all? I can’t think of what else might be causing this, as the game works fine without the init and render calls.

  • August 4th 2014 at 12:49 am

    I didn’t mean scaling / anchors of Sprites, I meant of the game itself. Calls like this: this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL or this.scale.pageAlignHorizontally = true which are in the Boot.js by default in the templates. It’s that sort of stuff that would need to be removed. If that doesn’t resolve it then you’ve got some CSS in your page causing it I’m afraid. Upload it somewhere and post the URL so we can check.

  • cdrch
    August 4th 2014 at 12:59 am

    Oh, okay. I had done both anyways. I just double-checked boot.js and I removed all of the CSS. The game is currently on Github (https://github.com/cdrch/gbjam3-game) and hosted here: https://gbjam3-game-c9-cdrch.c9.io/index.html. Since I’m using Cloud9 as my IDE, I can allow you to look directly at that if you have an account, but just looking at the link above should let you see the game.

  • August 4th 2014 at 11:11 am

    You need to remove the gameContainer div from the html and remove it from the Phaser.Game constructor too (just set it to an empty string)

  • cdrch
    August 4th 2014 at 9:15 pm

    Apparently this code does not like me. :)

    I changed the constructor’s string to empty, and I tested it without a named div and without a div at all. Identical results to before. I also tried it set up to just display a line of text, no tilemaps or anything else, and I also tried to just have it display an image at the size of the screen, identical to the example in this article – neither worked. I also, just to make sure it wasn’t Cloud9 messing something up, set up a seperate JS file with everything copied above into it and no states, which worked.

    I’m lost as to what might be causing it, but what else is different between the single file, no states setup and the multiple file, multiple states setup that could affect this?

    Thanks for all the help you’ve given.

  • August 4th 2014 at 10:07 pm

    Here – this is a complete zip file of a State swapping Phaser game that uses pixel scaling :)

    https://dl.dropboxusercontent.com/u/182996/phaser-pixel-states.zip

  • August 4th 2014 at 11:21 pm

    Wow, thanks! 😀

    When the jam is over (and the jam my college is hosting immediately after that is over…), I’m going to go through my old code and try to figure out what was causing this issue – even after I made sure mine was identical to yours, it still didn’t work. :/ Oh well. For now, I’ll be copying over code onto that template very, very, very carefully.

    Thanks again for all the help, and for Phaser!

  • Gan_HOPE326
    August 10th 2014 at 9:03 am

    Thanks for this tutorial – very useful!

    I used it to make a Phaser.js game for the GBJam, so I figured you might like to get the link!

    http://gamejolt.com/dashboard/developer/games/view/31214/

    (By the way, this is my second game with Phaser. I had just finished it when I realized how moronic I had been in building my own state system by discovering that Phaser, of course, has a built in one… next project I’ll keep it in mind!)

  • Elliot Winkler
    August 30th 2014 at 8:04 pm

    This is an interesting solution, but isn’t it sort of hacky? Does Phaser support overriding how the canvas is constructed before it gets added to the DOM?

  • September 15th 2014 at 11:21 am

    “Hacky” is the only way to reliably do pixel scaling atm. Unless you don’t care about older browsers, then there are several options.

  • December 10th 2014 at 6:46 pm

    Hmm… can you make something in phaser similar to FlashPunk’s scale?

  • December 13th 2014 at 5:41 pm

    Hey Rich,

    Finally went and implemented this solution in my own game and works great. It seems you can just modify the styling on the canvas for Firefox but you have to go with this solution to get Chrome (and presumably webkit?) to render sprites crisply/properly.

    That being said, you mentioned there was an easy way to solve the issue of loss of click events in the code. Could you possibly expand upon what that solution is? My game is going to be pretty heavy on click interaction (a simulation/strategy game) so I’ll need to get that working at some point. I wouldn’t mind getting dirty with the code and possibly opening a pull request if that helps get this feature working 100% faster.

  • Jack Guy
    May 6th 2015 at 9:10 pm

    Following up on Stefan’s question- was the fix for mouse input ever resolved?

  • Tukde
    June 10th 2015 at 3:40 am

    Bitchin’, thanks.

Make yourself heard