15 June 2010

Game Development for Android: A Quick Primer

[This post is by Chris Pruett, an outward-facing Androider who focuses on the world of games. — Tim Bray]

If you attended Google I/O this year, you might have noticed the large number of game developers showing off their stuff in the Android part of the Developer Sandbox. Unity, EA, Com2Us, Polarbit, Laminar Research, and several others demonstrated high-end games running on Android devices. Part of my role as a Game Developer Advocate for Android is to field requests for information about Android from game developers, and in the last six months the number of requests has gone through the roof. Since there’s clearly a huge amount of interest in Android game development, here’s an overview of how Android games work and what you as a developer should know.

Step One: Decide on a Target Device Class

There are basically two types of devices running Android that you should consider: lower-end devices like the G1 (which I’ll call “first generation” devices), and high-end devices like the Nexus One (”second generation” devices). Though there are a lot of different Android phones on the market, they fall rather neatly into these two classes when it comes to CPU and graphics performance, which are the variables that game developers usually care the most about.

First generation devices are generally phones with HVGA screens, running Android 1.5 or 1.6 (though a few are starting to make their way to 2.1), typically with an 500mhz CPU and hardware accelerated OpenGL ES 1.0 backend. A large number of devices sport internals similar to the G1 (Qualcomm MSM7K CPU/GPU at ~500mhz), so the G1 is representative of this class (and can be safely considered the low end of the spectrum). Based on my tests, these devices can push about 5000 textured, colored, unlit vertices per frame and still maintain 30 frames per second. Using OpenGL ES to draw, I can get just over 250 animating sprites on the screen at 30 frames per second (at 60 fps I can draw just over 100 sprites per frame). These aren’t hard numbers; my benchmarks are fairly primitive, and I’m sure that they can be improved (for example, I haven’t done tests using the GL_OES_point_sprite extension, which the G1 supports). But they should give you an idea of what the first generation class of devices can do.

Second generation devices generally have WVGA screens, much faster CPUs and GPUs, and support for OpenGL ES 2.0. The Nexus One and Verizon Droid by Motorola are both good examples of this class. These devices seem to be about 5x faster than the first generation devices when it comes to raw OpenGL 1.0 performance (I can get at least 27,000 textured unlit colored vertices per frame at 30 frames per second on all of the second generation devices I’ve tested). Using OpenGL ES 2.0 can be even faster, as these devices typically incur some overhead translating OpenGL ES 1.0 commands to their 2.0-native graphics hardware. However, the large screens on these devices often mean that they are fill-bound: the cost of filling the screen with pixels is high enough that it’s often not possible to draw faster than 30 frames per second, regardless of scene complexity.

Since there is a pretty wide performance delta between the first generation class of devices and the second, you should be careful when selecting a target. Based on our metrics about Android versions, first generation devices represent over half of all of the Android phones on the market (as of this writing, anyway; 2.0 devices are growing very quickly). Those games that are able to scale between the first and second generation devices have the largest audience.

Step Two: Pick a Language

If you’re an Android app programmer who’s thinking about getting into game development, chances are you are planning on writing code in Java. If you’re a game development veteran who’s thinking of bringing games to Android, it’s likely that you prefer to do everything in C++.

The side-scrolling action game that I wrote, Replica Island, is entirely Java. It uses OpenGL ES 1.0 to draw and is backwards compatible to Android 1.5. It runs at a good frame rate (close to 60 fps on the G1) across almost all Android devices. In fact, many of the popular games on Android Market were written in Java, so if you’re the type of person who finds coding in C++ like speaking in tongues, you can rest easy in the knowledge that Java on Android is perfectly viable for games.

That said, native code is the way to go if your game needs to run as fast as possible. We’ve just released the fourth revision of our Native Development Kit for Android, and it includes a number of improvements that are particularly useful to game developers. Using the NDK, you can compile your code into a shared library, wrap it in a thin Java shell to manage input and lifecycle events, and do all of the heavy lifting in C++ with regular OpenGL ES APIs. As of Revision 4, you can also draw directly into Java Bitmap pixel buffers from native code, which should be faster than loading bitmaps as GL textures every frame for 2D games that want to do their own scene compositing. Revision 4 also (finally!) includes gdb support for debugging your native code on the device.

You should know that when using the NDK, you don’t have access to Android Framework APIs. There’s no way, for example, to play audio from C++ (though we announced at Google I/O our intention to support OpenSL ES in the future). Some developers use the AudioTrack API to share a direct buffer with native code that mixes and generates a PCM stream on the fly, and many call from C++ into the Java SoundPool interface. Just be aware that for this type of work, a jump through JNI back into Java code is required.

Step Three: Carefully Design the Best Game Ever

Once you have a target system spec and have decided on a development environment, you’re off and running. But before you get too deep into your epic ragdoll physics-based space marine action online RPG with branching endings and a morality system, take a minute to think about your end users. Specifically, there are two areas that require consideration for Android games that you might not be used to: texture compression and input systems.

Texture compression is a way to (surprise!) compress your texture data. It can improve draw performance and let you pack more texture into vram. The problem with texture compression is that different graphics card vendors support different texture formats. The G1 and other MSM7k devices support ATI’s ATITC compression format. The Droid supports PowerVR’s PVRTC format. Nvidia’s Tegra2 platform supports the DXT format. The bad news is, these formats are not compatible. The good news is, all OpenGL ES 2.0 devices (including the Snapdragon-based Nexus One, the OMAP3-based Droid, and Tegra2 devices) support a common format called ETC1. ETC1 isn’t the best texture format (it lacks support for alpha channels), and it isn’t supported on the first generation devices, but it’s the most common format supported (the Android SDK provides a compressor utility (see sdk/tools/etc1tool) and runtime tools for this format).

The bottom line is that if you compress your textures, you’ll need to somehow provide different versions of those textures compressed with different formats. You could do this all in a single apk, or you could download textures from a web site over HTTP, or you could use ETC1 and restrict yourself to only OpenGL ES 2.0 devices. For Replica Island, I just chose not to compress my textures at all and had no problems. You can query the GL_EXTENSIONS string to see what the device you are currently running on supports.

String extensions = " " + gl.glGetString(GL10.GL_EXTENSIONS) + " ";
String version = gl.glGetString(GL10.GL_VERSION);
String renderer = gl.glGetString(GL10.GL_RENDERER);

boolean isSoftwareRenderer = renderer.contains("PixelFlinger");

// On 1.6 and newer, we could use ActivityManager.getDeviceConfigurationInfo() to get the GL version.
// To include 1.5, I'll use the GL version string.
boolean isOpenGL10 = version.contains(" 1.0");  
boolean supportsDrawTexture =
 extensions.contains(" GL_OES_draw_texture "); // draw_texture extension
boolean supportsETC1 =
 extensions.contains(" GL_OES_compressed_ETC1_RGB8_texture ");  // standard ETC1 support extension

// VBOs are guaranteed in GLES1.1, but they were an extension under 1.0.
// There's no point in using VBOs when using the software renderer (though they are supported).

boolean supportsVBOs =
 !isSoftwareRenderer && (!isOpenGL10 || extensions.contains("vertex_buffer_object "));

You should also think carefully about how your game will be played. Some phones have a trackball, some have a directional pad, some have a hardware keyboard, some support multitouch screens. Others have none of those things. Per the Compatibility Definition Document, all Android devices that have Android Market are required to have a touch screen and three-axis accelerometer, so if you can get away with just tilt and single touch, you don’t need to worry about input much. If you want to take advantage of the various input devices that these phones support (which, based on several thousand comments on Android Market about Replica Island, I wholeheartedly recommend), the Android API will package the events up for you in a standard way.

That said, one of the most dramatic lessons I learned after shipping Replica Island is that users want customizable controls. Even if you have added perfect support for every phone, many users will want to go in and tweak it. Or they prefer the hardware keyboard over their phone’s dpad. Or they prefer tilt controls over trackball controls. My advice: plan on providing customizable controls, both so that you can support phones that have input configurations that you didn’t consider, and also so that you can allow users to tweak the experience to match their preferences.

Step Four: Profit!

The rest is up to you. But before you go, here are a few resources that might come in handy:

  • HeightMapProfiler. This is a simple 3D benchmarking tool that I wrote. It is the source of the performance numbers in this post. You can also use it to test how various GL state affects performance on your device (texture size, texture filtering, mip-mapping, etc).

  • SpriteMethodTest. Another simple benchmarking tool, this one for sprite drawing. This code is also useful as a 2D game skeleton application.

  • GLSurfaceView. This is a Java class that makes it trivial to set up an OpenGL ES application. You can use this code in combination with the NDK or with Java alone.

  • Quake Port. The complete source for an Android port of Quake has been made available by Jack Palevich, an Android team engineer. It’s a great sample of how to mix Java and native code, how to download textures to the sdcard over HTTP, and all kinds of other cool stuff (check out his memory-mapped-to-sdcard texture manager).

  • Replica Island. Here’s the complete source to my game, released under Apache2. Use it as a reference, or to make your own games.