Building a Cross-Platform Game with libGDX, Part 3: iOS and RoboVM
Recently Google revealed their new Inbox app, the iOS port of which uses their j2objc tool to translate Java source to Objective-C. I thought I would share a few experiences with RoboVM, which compiles JVM bytecode directly into native iOS binaries.
When developing a game, the libGDX and RoboVM libraries contain almost all of the cross-platform support you'd want. The only platform-specific code you absolutely need is a small launcher, which looks something like this:
public class IOSLauncher extends IOSApplication.Delegate { @Override protected IOSApplication createApplication() { IOSApplicationConfiguration config = new IOSApplicationConfiguration(); config.preventScreenDimming = false; config.orientationPortrait = true; config.orientationLandscape = false; config.depthFormat = GLKViewDrawableDepthFormat.None; config.useAccelerometer = false; config.useCompass = false; config.allowIpod = true; return new IOSApplication(new AppMain(), config); } public static void main(String[] argv) { NSAutoreleasePool pool = new NSAutoreleasePool(); UIApplication.main(argv, null, IOSLauncher.class); pool.close(); } }
Note that IOSLauncher implements UIApplicationDelegate, so you can add delegate methods for app lifecycle, push notifications, URL handling, etc. right in this file. You also have access to the main UIViewController.
If you need to implement functionality not provided by the base libraries, RoboVM has a bridge to C or Objective-C native code. It's fairly simple to use, for instance I need to extend the default bindings to add an iOS 8-specific API call:
@Library("UIKit") @NativeClass class UIPopoverPresentationController extends NSObject { @Property public native void setSourceView(UIView v); }
Unlike JNI there is no native glue to write! Fortunately RoboVM has a pretty complete set of Cocoa Touch bindings, so usually the binding you need already exists, for example:
UIActivityViewController controller = new UIActivityViewController(activityItems, null); iosApplication.getUIViewController().presentViewController(controller, true, null);
In most instances you'll want to design an interface that exposes the desired functionality, then implement it against the different platforms, using Class.forName() to look up the platform-specific implementation. I've implemented bare-bones advertising, game services, IAP, and sharing interfaces along these lines.
There are still a few outstanding areas, for example stack traces are not exposed to the app, so you can't walk stack frames and printStackTrace() does nothing. However, you can install the Flurry bindings (or pick your favorite crash reporting tool) and enable crash reporting:
private void setupCrashReporting () { Flurry.setCrashReportingEnabled(true); Flurry.setAppVersion(APP_VERSION); Flurry.startSession(API_KEY); }
Unfortunately this isn't the entire picture, because in my tests the symbolication for my app's code was wrong (Flurry uses their own symbolication engine AFAIK) and I had to do it manually using the command line. Hopefully this will get better.
There's also no console output by default on iOS targets, but you can easily whip up a class to pipe it to Foundation.log(), e.g.:
System.setOut(new FoundationLogPrintStream());
How's performance? Pretty good, although I haven't measured in awhile. For my app, which has some CPU-intensive calculations, it seems a Kindle Fire HD and iPad 3rd gen are on about equal footing (ignoring GPU differences of course). You can take full advantage of multithreading and the RoboVM folks have done some optimizations to optimize memory allocation for this case.
There are GC pauses, but I haven't really noticed them often, and I don't go out of my way to avoid creating garbage either. You can also use the XCode Instruments tools to profile your app (among other things) and the stack traces are pretty well understandable.













