×
Namespaces

Variants
Actions

How to Suspend Java Game on Interrupt

From Nokia Developer Wiki
Jump to: navigation, search

This article explains how to meet the Java Verified requirements for suspending a game on various interrupts

Article Metadata
CompatibilityArticle
Created: grahamhughes (26 Mar 2013)
Last edited: hamishwillee (25 Jul 2013)

Contents

Introduction

This article explains how to meet the Java Verified requirements for suspending a game on various interrupts. Note that I'm referring to these as "interrupts" rather than "pauses", since the application itself does not necessarily "pause". It may continue running in the background, depending on the device. By "interrupt", I mean something that will interrupt the user in playing the game - any event where a system screen pops-up over the game.

Note.pngNote: These requirements apply to any application that requires constant interaction with the user - almost always a game. It does not, however, apply to games that are strictly turn-based and have no timer element, since the interrupt will not actually affect the game play.

Specifically, this article relates to Java Verified requirements:

  • FN2 - External incoming communication – voice call
  • FN3 - External incoming communication – SMS
  • FN4 - External incoming interruption – charging
  • FN14 - External Interruption – Alarm Clock

Note that not all of these events result in an application "interrupt" on all devices. Charging, for example, might just result in a change to a status bar icon. Also, on some devices, other events can result in an interrupt, such as:

  • changing the volume
  • connecting to a Bluetooth device


Detecting an Interrupt

There are four possible scenarios, depending on the device.

  1. hideNotify() will be called - assuming you are displaying a Canvas
  2. pauseApp() will be called
  3. no event will be called, the VM will be frozen for the duration of the interrupt
  4. no event will be called and the app will continue to run


