Pixel Perfect scaling a Phaser game
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 32x32 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:
var game = new Phaser.Game(160, 144, Phaser.CANVAS, '', { init: init, preload: preload, create: create, render: render });
The important things to note here are:
- Use the un-scaled resolution
- Always use Phaser.CANVAS as the render method
- 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:
var pixel = { scale: 4, canvas: null, context: null, width: 0, height: 0 }
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:
function init() {
// Hide the un-scaled game canvas
game.canvas.style['display'] = 'none';
// Create our scaled canvas. It will be the size of the game * whatever scale value you've set
pixel.canvas = Phaser.Canvas.create(game.width * pixel.scale, game.height * pixel.scale);
// Store a reference to the Canvas Context
pixel.context = pixel.canvas.getContext('2d');
// Add the scaled canvas to the DOM
Phaser.Canvas.addToDOM(pixel.canvas);
// Disable smoothing on the scaled canvas
Phaser.Canvas.setSmoothingEnabled(pixel.context, false);
// Cache the width/height to avoid looking it up every render
pixel.width = pixel.canvas.width;
pixel.height = pixel.canvas.height;
}
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:
function preload() {
game.load.image('pic', 'kof97.png');
}
function create() {
game.add.image(0, 0, 'pic');
}
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:
function render() {
// Every loop we need to render the un-scaled game canvas to the displayed scaled canvas:
pixel.context.drawImage(game.canvas, 0, 0, game.width, game.height, 0, 0, pixel.width, pixel.height);
}
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:
Drawbacks
There are two drawbacks to this method:
- 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.
- 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!
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 32x32 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:
var game = new Phaser.Game(160, 144, Phaser.CANVAS, '', { init: init, preload: preload, create: create, render: render });
The important things to note here are:
- Use the un-scaled resolution
- Always use Phaser.CANVAS as the render method
- 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:
var pixel = { scale: 4, canvas: null, context: null, width: 0, height: 0 }
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:
function init() {
// Hide the un-scaled game canvas
game.canvas.style['display'] = 'none';
// Create our scaled canvas. It will be the size of the game * whatever scale value you've set
pixel.canvas = Phaser.Canvas.create(game.width * pixel.scale, game.height * pixel.scale);
// Store a reference to the Canvas Context
pixel.context = pixel.canvas.getContext('2d');
// Add the scaled canvas to the DOM
Phaser.Canvas.addToDOM(pixel.canvas);
// Disable smoothing on the scaled canvas
Phaser.Canvas.setSmoothingEnabled(pixel.context, false);
// Cache the width/height to avoid looking it up every render
pixel.width = pixel.canvas.width;
pixel.height = pixel.canvas.height;
}
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:
function preload() {
game.load.image('pic', 'kof97.png');
}
function create() {
game.add.image(0, 0, 'pic');
}
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:
function render() {
// Every loop we need to render the un-scaled game canvas to the displayed scaled canvas:
pixel.context.drawImage(game.canvas, 0, 0, game.width, game.height, 0, 0, pixel.width, pixel.height);
}
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:
Drawbacks
There are two drawbacks to this method:
- 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.
- 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!