I am trying to optimize my HTML5 game engine and have noticed that currently a majority of the time taken in the game loop can be taken when drawing transformed tiles. I think this is because each transformed tile saves the state of the canvas, applies a transform, draws the tile, then restores the state of the canvas. Fortunately I have (at least) 2 classes of tiles, and if I am only drawing un-transformed tiles, the game runs quite smoothly. Take a look:

Slow with transformed tiles in the background: http://sgdk2.enigmadream.com/ben/JavaSampleo.html Faster with no transformed tiles in the background: http://sgdk2.enigmadream.com/ben/JavaSample.html

Sure you could say that drawing the extra tiles alone is responsible, but I have used Chrome profiling to confirm that XFrame (transformed frame) is consuming more than 50% of the time. So here's the drawing code for XFrame:

XFrame.prototype.draw = function(ctx, x, y) {
   ctx.transform(this.m11, this.m12, this.m21, this.m22, this.dx+x, this.dy+y);
   ctx.drawImage(this.imageSource, (this.cellIndex % this.graphicSheet.columns) * this.graphicSheet.cellWidth,
      Math.floor(this.cellIndex / this.graphicSheet.columns) * this.graphicSheet.cellHeight,
      this.graphicSheet.cellWidth, this.graphicSheet.cellHeight, 0, 0, this.graphicSheet.cellWidth, this.graphicSheet.cellHeight);

Now one thing I can imagine improving this is if I could determine that many of the tiles I was drawing were using the same transformation, and make the single transform span the drawing of many tiles. It would be a challenge given my current design, but I might be up to it if it would make a difference. However that is of little use because the position of the tile has to become part of the transformation if I want the location of the tile to be excluded from the transform operation, I think. For example, if I want to draw a tile rotated 90 degrees at a coordinate of 64,32, I can't simply apply the rotation matrix and then draw at those coordinates because 64,32 will get transformed to something like 32, -64 (right). So I have to apply the offset as part of the transform for all transformed drawing operations. And since each tile is drawn at a different location, that means a different transform for each tile.

Any suggestions for optimizing tile drawing operations where transformations may be applied at the tile level in real time?


You're answering your own question. If the API wraps up the transform ops, then obviously it is up to you to minimise those ops in the way you've described; you can't go beyond the API layer for optimisation. And that's really all you can do other than making sure of the usual details like minimising conditionals and other costly logic in your innermost draw loop. When I wrote a top-down tile scroller using canvas last year, I batched my ops this way, after initially setting the transform matrix and then applying the matrix to each individual tile, and of course it improved performance an order of magnitude. Doing every tile individually was only done while I was learning the API and setting up the code structure, then I refactored.

The only other thing I can suggest is to consider the CSS3/DOM route instead, and measure its performance against canvas on a browser-by-browser basis.

EDIT In response to your comment. Two things:

  • In my code, there is a very good reason why I had no problem drawing tiles: for the tile drawing, at least, I was using fillRect(x, y, width, height) such that I could simply use the fillRect params to specify the offset for each solid-colour tile, completely separately from doing actual "by-camera" transformations, which are applied to the canvas after you've drawn the tiles this way. In my case in fact, I also applied scaling multiplications to those 4 params directly, to ensure my camera could implement zoom. TBH, it was a little complicated due to the very limitations your question addresses.

  • However on that same note, you should be able to use drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) to draw to the desired offsets, such that no per-tile translate() calls are necessary. Then you can use bitmaps for your tiles, instead of flat colour as I did.

So again, only use translate, rotate and/or scale for the actual camera view as a whole, once you've already rendered all your individual tiles in at their various offsets using drawImage() or fillRect() to do the initial population, cheaply. Beyond that, there is really nothing more you can do to keep rendering costs down, other than simply skipping the explicit transforms altogether (can't recall if this is viable or not). HTH.

  • \$\begingroup\$ I'm not trying to go beyond the API layer for optimization, but find out what, if anything, I've missed in the API. For example, I said I can't batch the operations if I want to draw the tiles at different positions, but you said you did manage to batch them. How? My understanding is that the need to draw each tile at a different position requires a different transform for each tile (as I stated). So I see no possibility of batching. What am I missing? \$\endgroup\$
    – BlueMonkMN
    Jan 11 '12 at 14:58
  • \$\begingroup\$ @BlueMonkMN Yep. See my edit. \$\endgroup\$
    – Engineer
    Jan 11 '12 at 18:27
  • \$\begingroup\$ OK, I have two classes: XFrame and Frame. XFrame draws tiles that "need" real-time rotation or scaling and Frame is a simple drawImage with no save, restore or transform. Do I understand correctly that my Frame class basically does what you're suggesting? If that's the case, then this suggestion doesn't provide any way to further optimize XFrame. My only remaining thoughts are 1) pre-render XFrame images to another intermediate canvas and use that instead of doing real-time transforms; 2) Document the limitation: avoid lots of real-time transforms or accept the performance impact. \$\endgroup\$
    – BlueMonkMN
    Jan 11 '12 at 19:17
  • \$\begingroup\$ @BlueMonkMN Given the above, pre-rendering your backgrounds is the obvious outcome from an optimisation viewpoint: Construct/draw (fillRect/drawImage) the tile based background for a whole level or zone therof, once, and reuse each frame by transforming and/or blitting. If your level changes, only re-blit a small part of the base background (the pre-transform canvas). If you don't need camera rotation, I wouldn't bother with transforms; you could do everything you need just using drawImage and fillRect, figuring out the offset arithmetic yourself. That should even include scaling, IIRC. \$\endgroup\$
    – Engineer
    Jan 11 '12 at 19:56
  • 1
    \$\begingroup\$ It has always seemed to me that accelerated 2D APIs are missing something in this area. I don't understand why they are so geared toward transforming whole batches of drawing operations and have such limited support for applying real time transforms to individual drawing operations. Even the ability to apply the same transform, but translate the result differently (in a more optimal way) would be a big step forward for tile-based games. I guess I'm stuck with what I've got for now. \$\endgroup\$
    – BlueMonkMN
    Jan 11 '12 at 20:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .