Porting MZR to android (NDK)

Posted on

googleplay

Recently I spent some time porting MZR to android and wanted to do a quick write up of some of the challenges I encountered. I’m no android expert so all of these were news to me.

A bit about the code.

First things first. MZR is written in C++ on a custom cross platform engine. The bulk of the code is written in C++ with a thin driver/glue layer per platform. My engine supports PC, iOS and now Android. It’s very easy to add new platforms. However, as many of you who’ve done this engine type of thing know, each platform comes with time consuming maintenance work afterwards.

Here’s a list of things that go in that platform-dependent layer:

  • low level basics – core types, threading primitives
  • low level IO support – file systems, input devices
  • low lever render API – PC(DirectX), iOS (OpenGL ES)
  • low level sound API – both PC and iOS use OpenAL
  • networking – platform specific APIs for fetching data over http, socket based stuff, etc
  • other non-essential stuff (from game point of view): social platforms, store and billing support, etc

Important to stress that the platform bits are contained to that layer. No game code references low level platform APIs. Everything is accessed through a platform-independent internal API that in turn talks to the platform specific code. On PC that is C++, on iOS it would Objective-C and on Android Java/JNI. This ensures that the game code works the same on all platforms and changes to platforms don’t trigger changes to game code.

Note: this may seem an unnecessary layer of abstraction but I do use certain techniques that allow me to bypass indirections and keep performance benefits without introducing dependencies to platform specific code [possibly warrants a post of its own].

Porting to Android

Ironically the most daunting part of the whole process of porting MZR to Android was the project setup. I knew I had to use Android NDK so I can make use of my C++ code base. However, how does a full project get set up with both Java and NDK, what are the right ways to set up both build environment and Eclipse… all down to reading a lot of tutorials, internet posts and trying things out.

My final setup ended up with:

  • Eclipse project for the game. The Eclipse project only references the Java code, the C++ code is elsewhere.
  • Android.mk make-file that references all the C++ sources compiling to a single .so library

All C++ debugging was done using custom tools and printf. In the end I couldn’t set up Eclipse to behave with my folder structure, building and debugging. Main problems here included the source generating a lot of errors by the automatic Eclipse compilation and then refusing to build the project. The Android.mk file had all necessary include paths and built correctly.

Once I had the environment set up, it was down to writing some code.

Core Basics & OS 

Core basics were fairly easy to port. I use pthreads so nothing new there. Everything ported without issues.

However, I did get a nasty surprise in the atomic increment/decrement intrinsics encapsulation. On iOS the atomic inc/dec return the new value after the operation while the Android equivalents return the value before the operation. Something to watch out  for if you ever do this kind of low level porting.

File systems. Here on iOS it was all down to getting the right file system “root” folder and then leave it to standard C libraries to do the job. On Android however there are two types of storage file systems in my engine – one to read assets from (AssetFIleSystem) and one to save data to (SaveFileSystem). Fortunately I already do this separation in my code when I read assets and save game data. That didn’t present a problem beyond the android implementation details.

The asset file system is built on top of AssetManager. I had to pass the AssetManager down to the JNI code from Java and then use it in the platform specific Android code of the engine. The save file system just has a path to the target folder and uses standard C libraries code.

JNI or how Java and native code talk to each other

If you write stuff with NDK, you will inevitably need to do some JNI. Theoretically you can write an android game entirely in native code. However, in practice every third party SDK for android comes as a java library, including the ones provided by Google.

Nothing to worry about there though. JNI is not hard and is well documented.

Here’s some gotchas that caught me out.

Native -> Java

On the Java side just write your classes as you normally would – all the work is then handled in the native part. I tend to use static methods mainly to keep things simple.

1. Cache your methods at load time. You need to execute some code to locate the Java class and method so later on when you want to invoke that method you can use these “handles” to do it. It’s a good practice to do this in your JNI_OnLoad function. The JNI_OnLoad function is called when your native (.so) library is loaded.

2. Make sure you are familiar with how the type names works so you can produce the correct signature for your methods. Otherwise you’ll struggle to find them even though the name is the same and the class is there. Also pay attention to the return type of the method because that mandates how you invoke that method.

For example, invoking a static method that has a void return type would be:

env->CallStaticVoidMethod(classMyClass, methodMyMethod);

While invoking a method that returns “float” would look like this:

float myVal = env->CallStaticFloatMethod(classMyClass, methodMyMethod);

3. When you locate your class and cache the handle, you need to get a global reference to it or it can get moved and then you are left holding onto a dangling pointer.

