Saturday, August 28, 2010

Supporting iPhone and Android in the same codebase

I've noticed that a lot of people are surprised we have a cross platform engine for iPhone and Android. It also runs on Windows and Mac. Within a short period we could have any of our iPhone games running on any of those other platforms. I thought it would be worthwhile to go into some details about how we did it.

Engine and Platform Philosophy:

The Golden Hammer engine was written from the get-go to be cross platform. Keeping other platforms in mind early on will save a lot of effort when it's time to port. This really boils down to three things:
  • C++ is used for the bulk of the code.
  • Interface classes are used to wrap major systems.
  • No direct calls to the operating system are allowed outside of a platform wrapper.

C++ is supported on PC, Mac, iPhone, Android, XBox, Wii, PS3 and the list goes on and on. The operating system calls on iPhone are in Objective C, but the rest of your code can be in C++. Likewise with Java on Android through use of the NDK.

Major systems like rendering are wrapped in a high level interface. We support DirectX, OpenGL, and OpenGL ES renderers. Keeping the interface high level keeps the nasty high frequency virtual calls at bay when setting render states. For iPhone and Android you can get away with just an OpenGL ES renderer, but not being locked out of platforms like the Wii later on is nice to have. Sound is a place where you will need an interface in order to successfully port from iPhone to Android.

Platform function calls are all abstracted behind interfaces. On the iPhone this means that Objective C calls are not allowed anywhere in the game code outside a minimal set of classes to represent the platform. On Android this means that the game is not allowed to make any JNI calls except from within the platform layer.

Our engine is about 60,000 lines of platform-independent C++ code, with the platform interface layer running about 2,000 lines. In theory we don't need to modify any of those 60k lines in order to add support for a new platform.

Linking C++ and Operating System calls:

Calling Objective C code from C++ is super easy though the use of .mm files, which let you compile both Objective C and C++ within the same class. Provide a C++ interface for the game to use, and then subclass it with a .mm file to make the platform calls.

Communicating between C++ and Java is a bit more work with JNI as your only choice. You create a Java function on one side, and a C function on the other to act as the glue code. Then subclass your C++ interface to call your new wrapper functions. I recommend doing some web searches on JNI for details.

Handling Input:

There are many ways to set up your interface to the platform input. I don't strongly advocate one over another, but I can explain how we do it. The platform code never directly causes the game to do anything. We maintain an input state class with all the current key positions, accelerometer values, etc. When an accelerometer call comes in we update the input state. On the next frame the game looks at the updated input and makes decisions. On iphone this has the added bonus of never doing much work in response to an input event, which prevents the operating system buffer from overflowing and losing events.

The iPhone/Android Platform Layer:
  • Texture loading: On iPhone we use custom code for PVR loading, and UIImage for uncompressed textures. On Android we use BitMap.
  • Sound: On iPhone we use a platform independent OpenAL library with platform code for loading the files for effects, and AVAudioPlayer for music. On Android we tell Java code to start playing a sound.
  • File Access: This is a simple .mm interface on iPhone, but on Android we need to ask Java for a file handle that we can then read in C++.
  • Handling OS input events.
  • Creation of the OpenGL context.
  • Time calculation.
  • Threads.
  • Debug output.
Gotchas:

Android does not have breakpoints for native code. This means you are stuck with printf debugging while doing the port.

Setting up the Android NDK environment is time consuming. Essentially you need to download and set up the NDK, create makefiles, and build your C++ through the command line. You then link into the native library in your eclipse project.

Hardware fragmentation on Android is a real problem, even when ignoring the performance differences between a Galaxy and a Hero. All iPhones support OpenGL 1.1, but some Android phones are still on 1.0. The Hero can't generate mipmaps on the GPU for example. Be prepared to have to test on a wide variety of phones.

File size matters on Android phones before 2.2. The phones have a limited amount of space available for apps, because they can't be installed on the much larger SD card. If your app is too large, owners of older phones will uninstall quickly.

There's is no standard compressed texture format on Android. If you choose to include PVR files, you will also need to include an uncompressed fallback texture. This can be a problem when considering the file size.

Sounds on Android are slow, especially for starting and stopping. Maybe I'm missing something in my implementation, but I had to remove several sound effects during the port for performance reasons.

The NDK does not come with STL included. You will need to use something like STLPort.

The iPhone and Android audiences are not the same! Be prepared to have a new marketing challenge once your port is complete.

Final Notes:

Several of the topics here could easily be expanded into their own posts. If you'd like to see more detail on anything please let me know.




1 comment: