Optimizing Javascript Games

Share on FacebookShare on Google+Tweet about this on TwitterShare on Reddit

I’m currently working on a game using the Javascript game engine Phaser.  It’s going to be set in a city, so as part of the development process, I’m going to make a working city simulator and build the game on top of that.  The last month has been spent generating landscapes, roads, buildings, and more.  Once I had all of the items I wanted to generate created, I joined them together and found that it looked beautiful, but ran terribly.  I had gotten to the place that every developer gets to at some point in their project: optimization.

As I started racking my brain thinking of ways to optimize, I decided to write down all the steps to show later.  I’m going to be giving a short talk at work about optimizing javascript games (Optimizing Javascript Games PDF), but I wanted to write a slightly larger companion blog post about it.

The Problem:

I’ve got a map, and a map consists of 16 chunks.  A chunk consists of 400 tiles, and each tile consists of 10 layers drawn on top of each other (on the z axis).  When I started, an average map was generating roughly ~23,000 sprites.

Now, sprites alone aren’t the enemy.  You can see in the pixi.js examples online (BunnyMark) that pixi.js (the rendering engine Phaser uses) is perfectly capable of rendering 10’s of thousands of sprites on screen at the same time.  They even bounce around!  How is it possible that the rendering engine handles all those bouncing bunnies, but my game chokes with the same amount of sprites?  Textures.  The bunny texture for the pixi.js demo is very small, and requires very little overhead to render.  My isometric tile textures are significantly larger, and the combination of sprites and large textures is deadly to performance.

before-anything-fps

The Goal:

We want our game to run at 60FPS.  We’d probably tolerate 30FPS, but anything lower than that isn’t acceptable.  There’s a very cool example online that shows what difference frame rates look like that might help visualize what I mean, and why we want 60FPS.

The Fixes:

  • Physics:

When I started, I looked at some of the Phaser Isometric examples for guidance on what to do.  In the example I looked at, all the sprites had physics applied to them.

All the sprites I’ve been working with so far have been static.  Adding physics to buildings and roads doesn’t make a whole lot of sense.  I disabled physics across the board and got an 8FPS boost without any affect on gameplay.

after-physics-fps

  • Occlusion Culling:

I mentioned earlier that each tile has 10 layers on it.  Layers start at 0 with things like grass/water/sand and other landscape features.  Layer 1 is reserved for city tiles like sidewalks, roads, and highways.  Layer 2 is where buildings begin.  Before optimizing, I would fill the entire map with landscape tiles, then fill the area I wanted to be a part of the city with city tiles, and then start placing buildings on top of that.  Since those three levels occupy the same z index, the player can only see the most recently drawn one even though the other two are under it.  Unfortunately, just because we can’t see the tiles anymore doesn’t mean that our game doesn’t have to render them.

Well this doesn’t make a whole lot of sense.  Why are we rendering hundreds if not thousands of tiles we can’t see?  This is where Occlusion Culling comes into play.  I wrote a small function that will check to see if tiles exist under the current tiles, and if they do, don’t draw them.

 

Implementing this code resulted in a ~32% reduction in total sprites drawn, and got us a 6FPS boost.  Like the previous fix, this didn’t really cost us anything other than a little bit of up front processing.

after-hsr-fps

  • Layer Removal:

After implementing occlusion culling and feeling pretty good about trimming some fat off the layers, I started to think about what other tiles I didn’t really need to be rendering.  When I first started prototyping this game, I started with a layer of dirt and added more layers and tiles on top of that.  When I thought critically about it though, I realized the dirt added next to nothing visually, and was probably a pretty big performance suck.  I decided to remove it and see what happened.

with-dirt

to

without-dirt

You can see that visually there’s not a whole lot of difference between dirt and no dirt, but how does it perform?

post-dirt-fps

Amazingly.  We got a 12FPS boost from removing all the dirt, and ended up drawing 43% fewer sprites than we were rendering previously.  I think this is an example of looking critically at features in your programs and making hard choices of what to cut and what not to.  Cutting dirt from my maps wasn’t that big of a decision, but it did affect the aesthetics of the game.  When optimizing, think “what value does this provide” vs “what does this cost me”.  When you frame the question that way, getting rid of dirt (or perhaps other unneeded features) becomes an easier choice.

 

  • Lazy Loading:

