2 * Copyright (c) 2003-2009 jMonkeyEngine
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
9 * * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 package com.jmex.game;
35 import java.awt.Canvas;
36 import java.lang.Thread.UncaughtExceptionHandler;
37 import java.util.concurrent.Callable;
38 import java.util.concurrent.ExecutionException;
39 import java.util.concurrent.Future;
40 import java.util.concurrent.locks.Lock;
41 import java.util.concurrent.locks.ReentrantLock;
42 import java.util.logging.Level;
43 import java.util.logging.Logger;
44 import java.util.prefs.BackingStoreException;
45 import java.util.prefs.Preferences;
47 import com.jme.app.AbstractGame;
48 import com.jme.image.Image;
49 import com.jme.input.InputSystem;
50 import com.jme.input.MouseInput;
51 import com.jme.input.joystick.JoystickInput;
52 import com.jme.math.Vector3f;
53 import com.jme.renderer.Camera;
54 import com.jme.renderer.ColorRGBA;
55 import com.jme.system.DisplaySystem;
56 import com.jme.system.GameSettings;
57 import com.jme.system.PreferencesGameSettings;
58 import com.jme.system.dummy.DummySystemProvider;
59 import com.jme.system.jogl.JOGLSystemProvider;
60 import com.jme.util.GameTaskQueue;
61 import com.jme.util.GameTaskQueueManager;
62 import com.jme.util.NanoTimer;
63 import com.jme.util.TextureManager;
64 import com.jme.util.Timer;
65 import com.jmex.audio.AudioSystem;
66 import com.jmex.awt.jogl.JOGLAWTCanvasConstructor;
67 import com.jmex.awt.lwjgl.LWJGLAWTCanvasConstructor;
68 import com.jmex.game.state.GameStateManager;
71 * A game that implements all of the basic functionality that you will need.
73 * This is intended to be the next logical step up from {@link com.jme.app.SimpleGame
74 * SimpleGame} and can be utilised in production games.
76 * {@code StandardGame} provides the following features to ease game development:
79 * <li>client/server division without needing any code changes;</li>
80 * <li>an alternative settings to replace the PropertiesIO system;</li>
81 * <li>built-in (forced) multithreading as the OpenGL thread is managed for you;</li>
82 * <li>the ability to inject additional work into the OpenGL thread using a task queue;</li>
83 * <li>shadow support;</li>
86 * as well as re-initialisation of the graphical context (if settings change
87 * for example) and everything else a typical game requires.
89 * However, even with all of the extras that {@code StandardGame} provides it
90 * does not force anything extra on you as the non-necessary items should be
91 * put into your {@link com.jmex.game.state.GameState GameState}s and managed
92 * there. This process helps to organise the different aspects of your game and
93 * get the game process started ASAP to kill the long-standing problem of
96 * @author Matthew D. Hicks
97 * @version $Revision$, $Date$
99 public final class StandardGame extends AbstractGame implements Runnable {
100 private static final Logger logger = Logger.getLogger(StandardGame.class
103 public static final int DISPLAY_WINDOW = 1;
104 public static final int DISPLAY_CANVAS = 2;
106 public static boolean THREAD_FRIENDLY = true;
107 public static int DISPLAY_MODE = DISPLAY_WINDOW;
109 public static enum GameType {
113 private Thread gameThread;
114 private String gameName;
115 private GameType type;
116 private boolean started;
117 private Image[] icons;
120 private Camera camera;
121 private ColorRGBA backgroundColor;
122 private UncaughtExceptionHandler exceptionHandler;
124 private Canvas canvas;
126 private Lock updateLock;
128 public StandardGame(String gameName) {
129 this(gameName, GameType.GRAPHICAL, null);
132 public StandardGame(String gameName, GameType type) {
133 this(gameName, type, null);
136 public StandardGame(String gameName, GameType type, GameSettings settings) {
137 this(gameName, type, settings, null);
141 * @see AbstractGame#getNewSettings()
143 protected GameSettings getNewSettings() {
144 boolean newNode = true;
145 Preferences userPrefsRoot = Preferences.userRoot();
147 newNode = !userPrefsRoot.nodeExists(gameName);
148 } catch (BackingStoreException bse) { }
150 return new PreferencesGameSettings(
151 userPrefsRoot.node(gameName), newNode,
152 "game-defaults.properties");
154 /* To persist to a .properties file instead of java.util.prefs,
155 * subclass StandardGame with a getNewSettings method like this:
156 com.jme.system.PropertiesGameSettings pgs =
157 new com.jme.system.PropertiesGameSettings("pgs.properties");
163 public StandardGame(String gameName, GameType type, GameSettings settings, UncaughtExceptionHandler exceptionHandler) {
164 this.gameName = gameName;
166 this.settings = settings;
167 this.exceptionHandler = exceptionHandler;
168 backgroundColor = ColorRGBA.black.clone();
170 // if (this.settings == null) this.settings = getNewSettings();
171 // To load settings without displaying a Settings widget, enable
172 // the preceding if statement, and comment out the following if block.
173 if (this.settings == null) {
174 //setConfigShowMode(ConfigShowMode.AlwaysShow); // To override dflt.
179 updateLock = new ReentrantLock(true); // Make our lock be fair (first come, first serve)
182 public GameType getGameType() {
186 public void start() {
187 gameThread = new Thread(this);
188 if (exceptionHandler == null) {
189 exceptionHandler = new DefaultUncaughtExceptionHandler(this);
191 gameThread.setUncaughtExceptionHandler(exceptionHandler);
193 // Assign a name to the thread
194 gameThread.setName("OpenGL");
198 // Wait for main game loop before returning
200 while (!isStarted()) {
203 } catch (InterruptedException exc) {
204 logger.logp(Level.SEVERE, this.getClass().toString(), "start()", "Exception", exc);
211 if (type != GameType.HEADLESS) {
212 assertDisplayCreated();
214 // Default the mouse cursor to off
215 MouseInput.get().setCursorVisible(false);
219 if (type == GameType.GRAPHICAL) {
220 timer = Timer.getTimer();
221 } else if (type == GameType.HEADLESS) {
222 timer = new NanoTimer();
225 // Configure frame rate
226 int preferredFPS = settings.getFramerate();
227 long preferredTicksPerFrame = -1;
228 long frameStartTick = -1;
230 long frameDurationTicks = -1;
231 if (preferredFPS >= 0) {
232 preferredTicksPerFrame = Math.round((float)timer.getResolution() / (float)preferredFPS);
238 while ((!finished) && (!display.isClosing())) {
239 // Fixed framerate Start
240 if (preferredTicksPerFrame >= 0) {
241 frameStartTick = timer.getTime();
245 tpf = timer.getTimePerFrame();
247 if (type == GameType.GRAPHICAL) {
248 InputSystem.update();
252 display.getRenderer().displayBackBuffer();
254 // Fixed framerate End
255 if (preferredTicksPerFrame >= 0) {
257 frameDurationTicks = timer.getTime() - frameStartTick;
258 while (frameDurationTicks < preferredTicksPerFrame) {
259 long sleepTime = ((preferredTicksPerFrame - frameDurationTicks) * 1000) / timer.getResolution();
261 Thread.sleep(sleepTime);
262 } catch (InterruptedException exc) {
263 logger.log(Level.SEVERE,
264 "Interrupted while sleeping in fixed-framerate",
267 frameDurationTicks = timer.getTime() - frameStartTick;
269 if (frames == Long.MAX_VALUE) frames = 0;
272 if (THREAD_FRIENDLY) Thread.yield();
279 protected void initSystem() {
280 if (type == GameType.GRAPHICAL) {
282 // Configure Joystick
283 if (JoystickInput.getProvider() == null) {
284 JoystickInput.setProvider(InputSystem.INPUT_SYSTEM_LWJGL);
287 display = DisplaySystem.getDisplaySystem(settings.getRenderer());
290 display.setTitle(gameName);
292 display.setIcon( icons);
295 if (DISPLAY_MODE == DISPLAY_WINDOW) {
296 display.createWindow(settings.getWidth(), settings.getHeight(), settings.getDepth(), settings
297 .getFrequency(), settings.isFullscreen());
298 } else if (DISPLAY_MODE == DISPLAY_CANVAS) {
299 // XXX: included to preserve current functionality. Probably
300 // want to move this to prefs or the user of StandardGame.
301 if(JOGLSystemProvider.SYSTEM_IDENTIFIER.equals(settings.getRenderer()))
302 display.registerCanvasConstructor("AWT", JOGLAWTCanvasConstructor.class);
304 display.registerCanvasConstructor("AWT", LWJGLAWTCanvasConstructor.class);
305 canvas = (Canvas)display.createCanvas(settings.getWidth(), settings.getHeight());
307 camera = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
308 display.getRenderer().setBackgroundColor(backgroundColor);
310 // Setup Vertical Sync if enabled
311 display.setVSyncEnabled(settings.isVerticalSync());
317 display.getRenderer().setCamera(camera);
319 if ((settings.isMusic()) || (settings.isSFX())) {
323 display = DisplaySystem.getDisplaySystem(DummySystemProvider.DUMMY_SYSTEM_IDENTIFIER);
328 * The java.awt.Canvas if DISPLAY_CANVAS is the DISPLAY_MODE
333 public Canvas getCanvas() {
337 protected void initSound() {
338 AudioSystem.getSystem().getEar().trackOrientation(camera);
339 AudioSystem.getSystem().getEar().trackPosition(camera);
342 private void displayMins() {
343 display.setMinDepthBits(settings.getDepthBits());
344 display.setMinStencilBits(settings.getStencilBits());
345 display.setMinAlphaBits(settings.getAlphaBits());
346 display.setMinSamples(settings.getSamples());
349 private void cameraPerspective() {
350 camera.setFrustumPerspective(45.0f, (float)display.getWidth() / (float)display.getHeight(), 1.0f, 1000.0f);
351 camera.setParallelProjection(false);
355 private void cameraFrame() {
356 Vector3f loc = new Vector3f(0.0f, 0.0f, 25.0f);
357 Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
358 Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
359 Vector3f dir = new Vector3f(0.0f, 0.0f, -1.0f);
360 camera.setFrame(loc, left, up, dir);
363 public void resetCamera() {
367 protected void initGame() {
368 // Create the GameStateManager
369 GameStateManager.create();
372 protected void update(float interpolation) {
373 // Open the lock up for just a brief second
377 // Execute updateQueue item
378 GameTaskQueueManager.getManager().getQueue(GameTaskQueue.UPDATE).execute();
380 // Update the GameStates
381 GameStateManager.getInstance().update(interpolation);
383 if (type == GameType.GRAPHICAL) {
385 // Update music/sound
386 if ((settings.isMusic()) || (settings.isSFX())) {
387 AudioSystem.getSystem().update();
392 protected void render(float interpolation) {
393 display.getRenderer().clearBuffers();
395 // Execute renderQueue item
396 GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).execute();
398 // Render the GameStates
399 GameStateManager.getInstance().render(interpolation);
402 public void reinit() {
407 public void reinitAudio() {
408 if (AudioSystem.isCreated()) {
409 AudioSystem.getSystem().cleanup();
413 public void reinitVideo() {
414 GameTaskQueueManager.getManager().update(new Callable<Object>() {
415 public Object call() throws Exception {
418 display.recreateWindow(settings.getWidth(), settings
419 .getHeight(), settings.getDepth(), settings
420 .getFrequency(), settings.isFullscreen());
421 camera = display.getRenderer().createCamera(display.getWidth(),
422 display.getHeight());
423 display.getRenderer().setBackgroundColor(backgroundColor);
424 if ((settings.isMusic()) || (settings.isSFX())) {
432 public void recreateGraphicalContext() {
436 protected void cleanup() {
437 GameStateManager.getInstance().cleanup();
439 DisplaySystem.getDisplaySystem().getRenderer().cleanup();
440 TextureManager.doTextureCleanup();
441 TextureManager.clearCache();
443 JoystickInput.destroyIfInitalized();
444 if (AudioSystem.isCreated()) {
445 AudioSystem.getSystem().cleanup();
449 protected void quit() {
450 if (display != null) {
457 * The internally used <code>DisplaySystem</code> for this instance
458 * of <code>StandardGame</code>
465 public DisplaySystem getDisplay() {
470 * The internally used <code>Camera</code> for this instance of
471 * <code>StandardGame</code>.
478 public Camera getCamera() {
483 * The <code>GameSettings</code> implementation being utilized in
484 * this instance of <code>StandardGame</code>.
491 public GameSettings getSettings() {
496 * Override the background color defined for this game. The reinit() method
497 * must be invoked if the game is currently running before this will take effect.
499 * @param backgroundColor
501 public void setBackgroundColor(ColorRGBA backgroundColor) {
502 this.backgroundColor = backgroundColor;
506 * Gracefully shutdown the main game loop thread. This is a synonym
507 * for the finish() method but just sounds better.
511 public void shutdown() {
516 * Will return true if within the main game loop. This is particularly
517 * useful to determine if the game has finished the initialization but
518 * will also return false if the game has been terminated.
523 public boolean isStarted() {
528 * Specify the UncaughtExceptionHandler for circumstances where an exception in the
529 * OpenGL thread is not captured properly.
531 * @param exceptionHandler
533 public void setUncaughtExceptionHandler(UncaughtExceptionHandler exceptionHandler) {
534 this.exceptionHandler = exceptionHandler;
535 gameThread.setUncaughtExceptionHandler(this.exceptionHandler);
539 * Causes the current thread to wait for an update to occur in the OpenGL thread.
540 * This can be beneficial if there is work that has to be done in the OpenGL thread
541 * that needs to be completed before continuing in another thread.
543 * You can chain invocations of this together in order to wait for multiple updates.
545 * @throws InterruptedException
546 * @throws ExecutionException
548 public void delayForUpdate() throws InterruptedException, ExecutionException {
549 Future<Object> f = GameTaskQueueManager.getManager().update(new Callable<Object>() {
550 public Object call() throws Exception {
558 * Convenience method to let you know if the thread you're in is the OpenGL thread
561 * true if, and only if, the current thread is the OpenGL thread
563 public boolean inGLThread() {
564 if (Thread.currentThread() == gameThread) {
571 * Convenience method that will make sure <code>callable</code> is executed in the
572 * OpenGL thread. If it is already in the OpenGL thread when this method is invoked
573 * it will be executed and returned immediately. Otherwise, it will be put into the
574 * GameTaskQueue and executed in the next update. This is a blocking method and will
575 * wait for the successful return of <code>callable</code> before returning.
579 * @return result of callable.get()
582 public <T> T executeInGL(Callable<T> callable) throws Exception {
584 return callable.call();
586 Future<T> future = GameTaskQueueManager.getManager().update(callable);
591 * Will wait for a lock at the beginning of the OpenGL update method. Once this method returns the
592 * OpenGL thread is blocked until the lock is released (via unlock()). If another thread currently
593 * has a lock or it is currently in the process of an update the calling thread will be blocked until
594 * the lock is successfully established.
601 * Used in conjunction with lock() in order to release a previously assigned lock on the OpenGL thread.
602 * This <b>MUST</b> be executed within the same thread that called lock() in the first place or the lock
603 * will not be released.
605 public void unlock() {
609 public void setIcons( Image[] icons) {
614 class DefaultUncaughtExceptionHandler implements UncaughtExceptionHandler {
615 private static final Logger logger = Logger
616 .getLogger(DefaultUncaughtExceptionHandler.class.getName());
618 private StandardGame game;
620 public DefaultUncaughtExceptionHandler(StandardGame game) {
624 public void uncaughtException(Thread t, Throwable e) {
625 logger.log(Level.SEVERE, "Main game loop broken by uncaught exception", e);