OSDN Git Service

Set optimal mime types and executable settings.
[mikumikustudio/MikuMikuStudio.git] / src / com / jmex / game / StandardGame.java
1 /*
2  * Copyright (c) 2003-2009 jMonkeyEngine
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  * * Redistributions of source code must retain the above copyright
10  *   notice, this list of conditions and the following disclaimer.
11  *
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.
15  *
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.
19  *
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.
31  */
32 // $Id$
33 package com.jmex.game;
34
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;
46
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;
69
70 /**
71  * A game that implements all of the basic functionality that you will need.
72  * <p>
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.
75  * <p>
76  * {@code StandardGame} provides the following features to ease game development:
77  * 
78  * <ul>
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>
84  * </ul>
85  * 
86  * as well as re-initialisation of the graphical context (if settings change
87  * for example) and everything else a typical game requires.
88  * <p>
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
94  * start-up lag.
95  * 
96  * @author Matthew D. Hicks
97  * @version $Revision$, $Date$
98  */
99 public final class StandardGame extends AbstractGame implements Runnable {
100     private static final Logger logger = Logger.getLogger(StandardGame.class
101             .getName());
102     
103     public static final int DISPLAY_WINDOW = 1;
104     public static final int DISPLAY_CANVAS = 2;
105     
106     public static boolean THREAD_FRIENDLY = true;
107     public static int DISPLAY_MODE = DISPLAY_WINDOW;
108     
109     public static enum GameType {
110         GRAPHICAL, HEADLESS
111     }
112
113     private Thread gameThread;
114     private String gameName;
115     private GameType type;
116     private boolean started;
117     private Image[] icons;
118     
119     private Timer timer;
120     private Camera camera;
121     private ColorRGBA backgroundColor;
122     private UncaughtExceptionHandler exceptionHandler;
123     
124     private Canvas canvas;
125
126     private Lock updateLock;
127     
128     public StandardGame(String gameName) {
129         this(gameName, GameType.GRAPHICAL, null);
130     }
131
132     public StandardGame(String gameName, GameType type) {
133         this(gameName, type, null);
134     }
135
136     public StandardGame(String gameName, GameType type, GameSettings settings) {
137         this(gameName, type, settings, null);
138     }
139
140     /**
141      * @see AbstractGame#getNewSettings()
142      */
143     protected GameSettings getNewSettings() {
144         boolean newNode = true;
145         Preferences userPrefsRoot = Preferences.userRoot();
146         try {
147             newNode = !userPrefsRoot.nodeExists(gameName);
148         } catch (BackingStoreException bse) { }
149
150         return new PreferencesGameSettings(
151                 userPrefsRoot.node(gameName), newNode,
152                 "game-defaults.properties");
153
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");
158         pgs.load();
159         return pgs;
160         */
161     }
162
163     public StandardGame(String gameName, GameType type, GameSettings settings, UncaughtExceptionHandler exceptionHandler) {
164         this.gameName = gameName;
165         this.type = type;
166         this.settings = settings;
167         this.exceptionHandler = exceptionHandler;
168         backgroundColor = ColorRGBA.black.clone();
169
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.
175             getAttributes();
176         }
177
178         // Create Lock
179         updateLock = new ReentrantLock(true); // Make our lock be fair (first come, first serve)
180     }
181
182     public GameType getGameType() {
183         return type;
184     }
185
186     public void start() {
187         gameThread = new Thread(this);
188         if (exceptionHandler == null) {
189             exceptionHandler = new DefaultUncaughtExceptionHandler(this);
190         }
191         gameThread.setUncaughtExceptionHandler(exceptionHandler);
192         
193         // Assign a name to the thread
194         gameThread.setName("OpenGL");
195         
196         gameThread.start();
197
198         // Wait for main game loop before returning
199         try {
200             while (!isStarted()) {
201                 Thread.sleep(1);
202             }
203         } catch (InterruptedException exc) {
204             logger.logp(Level.SEVERE, this.getClass().toString(), "start()", "Exception", exc);
205         }
206     }
207
208     public void run() {
209         lock();
210         initSystem();
211         if (type != GameType.HEADLESS) {
212             assertDisplayCreated();
213             
214             // Default the mouse cursor to off
215             MouseInput.get().setCursorVisible(false);
216         }
217         
218         initGame();
219         if (type == GameType.GRAPHICAL) {
220             timer = Timer.getTimer();
221         } else if (type == GameType.HEADLESS) {
222             timer = new NanoTimer();
223         }
224
225         // Configure frame rate
226         int preferredFPS = settings.getFramerate();
227         long preferredTicksPerFrame = -1;
228         long frameStartTick = -1;
229         long frames = 0;
230         long frameDurationTicks = -1;
231         if (preferredFPS >= 0) {
232             preferredTicksPerFrame = Math.round((float)timer.getResolution() / (float)preferredFPS);
233         }
234
235         // Main game loop
236         float tpf;
237         started = true;
238         while ((!finished) && (!display.isClosing())) {
239             // Fixed framerate Start
240             if (preferredTicksPerFrame >= 0) {
241                 frameStartTick = timer.getTime();
242             }
243
244             timer.update();
245             tpf = timer.getTimePerFrame();
246
247             if (type == GameType.GRAPHICAL) {
248                 InputSystem.update();
249             }
250             update(tpf);
251             render(tpf);
252             display.getRenderer().displayBackBuffer();
253
254             // Fixed framerate End
255             if (preferredTicksPerFrame >= 0) {
256                 frames++;
257                 frameDurationTicks = timer.getTime() - frameStartTick;
258                 while (frameDurationTicks < preferredTicksPerFrame) {
259                     long sleepTime = ((preferredTicksPerFrame - frameDurationTicks) * 1000) / timer.getResolution();
260                     try {
261                         Thread.sleep(sleepTime);
262                     } catch (InterruptedException exc) {
263                         logger.log(Level.SEVERE,
264                                    "Interrupted while sleeping in fixed-framerate",
265                                    exc);
266                     }
267                     frameDurationTicks = timer.getTime() - frameStartTick;
268                 }
269                 if (frames == Long.MAX_VALUE) frames = 0;
270             }
271
272             if (THREAD_FRIENDLY) Thread.yield();
273         }
274         started = false;
275         cleanup();
276         quit();
277     }
278
279     protected void initSystem() {
280         if (type == GameType.GRAPHICAL) {
281
282             // Configure Joystick
283             if (JoystickInput.getProvider() == null) {
284                 JoystickInput.setProvider(InputSystem.INPUT_SYSTEM_LWJGL);
285             }
286
287             display = DisplaySystem.getDisplaySystem(settings.getRenderer());
288             displayMins();
289             
290             display.setTitle(gameName);
291             if( icons != null) {
292                 display.setIcon( icons);
293             }
294
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);
303                 else
304                     display.registerCanvasConstructor("AWT", LWJGLAWTCanvasConstructor.class);
305                 canvas = (Canvas)display.createCanvas(settings.getWidth(), settings.getHeight());
306             }
307             camera = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
308             display.getRenderer().setBackgroundColor(backgroundColor);
309
310             // Setup Vertical Sync if enabled
311             display.setVSyncEnabled(settings.isVerticalSync());
312
313             // Configure Camera
314             cameraPerspective();
315             cameraFrame();
316             camera.update();
317             display.getRenderer().setCamera(camera);
318
319             if ((settings.isMusic()) || (settings.isSFX())) {
320                 initSound();
321             }
322         } else {
323                 display = DisplaySystem.getDisplaySystem(DummySystemProvider.DUMMY_SYSTEM_IDENTIFIER);
324         }
325     }
326
327     /**
328      * The java.awt.Canvas if DISPLAY_CANVAS is the DISPLAY_MODE
329      * 
330      * @return
331      *      Canvas
332      */
333     public Canvas getCanvas() {
334         return canvas;
335     }
336     
337     protected void initSound() {
338         AudioSystem.getSystem().getEar().trackOrientation(camera);
339         AudioSystem.getSystem().getEar().trackPosition(camera);
340     }
341
342     private void displayMins() {
343         display.setMinDepthBits(settings.getDepthBits());
344         display.setMinStencilBits(settings.getStencilBits());
345         display.setMinAlphaBits(settings.getAlphaBits());
346         display.setMinSamples(settings.getSamples());
347     }
348
349     private void cameraPerspective() {
350         camera.setFrustumPerspective(45.0f, (float)display.getWidth() / (float)display.getHeight(), 1.0f, 1000.0f);
351         camera.setParallelProjection(false);
352         camera.update();
353     }
354
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);
361     }
362     
363     public void resetCamera() {
364         cameraFrame();
365     }
366
367     protected void initGame() {
368         // Create the GameStateManager
369         GameStateManager.create();
370     }
371
372     protected void update(float interpolation) {
373         // Open the lock up for just a brief second
374         unlock();
375         lock();
376
377         // Execute updateQueue item
378         GameTaskQueueManager.getManager().getQueue(GameTaskQueue.UPDATE).execute();
379
380         // Update the GameStates
381         GameStateManager.getInstance().update(interpolation);
382
383         if (type == GameType.GRAPHICAL) {
384
385             // Update music/sound
386             if ((settings.isMusic()) || (settings.isSFX())) {
387                 AudioSystem.getSystem().update();
388             }
389         }
390     }
391
392     protected void render(float interpolation) {
393         display.getRenderer().clearBuffers();
394
395         // Execute renderQueue item
396         GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).execute();
397
398         // Render the GameStates
399         GameStateManager.getInstance().render(interpolation);
400     }
401     
402     public void reinit() {
403         reinitAudio();
404         reinitVideo();
405     }
406     
407     public void reinitAudio() {
408         if (AudioSystem.isCreated()) {
409             AudioSystem.getSystem().cleanup();
410         }
411     }
412
413         public void reinitVideo() {
414                 GameTaskQueueManager.getManager().update(new Callable<Object>() {
415                         public Object call() throws Exception {
416                                 displayMins();
417
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())) {
425                                         initSound();
426                                 }
427                                 return null;
428                         }
429                 });
430         }
431
432         public void recreateGraphicalContext() {
433                 reinit();
434         }
435
436     protected void cleanup() {
437         GameStateManager.getInstance().cleanup();
438         
439         DisplaySystem.getDisplaySystem().getRenderer().cleanup();
440         TextureManager.doTextureCleanup();
441         TextureManager.clearCache();
442         
443         JoystickInput.destroyIfInitalized();
444         if (AudioSystem.isCreated()) {
445             AudioSystem.getSystem().cleanup();
446         }
447     }
448
449     protected void quit() {
450         if (display != null) {
451             display.reset();
452             display.close();
453         }
454     }
455
456     /**
457      * The internally used <code>DisplaySystem</code> for this instance
458      * of <code>StandardGame</code>
459      * 
460      * @return
461      *      DisplaySystem
462      * 
463      * @see DisplaySystem
464      */
465     public DisplaySystem getDisplay() {
466         return display;
467     }
468
469     /**
470      * The internally used <code>Camera</code> for this instance of
471      * <code>StandardGame</code>.
472      * 
473      * @return
474      *      Camera
475      *      
476      * @see Camera
477      */
478     public Camera getCamera() {
479         return camera;
480     }
481
482     /**
483      * The <code>GameSettings</code> implementation being utilized in
484      * this instance of <code>StandardGame</code>.
485      * 
486      * @return
487      *      GameSettings
488      *      
489      * @see GameSettings
490      */
491     public GameSettings getSettings() {
492         return settings;
493     }
494
495     /**
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.
498      * 
499      * @param backgroundColor
500      */
501     public void setBackgroundColor(ColorRGBA backgroundColor) {
502         this.backgroundColor = backgroundColor;
503     }
504
505     /**
506      * Gracefully shutdown the main game loop thread. This is a synonym
507      * for the finish() method but just sounds better.
508      * 
509      * @see #finish()
510      */
511     public void shutdown() {
512         finish();
513     }
514
515     /**
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.
519      * 
520      * @return
521      *      boolean
522      */
523     public boolean isStarted() {
524         return started;
525     }
526
527     /**
528      * Specify the UncaughtExceptionHandler for circumstances where an exception in the
529      * OpenGL thread is not captured properly.
530      * 
531      * @param exceptionHandler
532      */
533     public void setUncaughtExceptionHandler(UncaughtExceptionHandler exceptionHandler) {
534         this.exceptionHandler = exceptionHandler;
535         gameThread.setUncaughtExceptionHandler(this.exceptionHandler);
536     }
537
538     /**
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.
542      * 
543      * You can chain invocations of this together in order to wait for multiple updates.
544      * 
545      * @throws InterruptedException
546      * @throws ExecutionException
547      */
548     public void delayForUpdate() throws InterruptedException, ExecutionException {
549         Future<Object> f = GameTaskQueueManager.getManager().update(new Callable<Object>() {
550             public Object call() throws Exception {
551                 return null;
552             }
553         });
554         f.get();
555     }
556
557     /**
558      * Convenience method to let you know if the thread you're in is the OpenGL thread
559      * 
560      * @return
561      *      true if, and only if, the current thread is the OpenGL thread
562      */
563     public boolean inGLThread() {
564         if (Thread.currentThread() == gameThread) {
565             return true;
566         }
567         return false;
568     }
569
570     /**
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.
576      * 
577      * @param <T>
578      * @param callable
579      * @return result of callable.get()
580      * @throws Exception
581      */
582     public <T> T executeInGL(Callable<T> callable) throws Exception {
583         if (inGLThread()) {
584             return callable.call();
585         }
586         Future<T> future = GameTaskQueueManager.getManager().update(callable);
587         return future.get();
588     }
589
590     /**
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.
595      */
596     public void lock() {
597         updateLock.lock();
598     }
599
600     /**
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.
604      */
605     public void unlock() {
606         updateLock.unlock();
607     }
608
609     public void setIcons( Image[] icons) {
610         this.icons = icons;
611     }
612 }
613
614 class DefaultUncaughtExceptionHandler implements UncaughtExceptionHandler {
615     private static final Logger logger = Logger
616             .getLogger(DefaultUncaughtExceptionHandler.class.getName());
617     
618     private StandardGame game;
619
620     public DefaultUncaughtExceptionHandler(StandardGame game) {
621         this.game = game;
622     }
623
624     public void uncaughtException(Thread t, Throwable e) {
625         logger.log(Level.SEVERE, "Main game loop broken by uncaught exception", e);
626         game.shutdown();
627         game.cleanup();
628         game.quit();
629     }
630 }