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
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.