Optimizing HTML5 Canvas to Improve your Game Performance

So I coded a game this month for the github game compo called “Pappu Pakia“. This is my first HTML5 Game with beautiful graphics by Kushagra. When I was finished with the coding part, it was pretty exciting until I played it a few times when my CPU was busy, then on an old laptop and finally on a mobile device. In all the cases it was laggy, except in the last one where it was super slow. So what do we do now ? Time to Optimize!

The Start

I started off by Googling and ended up reading like 10-15 articles. Eventually I found that all of them in a way repeated the points mentioned in this HTML5Rocks article. So maybe you can save time by just reading this and none other. I also approached some people who do HTML5 Game coding, and whatever they suggested was kindof a repetition of that article. None of those tips actually applied to my context.

Profiling

Moving on to analyzing by profiling my code. Fired up the devtools, hit the Profile tab, click the Record button and viewed the report. Now I’ll share what I did to optimize and improve performance of my game. Just note that these are quite specific to my game, but can be applicable to many other contexts! Although I’ll try to keep any following code pretty much generic.

Problems and Solutions

Drawing Layered Backgrounds

This was the biggest issue with performance. I had several layers of backgrounds for clouds, back trees, front trees, ground, grass and digged holes for forks. Each moved at different velocities to produce a neat parallax effect. Obviously they were drawn using drawImage. Each layer has 2 calls to drawImage to create a seamless effect. Hopefully you can imagine how harsh these reflows and repaints are, on the browser performance.

The first thing I did was to simply combined the 3 layers (clouds, back trees, front trees) into 1 and used that. But now, what about the parallax effect ? Well, I modified my code in a way so that devices with less than 50 FPS wouldn’t enjoy the parallax effect anymore, while systems with more than that would still experience it.

if (FPS > 50) {
  drawLayer1();
  drawLayer2();
  drawLayer3();
}
else {
  drawCombineLayer();
}

A Major performance gainer. Good part is, people with faster and free (lighter load on CPU/GPU) environment (leading to more FPS) can still enjoy the parallax. Cool, ain’t it ? I also combined the images for grass and grounds.

Another micro-optimization I did was to make use of all the 9 arguments of drawImage. Previously I was simply just drawing the layers twice, but now I picked the source of both the drawings that would be visible in the canvas and drew them accordingly with appropriate destination x/y positions and width/height.

Drawing Few Items

Forks, Branches, Pakias and Collectibles are all pre-generated and stored in an array. They’re then drawn when required. One might skip this, but draw as few as possible. If the x/y position of any object is off the canvas edges then just return; from the drawing procedure instead of actually drawing it.

Detecting Collisions with Few Items

If there are 6 forks in the pre-generated array, then it is NOT required to check the character’s collision with all of them (in our case). We can just check collision with the “first” fork in every frame. Since the game character has NO velocity in the y coordinate, it can only hit 1 fork at a time. Same with branches, collectibles, etc.

Using Sprites over Canvas Operations

When drawing simple objects, canvas operations, i.e., your plain good Javascript code is fine. But when things start getting so complex that you have to employ methods like rotate, scale, save, restore, createLinearGradient or properties like shadowBlur, etc. on the 2D rendering context several times, then you should consider using images. A quick drawImage would be way more faster.

Initially I drew the collectibles with a real messy code that was bad on performance. Later I simply switched to sprites for performance.

Although the character (Pappu) itself still uses rotate with save and restore for proper realistic flying effects. Unfortunately, there’s no alternative to this particular usecase.

drawImage at Integer Coordinates

Using whole numbers over floating points as arguments to various methods will cause your images or sprites to blur as the browser will actually try to draw into those subpixels. It actually happened with all the forks. Funny ?

So I converted all the arguments that were being passed to drawImage and translate, into integers.

Some Extra Possible Optimizations

Let me list few extra optimizations I could have done but didn’t get the time:

  • Just like many others, on every reflow (frame rendering), I am clearing the entire canvas. I could have just cleared the parts where forks, collectibles, branches, game character, enemies, etc. are visible and not everything. Will be cheaper at the cost of more code.
  • Instead of using Canvas Linear Gradients for the sky, it would be better to drawImage separately or add it to the image that holds clouds and trees.
  • Managing all game mechanics/physics like velocities, accelerations, gravity, etc. based on time rather than frames would lead to a more smoother and reliable game. Then limiting the frame to something low might also help in providing a consistent experience on computers and mobiles.

Conclusion

Let’s hope some of you find these tips useful. Consider reading the html5 rocks article and enjoy the game. Source code for the game is available on Github.

Share:

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>