Clean shutdown of StandardGame

As discussed in other threads before, there occur problems with the shutdown of StandardGame in a threaded environment. There are always at least the main thread and the OpenGL thread, so this should apply for nearly everybody.



The relevant threads:

  • Program exit?
  • How to cleanly shut down StandardGame ?
  • Threading and StandardGame



    The problem typically occurs when the OpenGL thread closes (f.e. because the user clicked the X of the window) but other threads still run and use GameTaskQueueManager with a .get() on the returned Future object. Try closing your games window when your main thread still set’s up your GameStates :wink:



    For cases where no cleaning up of the setup thread is required, there is a simpler solution that has been proposed by Momoko_Fan:


    final StandardGame game = new StandardGame("TestLock");
    game.start();
   
    // setup all the game states in daemon thread, to shutdown when OpenGL thread shuts
    Thread setupThread = new Thread(new SetupProcess(game));
    setupThread.setDaemon(true);
    setupThread.start();



But if any running thread wants to clean up before dying, this can be done with my solution.
I worked out a class which deals with the problem and provides a workaround which should suit for nearly every user of StandardGame.

Here is the intended use of my solution:


final StandardGame game = new StandardGame("TestLock");
game.start();
GameShutdownManager.get().addSystemExitWithMaxDelay(5000);
// if a thread needs to clean up, register it to be interrupted or add a custom listener
GameShutdownManager.get().addThreadInterrupter(Thread.currentThread());
GameShutdownManager.get().addListener(...);



Here comes the code for the GameShutdownManager class:


package org.pohldev.jmeutil;

import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jmex.game.state.GameState;
import com.jmex.game.state.GameStateManager;

/**
 * This class serves as a workaround for clean shutdown of
 * <code>StandardGame</code> when other threads are working. At least the
 * main-Thread which can be considered the initialization thread of a game
 * application could still run if the OpenGL thread stops. It is recommended to
 * add a <code>ShutdownListener</code> to the <code>GameShutdownManager</code>
 * for every game that uses the <code>GameTaskQueueManager</code> to make sure
 * the threads do not lock on a future that will never occur.
 * <p>
 * Please note, that an internal game state is created and attached to the
 * <code>GameStateManager</code> if listeners are added. Also note that this
 * class is thread safe.
 *
 * @author Sven Pohl
 */
public class GameShutdownManager {
  private static final Logger logger = Logger.getLogger(GameShutdownManager.class.getName());

  private static final GameShutdownManager INSTANCE = new GameShutdownManager();

  private GameShutdownManager() {
  }

  public static GameShutdownManager get() {
    return INSTANCE;
  }

  private ConcurrentLinkedQueue<ShutdownListener> queue = new ConcurrentLinkedQueue<ShutdownListener>();

  private ShutdownObserverGamestate gameState;

  public synchronized void addListener(ShutdownListener listener) {
    if (queue.isEmpty()) {
      gameState = new ShutdownObserverGamestate();
      GameStateManager.getInstance().attachChild(gameState);
    }
    queue.add(listener);
  }

  public synchronized void removeListener(ShutdownListener listener) {
    queue.remove(listener);
    if (queue.isEmpty()) {
      gameState = new ShutdownObserverGamestate();
      GameStateManager.getInstance().detachChild(gameState);
    }
  }

  public interface ShutdownListener {
    void onShutdown();
  }

  private class ShutdownObserverGamestate extends GameState {
    public void cleanup() {
      for (Iterator<ShutdownListener> i = queue.iterator(); i.hasNext();) {
        ShutdownListener listener = i.next();
        listener.onShutdown();
      }
    }

    /** no need to update anything */
    public void render(float tpf) {
    }

    /** no need to update anything */
    public void update(float tpf) {
    }
  }

  public ShutdownListener addThreadInterrupter(final Thread thread) {
    ShutdownListener result = new ShutdownListener() {

      public void onShutdown() {
        if (thread.isAlive())
          thread.interrupt();
      }

    };
    addListener(result);
    return result;
  }

  /**
   * Convenience method to add a ShutdownListener, which executes a
   * System.exit(0) after the game (OpenGL) thread has finished.
   *
   * @param maxDelayMillis
   *          the maximum delay in milliseconds of the System.exit(0) call. If
   *          the OpenGL thread did not finish then, System.exit(0) is called. 0
   *          means no delay at all, which can lead to java virtual machine
   *          crash.
   * @return the created ShutdownListener
   */
  public ShutdownListener addSystemExitWithMaxDelay(final int maxDelayMillis) {
    ShutdownListener result = new ShutdownListener() {

      public void onShutdown() {
        // TODO is this assumption reliable?
        final Thread openglThread = Thread.currentThread();
        if (maxDelayMillis <= 0)
          System.exit(0);

        try {
          new Thread(new Runnable() {

            public void run() {
              long startMillis = System.currentTimeMillis();
              while (openglThread.isAlive()
                  && System.currentTimeMillis() < startMillis + maxDelayMillis) {
                try {
                  Thread.sleep(50);
                } catch (InterruptedException e) {
                  logger.warning("delay of system exit interrupted");
                }
              }
              System.exit(0);
            }
          }).start();
        } catch (Throwable e) {
          logger.log(Level.WARNING, "could not start thread to call a delayed "
              + "System.exit(0). exiting instantly.", e);
          System.exit(0);
        }
      }

    };
    addListener(result);
    return result;
  }

}



Further on here is a test class simulating the problem, and solves it with this class:


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import org.pohldev.jmeutil.GameShutdownManager;

import com.jme.util.GameTaskQueueManager;
import com.jmex.game.StandardGame;

public class TestStandardGameShutdown {

  public static void main(String[] args) {
    final StandardGame game = new StandardGame("TestLock");
    game.start();
   
    // solution to the problem that the game
    // does not shut down without this
    GameShutdownManager.get().addSystemExitWithMaxDelay(5000);
   
    // An alternative is...
    // GameShutdownManager.get().addThreadInterrupter(Thread.currentThread());

    // shuts down the game 2 seconds later
    // Also try closing the window manually instead.
    new Thread(new Runnable() {

      public void run() {
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
        game.shutdown();
      }
    }).start();
   
    // do something, lets say we would instead setup a few complex game states
    long startTime = System.currentTimeMillis();
    long stopTime = startTime + 4000;
    while (System.currentTimeMillis() < stopTime) { }
   
    // inside the complex initialisation say we got to do something
    // which needs to be executed in the OpenGL thread.
    System.out.println("try to execute code in OpenGL thread... this locks");
    try {
      GameTaskQueueManager.getManager().update(
          new Callable<Void>() {

            public Void call() throws Exception {
              // lets imagine we use a texture renderer or anything
              return null;
            }
          }).get();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    }
    System.out.println("call successful done!");
   
  }
 
}



Any comments are welcome. I am a newbie in this forum, so please feel free to teach me.
----
edit: changed the code to be compatible with java 1.5 and removed doubled code in InterrupterGameState
edit: renamed InterrupterGameState to ShutdownObserverGamestate
edit (22:16): I have posted a feature request here.