jclass classMyClass = env->;FindClass( "com/fc/mzr/MyClass");
if (classMyClass == NULL)
{
    FAIL("Can't find MyClass class.");
}

//get global reference to my class
classMyClass = (jclass)env->NewGlobalRef(classMyClass);

4. Make sure the JNI environment you use is attached to the thread you are calling java methods from. For example, in my code a run a number of threads, mostly for IO – file access, networking, etc but also for stuff like sound, music, etc. If you don’t do that you may get crashes that sometimes can be difficult to trace and seemingly inexplicable.

You can ensure the JNI environment is attached to the tread like this:

JNIEnv* JNI_GetThreadEnv()
{
   JNIEnv *env = NULL;
   bool isAttached = false;

   int status = g_JavaVM->GetEnv((void **) &env, JNI_VERSION_1_6);
   if(status == JNI_EDETACHED)
   {
       status = g_JavaVM->AttachCurrentThread(&env, NULL);
       DEBUG_LOG("GetEnv: attach status: %d", status);

       if(status != 0)
       {
    	   DEBUG_LOG("GetEnv: failed to attach current thread");
           return NULL;
       }

       if (status == JNI_EVERSION)
       {
    	   DEBUG_FAIL("GetEnv: version not supported");
       }

       isAttached = true;
   }
   else
   {
	 if (status == JNI_EVERSION)
	{
	   DEBUG_FAIL("GetEnv: version not supported");
	}
   }

   return env;
}

And then your calling code would look something like this:

JNIEnv* env = JNI_GetThreadEnv();
env->CallStaticVoidMethod(classMyClass, methodMyMethod);

5. Make sure you complete your Java execution quickly and return control back to JNI, especially if you have further code that uses that JNI environment in the same function – it won’t stay attached to that thread forever. I make it a practice to have blocking code in the Java part be executed in another task and then callback when long blocking operations are expected.

Java -> JNI

Calling native methods from Java is somewhat easier to set up. You need to declare methods as native in the Java class and then declare them in a very specific way in the native source.

1. Declare the methods in native code as C signature and with the correct name. Curiously the dots in the class path package name become underscores. The ‘extern “C”‘ part is really important, especially if you are using C++ – otherwise JNI won’t find your function.

extern "C"
{
	//mappted to class called MyClass in pacakge com.fc.mzr and the method is called myNativeMethod
	JNIEXPORT void JNICALL Java_com_fc_mzr_MyClass_myNativeMethod(JNIEnv *env, jobject obj, jstring myStringParam);
}

2. Don’t make assumptions on what thread you are once your JNI native methods are invoked. I spawn threads on the Java side to do async operations like Facebook requests, so the callbacks often come from another thread.

General JNI 

Your native code is compiled to .so library that is then loaded by the Java code. You will have to put some code there to load your library. If you have multiple .so libraries that depend on each other then you need to load them in the correct order otherwise you may get a run-time linking error. This is not a 100% on all devices so take care because it may be that your device is working fine but others crash on start because of this.

And now some input multi-touch FUN!

Input was fairly straightforward. I handled the input in the Java part of the code and handed it over to the native part. I did have some problem with events being handled on a different thread to main loop. I ended up caching the events and picking them up once per game frame to ensure all events were handled consistently.

Beyond that there is a manifest setting to enable multi-touch support and the code for support multiple touches may need some care. It certainly took me some time to get it right but I attribute it to me being new to it rather than any gotchas. It all makes perfect sense now.

OpenGL ES is easy… oh wait… what was that bit about the context?

My OpenGL ES code that I use on iOS ported directly with minimal changes to Android. It just worked. There was little need to debug or fix things.

However, there’s a nasty surprise for Android developers with OpenGL ES. When an Android app is suspended (pressing the home button) it would lose the OpenGL context. That means that all resources referenced by the app are lost and the app needs to reallocate/reload all of that once the context is recreated.

MZR uses a lot of dynamic geometry and render-to-texture surfaces. Not only would I have to reload all static resources (like textures) but also recreate all resources that were dynamically generated such as old mazes, pre-rendered texture atlases, pre-rendered signs, etc.

In Android (I’m not sure which version this was introduced in) there is a method on the GLSurfaceView class called “setPreserveEGLContextOnPause”. That supposedly keeps the context so that it survives the app being suspended to the background although it doesn’t guarantee it will be back once the app is resumed. I used that – it works. For the most part the context is there – however I have gotten reports that on some devices it isn’t.

