Pages

Tuesday, 10 July 2012

Android game loops

I’ve been porting some of the Android SDK samples from their original Java over to Oxygene for Java, with the hope they’ll be included in a forthcoming update to the product.

I’d already contributed a port of the AccelerometerPlay demo, which is a neat little demo of using the accelerometer to control several balls on a surface (the screen).

Among other examples I’ve been looking at are the Lunar Lander and JetBoy demo games. Now, if you wanted to start writing a graphics frame-based game that used a SurfaceView as its output you’d think these would be a great starting point, right?

LunarLander

Wrong!

Well, kind of wrong anyway.

JetBoy

The games work ok and you can play them, although they both have the shortcoming that they were written back in the days when most devices had a D-Pad (directional pad) and/or physical keyboard. Consequently the original examples are unplayable on many current devices. That’s no problem – it’s not much trouble to add in support for touches on bits of the screen or swipes across or up and down the screen. Actually I had to add in swipe support for the Snake sample game, which also relied on a D-Pad, but since that one’s not based on a thread (being a simpler game it uses a Handler and its sendMessageDelayed() method).

The problem with Lunar Lander and JetBoy is with how their thread-based operation has been laid out. The JetBoy architecture was based on the simpler Lunar Lander and so both games suffer from the same problem. The symptom is that if you switch away, maybe by pressing Home or by a phone call coming through, and then switch back the game crashes out. If you look at a logcat from either app running and crashing you’ll find that the problem is an IllegalThreadStateException being thrown with the message:

java.lang.IllegalThreadStateException: Thread already started.

This doesn’t look like a very good basis for a couple of evidently appealing sample apps, being designed with a thread crash in… Clearly people will base their own games on these apps and be rather frustrated by this poor design.

Anyway, enough of the criticism. What’s the issue here? Well, a cursory search on the web shows this has (unsurprisingly) cropped up a lot of times and there have been various ways it has been responded to. So let’s look at the problem and the solution.

The games have an Activity whose UI is made up of a SurfaceView descendant. The game logic is contained in a Thread descendant. When the app starts up and the activity’s onCreate() method runs it creates the custom surface view and asks that view to create the thread.

The surface view has a couple of methods that mark the lifetime of the actual drawable surface: surfaceCreated() and surfaceDestroyed(). In surfaceCreated() the thread is set in motion with a call to its start() method. When the surface is destroyed, for example by switching back to the home screen, two things occur. Firstly a member field flag is set to indicate that the thread’s body (its run() method) should stop whirring round updating state and drawing frames. Then the thread’s join() method is called, which blocks the surface view thread until that run() method has done its job and exited.

Ok, with me so far? No evident problem so far. What happens when we switch back to the app?

Well, when the app’s UI resumes the surface view’s surfaceCreated() method is called, which calls the thread’s start() method to start it off again. And therein lies the nub of the problem. As the documentation states, start() will throw an IllegalThreadStateException if the thread has been started before.

Because of this coding scheme the Lunar Lander and JetBoy apps are set up to fail. So, what’s a good solution?

Well, I’ve tried a couple of options, some of which are frowned upon as not being the right way and settled for the suggestion of StackOverflow user Pompe de velo in this post. The changes required are as follows:

  • in the thread define member variables:
    • var mPauseLock: Object := new Object;
    • var mPaused: Boolean;
  • have the thread’s run() method keep iterating endlessly (while true do... instead of while mRun do...).
  • in this loop, after all the individual iteration’s physics update and frame drawing add:

    locking mPauseLock do
       while mPaused do
         try
           mPauseLock.wait
         except
           on e: InterruptedException do
             { whatever };
         end

  • if your thread already has pause() and unpause()/resume() type methods implemented then great. If not add them.
  • have the pause() method do this:

    locking mPauseLock do
      mPaused := true;

  • have the unpause() method do this:

    locking mPauseLock do
    begin
      mPaused := false;
      mPauseLock.notifyAll
    end;

  • in the activity’s onPause() method call the thread’s pause() method
  • declare a member field in the surface view:

    var mGameIsRunning: Boolean := false;

  • in the surface view’s surfaceDestroyed() method remove all the code relating to managing the thread
  • have the surface view’s surfaceCreated() method change the call to thread.start() to this code:

    if not mGameIsRunning then
    begin
      thread.start;
      mGameIsRunning := true
    end
    else
      thread.unpause;

Yeah, so there’s a few changes there, but once done the app can survive being switched away from and back to.

It would probably be a good move to take the fixed app, strip all the logic out and use it as a skeleton gaming loop app, which I may well do.

For now, I have more bugs to fix in SDK demo apps I’m porting over. Currently I’m getting on top of an issue with the BluetoothChat demo where exiting and restarting the app means connections from another device no longer show up on the UI. It would appear the Android SDK demos didn’t get a particularly good test through….. What’s more of a shame, though, is that despite numerous people reporting issues, the demo apps do not get updated to fix the problems.

<sigh>

2 comments:

  1. I am building a colour program for android operating system. Increasing the FingerPaint program example provided with the SDK. However, as opposed to FingerPaint, I'm using SurfaceView with a individual making line to sketch to the outer lining area.

    ReplyDelete