As I mentioned before, a map is made up of several chunks.  Each of those chunks is in it’s own rendering group for purposes of isolation and for anticipation of doing lazy loading of chunks later on.  The viewport for our game is 1024×768, and a single chunk will more than fill it.  Depending on where in the chunk you’re at, you may see parts of multiple chunks.  No matter where you are though, you won’t ever see the whole map.

lazy-loading

You can see in the above diagram that I’ve drawn a 4×4 chunk map.  The red square in the middle is the viewport (camera), and the yellow chunks are the ones that are (at least partially) in view.  The grey squares are chunks that are completely out of view.  So if a chunk is completely out of view, why do we bother rendering it?  Well we really shouldn’t!  Lazy loading is a concept that essentially means that we will load resources as we need them, and not a moment before.  As a chunk comes into view of the camera, it will become visible, and when a chunk leaves the view of the camera, it will become invisible.

This was the hardest optimization to implement because of a few reasons.  The world and the camera use different coordinate systems by default.  Luckily, both the camera and the world can understand the other’s coordinates (through properties like game.world.camera.view).  Unfortunately, when I draw a sprite on the screen using the Phaser Isometric Plugin, even if I draw it at [0,0], it doesn’t actually appear at [0,0].  It makes it easy on me so I don’t have to do the math to make my isometric tiles line up correctly.  Unfortunately, it makes it really difficult to compare the x/y coordinates of a group to the camera’s viewport.

I sort of hacked up a way to do this comparison, and it’s not pretty, but it does work.  There may be a better way to do it, but I’m still a beginner to Phaser, so who knows.

After I’ve found the appropriate coordinates for the group, I compare those coordinates to the camera viewport when the player moves the camera.

The result is a totally seamless scrolling through the map, and a nice boost to FPS (roughly 10-12).  The real benefit though is that it lets us scale our map size up.  If I did all those fixes I described above and decided I wanted to scale up my map to 5×5 or 10×10, I’d have to think outside the box and refactor again since those fixes don’t scale with map size.  This fix scales and will be a big help later on.

after-lazy-loading-fps

  • Libraries:

Lazy loading took me days and days, and after I got done, I looked and I had 60FPS and was ecstatic!  Unfortunately, I still had a good amount of camera judder and some weird visual artifacts.  I was looking online for a fix and realized I was a full 2 minor versions behind on Phaser.  I updated Phaser and Phaser Isometric Plugin, reran my game, and all my graphical issues were resolved and the camera judder was significantly improved.  Don’t ever underestimate the difference having up-to-date libraries can make on performance.

The Results:

I got to 60FPS, and got my camera movement pretty smooth, so I’m pretty happy with it now.  I think it’s important to optimize when you need to optimize and not prematurely do so.  I could continue to optimize at this point, but for what?  My game isn’t anywhere near feature complete, and optimizing now may cost me a lot of time and result in something I’ll completely scrap later on.

IsoCitySim

I hope you’ve enjoyed my blog post, found some value in it, and it helps you with your programming later.

7 comments

  1. thanks for posting this. it scrolls nice and smoothly in my browser.

    regarding your chunk visibility check though, (you are probably aware of this but I’ll mention it anyway for anybody else reading). there is no point checking chunks for intersection that you know aren’t going to be visible anyway (the grey chunks). I would flag the potentially visible chunks (the yellow ones) and only loop through those with the intersect check.

    In your engine (using the diagram above), it appears that the viewport can only ever cover 4 chunks. to determine which chunks these are you can take the middle points along the top, left, bottom and right of the viewport rectangle, and then should be able to calculate which chunk that point falls in based on your tile size etc

    this way instead of checking 16 intersections you’re only ever checking 4. this should scale up better.

    i’m guessing with a fully optimized engine, ideally you would only really want to be using as many sprites as covers the 4 visible chunks and then recycle them along the edges as the camera moves.. ie move them to a new position. (assuming you can swap out the texture on a sprite quickly to another one from the atlas).. this should then of course scale to “unlimited” size maps.. I’m assuming that’s what this does (but not iso) http://www.html5gamedevs.com/topic/9573-phaser-tiled-plugin/

    only theory at the moment sorry.

    regards
    J

Leave a Reply

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