I also made sweeping changes to MZR to make it reconstruct all resources so that it can survive a “lost” context. I did hit some problems with that. I’m assuming this is completely my own fault but I was unable to completely trace the issue on Android. On both PC and iOS I was able to nuke all resources and restore them again but the android version just wouldn’t work. I had limited time to invest in this issue so I left it at that. If there is a need to fix that entirely I’ll look into it again.

OpenAL works a treat but there is a… lag!

I use OpenAL on iOS and PC. The code is the same. I have a high-level audio engine (that uses XACT as authoring tool) that’s built on top of that.

My first reaction was to use one of the Android sound APIs but it looked like a lot of work compared to a tested OpenAL implementation. I found a library called openal-soft that has an Android port. Dropped that in (skipping all the build/setup details) and it worked perfectly….  apart from the half a second delay between triggering a sound and the sound actually being audible. The delay would vary with different devices.

At some point I was going to ship with that lag in but in end thought I’d try the native android audio library – OpenSL.

OpenSL ES is very different from OpenAL. It has a COM like interface and it required some getting used to. This tutorial proved very useful.

I had to make a fresh implementation of my low-level sound system interface using OpenSL.

To play compressed music I used the MediaPlayer in the Java part of the code. I had a policy to cache these players on iOS so that I can have a zero-seconds start of the music playback. On android however there is (can be) a limit of available players at any given moment so you could end up with the player failing to play your music track. I was hitting this problem at some point.

Android devices come in all forms and shapes

MZR is a portrait game. I could limit the orientation to “portrait” in the android manifest. However, that doesn’t guarantee the resolution or the aspect ratio of the main display buffer.

This means that MZR suddenly needed to support all sorts of resolutions, not just the standard iPhone and iPad ones. MZR is mainly a 3D game so that wasn’t a big problem for the game content.

Front end 2D interface however is another matter. I have a 2D layout tool where I can layout each of my 2D interface pages that I then load and display in-game. To support an unexpected resolution the tool supports anchoring. Anchoring is used in many windowing systems to control the behaviour of UI elements when window panels change size. For example if a button is anchored to the left of the parent panel, it will stay relative to the left edge of that panel. In my tool I can anchor elements both horizontally and vertically. Respectively they can be anchored left, centre or right and top, centre and bottom. That solves most problems with small variations of the resolution and aspect ratios.

One final glitch is font rendering. In my engine fonts try to stay pixel perfect in 2D unless explicitly overridden by the game code. Fonts would look really small on screen for higher resolutions. I had to introduce multiple font sizes and switch at certain resolution thresholds to keep the fonts crisp and relatively well sized.

Android devices come in all muscle shapes and sizes

MZR is a fairly demanding game. Despite its simplicity it renders quite a bit of geometry and overdraws the screen with transparent effects. This could prove particularly troublesome especially if the hardware is slightly out of date or/and resolution is really high.

To address this on iOS I had a predefined list for each version of iOS device with graphics quality settings. Some features would be enabled on some and disabled on other devices. For example iPhone4 has hardly any graphics features enabled and iPhone5 has all the bells-and-whistles… with original iPad Mini being somewhere in between.

For android this was not a practical approach. With potentially thousands of devices I wouldn’t be able to create such a list. To solve this I introduced a number of features:

  • a graphics settings page in the options menu – the user can go and switch graphics options on/off to tweak their performance
  • an automatic measurement of average and lowest FPS (frames-per-second) and switching graphics options off if that FPS measurement drops under certain limit; that way the app always starts with maximum settings and after a couple of runs (if FPS was not satisfactory) dropped the settings to recover some FPS
  • server solution – after every run the game would upload average FPS info to the server anonymously for that device (and settings); the server can then aggregate the data and find an optimal preset of settings for that device type;
  • server presets – every time the game boots it contacts the server and asks for graphics settings presets – if presets exist for that device then they are downloaded and set as default – turning this process into self-accumulating list for thousands of devices

Immersive mode & full screen

When running a fullscreen game on android you will want to hide the status bar. Also some android devices have their home and back buttons at the bottom of the screen – (action bar)?

There are a few examples on the internet how to handle this but what I’d missed out is that I have to set up an OnSystemUiVisibilityChangeListener which is a handler that gets called when there are changes to the visibility settings of the app. In that handler I’d have to set the appropriate flags to make the app full screen.

You can read more about Immersive mode here: https://developer.android.com/training/system-ui/immersive.html

Check it out!

The game is now available on the Google play store. Be sure to check it out:

https://play.google.com/store/apps/details?id=com.funkycircuit.mzr

That’s it for now.

Thanks for reading!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s