I thought I’d follow-up my original post about Doppelgänger, and take some time to detail its implementation from a rendering perspective. There’s a lot that can be done to ensure your AIR for iOS apps hit their target frame rate, but these optimizations aren’t always obvious. I’ll also mention a few common pitfalls that leave many Flash developers scratching their head.
I hope you find the following useful for your own projects.
Test on the lowest common denominator
Test frequently on a physical device and use the lowest common denominator. For example, if you’re targeting both iPad’s then get your hands on an iPad 1. Don’t exclusively test on the iPad 2 and assume everything will be fine and dandy for those using previous generation hardware. You’ll be asking for trouble if you do.
Profile your app using Instruments
I know Flash Professional and ADT allow you to develop iOS apps using a PC, but if you’re at all serious then you really should get yourself a Mac. Why? Well you’ll be able to install the Instruments application, which allows you to analyse the performance of your app as it runs on a device.
Instruments was invaluable during the development of Doppelgänger. It allowed me to examine the app’s memory consumption, CPU usage and frame rate, making it very easy to spot areas in my code that were causing CPU spikes very early in the development process. My personal goal was to keep CPU usage at around 20%. It was easy to spot any spikes above this and quickly address these by optimizing either my code or graphics.
Don’t ignore spikes in CPU usage. Endeavour to fix all bottlenecks as early as possible.
Focus on rendering bottlenecks from the outset
Graphics rendering is often the single biggest bottleneck when porting Flash apps to iOS. Be prepared to flatten your display lists, simplify complex vector shapes, reduce the number of layers, and use bitmaps where appropriate. Also, perform incremental tests of your graphics assets and use Instruments to measure performance.
The original FLA for Doppelgänger was quite bloated. There was an insane amount of vector detail in many of the assets and an unnecessarily deep display list. I converted much of what was there to bitmaps to maximize render performance. I also tested many of the graphics assets in isolation by simply publishing individual test apps and profiling with Instruments. Once happy with the performance I’d layer more animations together and test again. This allowed me to quickly find any graphical assets that were going to hurt performance.
AIR for iOS provides two choices of rendering engine – CPU mode and GPU mode. The CPU renderer is the same software renderer used by the desktop Flash Player. However, on iOS devices we can instruct Flash to offload much of this work to the GPU, which when used correctly, can accelerate rendering. Doppelgänger took advantage of GPU rendering to help maximise its frame rate.
Bitmap caching is your friend
When a display object changes in any way, or intersects the bounding area of some other display object that has changed, then it will need to be redrawn. Unfortunately redrawing complex vector shapes, text, or clips containing deeply nested display lists can be expensive.
Thankfully help is at hand in the form of bitmap caching. Put simply, bitmap caching stores a bitmap representation of a display object and uses the bitmap version whenever the object needs to be re-drawn. In many situations, drawing the bitmap representation to the screen is typically much faster than redrawing the original. Bitmap caching is particularly useful for objects that have translation (movement along the x- or y-axis) or transformation (scaling or rotation) applied to them.
Doppelgänger makes heavy use of bitmap caching to minimise the render time of each frame.
Bitmap caching is your enemy
Hold on! Didn’t I just say that Bitmap Caching was good? Yes I did. But only if it’s used correctly.
Many developers seem to think that activating GPU rendering and bitmap caching everything, will somehow guarantee a blindingly fast frame rate. This couldn’t be further from the truth. Misuse of bitmap caching will cripple your application. Even if you fully understand the ins-and-outs of bitmap caching, it’s all too easy to make a simple mistake that can undo much of your previous optimizations.
To benefit from bitmap caching you need to ensure that the cached bitmap does not frequently become invalidated. If invalid, the bitmap will need to be recreated by rendering its source display object and copying a bitmap representation to an off-screen buffer. To prevent this, don’t cache movie clips that contain timeline animations or container clips that have children that move relative to it.
I must admit that I slipped up here. Some of my initial tests on Doppelgänger performed poorly on iPad 1. I eventually tracked it down to the shutters that open and close between levels. I’d accidentally cached the container, which held both halves of the shutter. Essentially, as they opened and closed, every frame of animation was invalidating the cached version and forcing a re-cache. With the shutters consuming almost twice the height of the screen when fully open, re-caching was killing performance. This was fixed by simply caching each of the shutters’ child clips rather than the container.
To see the shutters in action, take a look at the gameplay video of Doppelgänger.
Bitmaps Vs. Vectors
Use bitmaps or vectors based on their strengths. It’s easy to get into the mindset of using bitmaps for everything because they tend to render faster. However bitmaps can consume significantly more memory and can quickly exhaust GPU memory when GPU Rendering is being used.
Doppelgänger uses a combination of both vector and bitmap assets. The timer’s ticking clock, for example, is a vector timeline animation. With a bounding area of 173×161 pixels and running for 90 frames, a series of bitmaps would simply have been too expensive (almost 10Mb of uncompressed data). The same was also true of the tick and cross animations, which both cover a fairly large area of the screen come the end of their animation cycles. And of course the WeeMee’s themselves are constructed from vector shapes, helping to reduce the app’s footprint and ensuring the WeeMee’s maintain their image fidelity when scaled.
Swap render quality where appropriate
Consider using a lower render quality setting. Bitmaps are relatively unaffected by this and it really can be hard to spot any differences for vector content that moves.
Doppelgänger constantly switches between low and high rendering quality during the lifetime of each game. This gives a massive performance boost, especially on the iPad 1. The drop in render quality is noticeable on the timer’s clock but I felt it was worth doing in order to maximize the frame rate. Each of the WeeMees are rendered into pixel buffers using the highest render quality setting just before each level begins. The bitmap representations of each WeeMee is then used during the level, which by this point has switched to the lowest render setting.
It can very much feel like smoke and mirrors some times, but it’s the final result that counts. These challenges aren’t specific to Flash and ActionScript either. Targeting mobile requires a little more effort than developing for the desktop. However, with some careful planning and an understanding of the constraints you’re working within, you should be able to make the necessary compromises to ensure a fun and engaging experience.