Since in the case of (4) there is nothing you can do, (and it occurs on only one device I have ever seen (O2 X2), and it's a very old phone) I'm going to ignore that scenario.

On Nokia devices, pauseApp() is never called. So, we're going to rely on hideNotify(), and cope with other possibilities later. See Porting and Portability .

What We Must Do

In order to comply with Java Verified, we MUST display a pause screen. This need not contain anything more than "press 5 to continue", "tap the screen to continue", or whatever is appropriate. You need access to this screen from the game as well, in order to comply with Java Verified FN5. Most games have a "pause menu", which includes options to exit the game, turn sound on or off, and so on, as well as to resume play.

We Don't Need to Resume

We don't need to do anything at the end of the interruption, other than to be in the "pause" state. So, we do not need to intercept showNotify() or startApp() events to detect the end. Once we're interrupted, that's enough.

Creating a Pause State

Don't stop threads. Any threads that loop need to go into an "idle" state.

public class MyGame extends Canvas implements Runnable {
private volatile boolean paused;
 
protected void hideNotify() {
// we intercept this to detect an incoming call/sms/etc
pauseGame = true;
}
 
public void run() {
while (!exitGame) {
if (!pauseGame) {
doGameLogic();
}
repaint();
serviceRepaints();
makeSureYouSleep();
}
}
 
protected void paint(Graphics g) {
int width = getWidth();
int height = getHeight();
if (pauseGame) {
// this paints something like "press 5 to resume"
paintPauseScreen(g, width, height);
} else {
paintGameScreen(g, width, height);
}
}
}

You might already have some kind of state mechanism, in which case "PAUSE" is just another state.

    private volatile int state;
private int stateToResume;
 
protected void hideNotify() {
if (state != State.PAUSE) {
// so we know where to go back to
stateToResume = state;
// go to "pause" state
state = State.PAUSE;
}
}

Why Not Stop Threads?

Why not do this?

// DON'T DO THIS
 
private volatile boolean running;
 
public void run() {
running = true;
while (running) {
// do game stuff
}
}
 
protected void hideNotify() {
// stop the thread
running = false;
}
 
protected void showNotify() {
// kick off a new thread to resume the game
(new Thread(this)).start();
}

So, why not?

You cannot stop a thread. All you can do is leave a message to tell a thread to stop itself, as in the code above with the boolean variable. The thread won't immediately stop... it will only stop next time the loop goes around and checks the variable. On a typical game, on a low end phone, that might be as much as 100ms later.

What's the problem?

Some low end devices are not able to run Java apps while handling a call. As soon as the phone rings, the application "goes into the background", which can actually mean that the entire Java VM is frozen and no more bytecode executes until the end of the call. The effect of this is that the events might not happen when you expect.

Expected: hideNotify() at the start of the call, showNotify() when the call ends.

Actual: (possibly, depending on device) hideNotify() and showNotify() happen in rapid succession, one then the other, with no delay in between - this can happen if both events are sitting in the event queue when the VM gets "unfrozen".

Think about what happens in my "don't do this" example, if hideNotify() and showNotify() happen one then other, with no delay in between.

  1. hideNotify() gets called, and sets "running" to false
  2. showNotify() gets called, and starts a new Thread.
  3. the new Thread enters run(), and sets "running" to true
  4. the old Thread eventually loops around, checks "running" and finds it's true - it doesn't fall out of the loop

We now have two threads running around the game loop and some very odd things happening.

I have seen this dozens and dozens of times, where game developers have used this technique to stop and restart the game thread. The code works fine on the device they originally tested the code on (probably a Nokia), but causes havoc when you try to get the game working on other devices.

Allowing the thread to keep running, in a loop where it just "idles" (sleep-loop-sleep-loop-...) is much more reliable across different implementations of MIDP, and never causes any problems in my experience. Only the most minimal amount of code is executing (even with a short sleep), so battery impact is light.

hideNotify()

In the example above, we've used hideNotify() to detect the interrupt. One point to remember is that hideNotify() also occurs when the Canvas stops being current Displayable. Ideally, a game will create one Canvas, set it "current", and never call setCurrent() ever again. If you must change Displayables, make sure you account for that in your hideNotify() code.

Porting and Portability

Multiple Events

Some devices will generate multiple events. The obvious example would be pauseApp() and hideNotify(). However, it is not unheard of for devices to generate multiple events of the same kind. Earlier, I offered this version of hideNotify():

    private volatile int state;
private int stateToResume;
 
protected void hideNotify() {
if (state != State.PAUSE) {
// so we know where to go back to
stateToResume = state;
// go to "pause" state
state = State.PAUSE;
}
}

Notice that this copes with receiving a hideNotify() event while already in the PAUSE state. Without the "if", you would end up with stateToResume getting set to PAUSE, and a paused game that resumes back into the pause state!

startApp() and pauseApp()

Nokia devices never call pauseApp(), which in turn means they call startApp() only once. This is not universally true of MIDP devices. For portability, always wrap the startApp() code in with a guard flag:

    private boolean started;
protected void startApp() {
if (!started) {
// do initialization stuff here, and never do it again
started = true;
}
}

In general, you can forget pauseApp() if you're handling hideNotify(), since devices that send pauseApp() will invariably send hideNotify() (I can't think of one that doesn't). So, I recommend you leave pauseApp() empty. However, if you come across a device where interrupt detection is not working, you could try routing the pauseApp() event to hideNotify():

    protected void pauseApp() {
Displayable d = Display.getDisplay(this).getCurrent();
if (d instanceof Canvas) {
((Canvas) d).hideNotify();
}
}

No Events

What if you don't get an event at all? On some devices, the VM just freezes during a phone call, and unfreezes at the end without telling you.

Another thing we have to deal with is time. If your game depends on elapsed time (for example, you have a certain time to complete a level), then you need to know for how long the VM froze so that you don't resume the game and immediate time-out.

Well, we can detect the VM freezing... this isn't nice, but it works.

public final class SuspendDetector implements Runnable {
private static final int SLEEP_TIME = 400;
private static final int THRESHOLD = 2500;
 
private static volatile boolean quit;
 
/** Keeps track of the total time for which the VM has been suspended */
private static volatile long timeDelta;
 
/** Target object for notification */
private static Canvas target;
 
/** This is a singleton class */
private static SuspendDetector instance;
 
public static void attach(Canvas c) {
if (instance == null) {
instance = new SuspendDetector();
}
target = c;
}
 
/** @return adjusted current time, accounting for VM suspension */
public static long currentTimeMillis() {
return System.currentTimeMillis() - timeDelta;
}
 
/** Terminate watchdog thread */
public static void stop() {
quit = true;
}
 
private SuspendDetector() {
(new Thread(this)).start();
}
 
/** Watch for VM suspension */
public void run() {
while (!quit) {
long before = System.currentTimeMillis();
try {
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
// ignore
}
// for how long did we actually sleep?
long elapsed = System.currentTimeMillis() - before;
 
if (!quit && elapsed > THRESHOLD) {
// treat this as a VM suspension
timeDelta += elapsed;
if (target != null) {
// inform target
target.hideNotify();
}
}
}
}
}

It works on the principle that, if the VM is frozen, a sleep() will take much longer that it was supposed to. Once you add this class, you just need to:

SuspendDetecter.attach(myCanvas);

Don't forget to make sure you kill the thread when the application shuts down.

SuspendDetector.stop();

You can replace calls to System.currentTimeMillis() in your game code with SuspectDetector.currentTimeMillis() - provided you don't rely on the return value telling you the correct time! Use it only to determine elapsed time since the start of the game. This should make sure that game-time remains unaffected.

There are two tweakable parameters here.

  • SLEEP_TIME - make this shorter if you need to respond to an interrupt more promptly
  • THRESHOLD - this is the length an interrupt must last to be detected
    • make it shorter if interrupts are getting missed
    • make it longer if slow parts of the code (like writing to RMS) are creating "false interrupts"
This page was last modified on 25 July 2013, at 06:48.
80 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×