1/*
2 * Copyright (c) 2009-2010 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
33package com.jme3.app;
34
35import com.jme3.app.state.AppStateManager;
36import com.jme3.asset.AssetManager;
37import com.jme3.audio.AudioContext;
38import com.jme3.audio.AudioRenderer;
39import com.jme3.audio.Listener;
40import com.jme3.input.*;
41import com.jme3.math.Vector3f;
42import com.jme3.renderer.Camera;
43import com.jme3.renderer.RenderManager;
44import com.jme3.renderer.Renderer;
45import com.jme3.renderer.ViewPort;
46import com.jme3.system.JmeContext.Type;
47import com.jme3.system.*;
48import java.net.MalformedURLException;
49import java.net.URL;
50import java.util.concurrent.Callable;
51import java.util.concurrent.ConcurrentLinkedQueue;
52import java.util.concurrent.Future;
53import java.util.logging.Level;
54import java.util.logging.Logger;
55
56/**
57 * The <code>Application</code> class represents an instance of a
58 * real-time 3D rendering jME application.
59 *
60 * An <code>Application</code> provides all the tools that are commonly used in jME3
61 * applications.
62 *
63 * jME3 applications should extend this class and call start() to begin the
64 * application.
65 *
66 */
67public class Application implements SystemListener {
68
69    private static final Logger logger = Logger.getLogger(Application.class.getName());
70
71    protected AssetManager assetManager;
72
73    protected AudioRenderer audioRenderer;
74    protected Renderer renderer;
75    protected RenderManager renderManager;
76    protected ViewPort viewPort;
77    protected ViewPort guiViewPort;
78
79    protected JmeContext context;
80    protected AppSettings settings;
81    protected Timer timer = new NanoTimer();
82    protected Camera cam;
83    protected Listener listener;
84
85    protected boolean inputEnabled = true;
86    protected boolean pauseOnFocus = true;
87    protected float speed = 1f;
88    protected boolean paused = false;
89    protected MouseInput mouseInput;
90    protected KeyInput keyInput;
91    protected JoyInput joyInput;
92    protected TouchInput touchInput;
93    protected InputManager inputManager;
94    protected AppStateManager stateManager;
95
96    private final ConcurrentLinkedQueue<AppTask<?>> taskQueue = new ConcurrentLinkedQueue<AppTask<?>>();
97
98    /**
99     * Create a new instance of <code>Application</code>.
100     */
101    public Application(){
102        initStateManager();
103    }
104
105    /**
106     * Returns true if pause on lost focus is enabled, false otherwise.
107     *
108     * @return true if pause on lost focus is enabled
109     *
110     * @see #setPauseOnLostFocus(boolean)
111     */
112    public boolean isPauseOnLostFocus() {
113        return pauseOnFocus;
114    }
115
116    /**
117     * Enable or disable pause on lost focus.
118     * <p>
119     * By default, pause on lost focus is enabled.
120     * If enabled, the application will stop updating
121     * when it loses focus or becomes inactive (e.g. alt-tab).
122     * For online or real-time applications, this might not be preferable,
123     * so this feature should be set to disabled. For other applications,
124     * it is best to keep it on so that CPU usage is not used when
125     * not necessary.
126     *
127     * @param pauseOnLostFocus True to enable pause on lost focus, false
128     * otherwise.
129     */
130    public void setPauseOnLostFocus(boolean pauseOnLostFocus) {
131        this.pauseOnFocus = pauseOnLostFocus;
132    }
133
134    @Deprecated
135    public void setAssetManager(AssetManager assetManager){
136        if (this.assetManager != null)
137            throw new IllegalStateException("Can only set asset manager"
138                                          + " before initialization.");
139
140        this.assetManager = assetManager;
141    }
142
143    private void initAssetManager(){
144        if (settings != null){
145            String assetCfg = settings.getString("AssetConfigURL");
146            if (assetCfg != null){
147                URL url = null;
148                try {
149                    url = new URL(assetCfg);
150                } catch (MalformedURLException ex) {
151                }
152                if (url == null) {
153                    url = Application.class.getClassLoader().getResource(assetCfg);
154                    if (url == null) {
155                        logger.log(Level.SEVERE, "Unable to access AssetConfigURL in asset config:{0}", assetCfg);
156                        return;
157                    }
158                }
159                assetManager = JmeSystem.newAssetManager(url);
160            }
161        }
162        if (assetManager == null){
163            assetManager = JmeSystem.newAssetManager(
164                    Thread.currentThread().getContextClassLoader()
165                    .getResource("com/jme3/asset/Desktop.cfg"));
166        }
167    }
168
169    /**
170     * Set the display settings to define the display created.
171     * <p>
172     * Examples of display parameters include display pixel width and height,
173     * color bit depth, z-buffer bits, anti-aliasing samples, and update frequency.
174     * If this method is called while the application is already running, then
175     * {@link #restart() } must be called to apply the settings to the display.
176     *
177     * @param settings The settings to set.
178     */
179    public void setSettings(AppSettings settings){
180        this.settings = settings;
181        if (context != null && settings.useInput() != inputEnabled){
182            // may need to create or destroy input based
183            // on settings change
184            inputEnabled = !inputEnabled;
185            if (inputEnabled){
186                initInput();
187            }else{
188                destroyInput();
189            }
190        }else{
191            inputEnabled = settings.useInput();
192        }
193    }
194
195    /**
196     * Sets the Timer implementation that will be used for calculating
197     * frame times.  By default, Application will use the Timer as returned
198     * by the current JmeContext implementation.
199     */
200    public void setTimer(Timer timer){
201        this.timer = timer;
202
203        if (timer != null) {
204            timer.reset();
205        }
206
207        if (renderManager != null) {
208            renderManager.setTimer(timer);
209        }
210    }
211
212    public Timer getTimer(){
213        return timer;
214    }
215
216    private void initDisplay(){
217        // aquire important objects
218        // from the context
219        settings = context.getSettings();
220
221        // Only reset the timer if a user has not already provided one
222        if (timer == null) {
223            timer = context.getTimer();
224        }
225
226        renderer = context.getRenderer();
227    }
228
229    private void initAudio(){
230        if (settings.getAudioRenderer() != null && context.getType() != Type.Headless){
231            audioRenderer = JmeSystem.newAudioRenderer(settings);
232            audioRenderer.initialize();
233            AudioContext.setAudioRenderer(audioRenderer);
234
235            listener = new Listener();
236            audioRenderer.setListener(listener);
237        }
238    }
239
240    /**
241     * Creates the camera to use for rendering. Default values are perspective
242     * projection with 45° field of view, with near and far values 1 and 1000
243     * units respectively.
244     */
245    private void initCamera(){
246        cam = new Camera(settings.getWidth(), settings.getHeight());
247
248        cam.setFrustumPerspective(45f, (float)cam.getWidth() / cam.getHeight(), 1f, 1000f);
249        cam.setLocation(new Vector3f(0f, 0f, 10f));
250        cam.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);
251
252        renderManager = new RenderManager(renderer);
253        //Remy - 09/14/2010 setted the timer in the renderManager
254        renderManager.setTimer(timer);
255        viewPort = renderManager.createMainView("Default", cam);
256        viewPort.setClearFlags(true, true, true);
257
258        // Create a new cam for the gui
259        Camera guiCam = new Camera(settings.getWidth(), settings.getHeight());
260        guiViewPort = renderManager.createPostView("Gui Default", guiCam);
261        guiViewPort.setClearFlags(false, false, false);
262    }
263
264    /**
265     * Initializes mouse and keyboard input. Also
266     * initializes joystick input if joysticks are enabled in the
267     * AppSettings.
268     */
269    private void initInput(){
270        mouseInput = context.getMouseInput();
271        if (mouseInput != null)
272            mouseInput.initialize();
273
274        keyInput = context.getKeyInput();
275        if (keyInput != null)
276            keyInput.initialize();
277
278        touchInput = context.getTouchInput();
279        if (touchInput != null)
280            touchInput.initialize();
281
282        if (!settings.getBoolean("DisableJoysticks")){
283            joyInput = context.getJoyInput();
284            if (joyInput != null)
285                joyInput.initialize();
286        }
287
288        inputManager = new InputManager(mouseInput, keyInput, joyInput, touchInput);
289    }
290
291    private void initStateManager(){
292        stateManager = new AppStateManager(this);
293
294        // Always register a ResetStatsState to make sure
295        // that the stats are cleared every frame
296        stateManager.attach(new ResetStatsState());
297    }
298
299    /**
300     * @return The {@link AssetManager asset manager} for this application.
301     */
302    public AssetManager getAssetManager(){
303        return assetManager;
304    }
305
306    /**
307     * @return the {@link InputManager input manager}.
308     */
309    public InputManager getInputManager(){
310        return inputManager;
311    }
312
313    /**
314     * @return the {@link AppStateManager app state manager}
315     */
316    public AppStateManager getStateManager() {
317        return stateManager;
318    }
319
320    /**
321     * @return the {@link RenderManager render manager}
322     */
323    public RenderManager getRenderManager() {
324        return renderManager;
325    }
326
327    /**
328     * @return The {@link Renderer renderer} for the application
329     */
330    public Renderer getRenderer(){
331        return renderer;
332    }
333
334    /**
335     * @return The {@link AudioRenderer audio renderer} for the application
336     */
337    public AudioRenderer getAudioRenderer() {
338        return audioRenderer;
339    }
340
341    /**
342     * @return The {@link Listener listener} object for audio
343     */
344    public Listener getListener() {
345        return listener;
346    }
347
348    /**
349     * @return The {@link JmeContext display context} for the application
350     */
351    public JmeContext getContext(){
352        return context;
353    }
354
355    /**
356     * @return The {@link Camera camera} for the application
357     */
358    public Camera getCamera(){
359        return cam;
360    }
361
362    /**
363     * Starts the application in {@link Type#Display display} mode.
364     *
365     * @see #start(com.jme3.system.JmeContext.Type)
366     */
367    public void start(){
368        start(JmeContext.Type.Display);
369    }
370
371    /**
372     * Starts the application.
373     * Creating a rendering context and executing
374     * the main loop in a separate thread.
375     */
376    public void start(JmeContext.Type contextType){
377        if (context != null && context.isCreated()){
378            logger.warning("start() called when application already created!");
379            return;
380        }
381
382        if (settings == null){
383            settings = new AppSettings(true);
384        }
385
386        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
387        context = JmeSystem.newContext(settings, contextType);
388        context.setSystemListener(this);
389        context.create(false);
390    }
391
392    /**
393     * Initializes the application's canvas for use.
394     * <p>
395     * After calling this method, cast the {@link #getContext() context} to
396     * {@link JmeCanvasContext},
397     * then acquire the canvas with {@link JmeCanvasContext#getCanvas() }
398     * and attach it to an AWT/Swing Frame.
399     * The rendering thread will start when the canvas becomes visible on
400     * screen, however if you wish to start the context immediately you
401     * may call {@link #startCanvas() } to force the rendering thread
402     * to start.
403     *
404     * @see JmeCanvasContext
405     * @see Type#Canvas
406     */
407    public void createCanvas(){
408        if (context != null && context.isCreated()){
409            logger.warning("createCanvas() called when application already created!");
410            return;
411        }
412
413        if (settings == null){
414            settings = new AppSettings(true);
415        }
416
417        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
418        context = JmeSystem.newContext(settings, JmeContext.Type.Canvas);
419        context.setSystemListener(this);
420    }
421
422    /**
423     * Starts the rendering thread after createCanvas() has been called.
424     * <p>
425     * Same as calling startCanvas(false)
426     *
427     * @see #startCanvas(boolean)
428     */
429    public void startCanvas(){
430        startCanvas(false);
431    }
432
433    /**
434     * Starts the rendering thread after createCanvas() has been called.
435     * <p>
436     * Calling this method is optional, the canvas will start automatically
437     * when it becomes visible.
438     *
439     * @param waitFor If true, the current thread will block until the
440     * rendering thread is running
441     */
442    public void startCanvas(boolean waitFor){
443        context.create(waitFor);
444    }
445
446    /**
447     * Internal use only.
448     */
449    public void reshape(int w, int h){
450        renderManager.notifyReshape(w, h);
451    }
452
453    /**
454     * Restarts the context, applying any changed settings.
455     * <p>
456     * Changes to the {@link AppSettings} of this Application are not
457     * applied immediately; calling this method forces the context
458     * to restart, applying the new settings.
459     */
460    public void restart(){
461        context.setSettings(settings);
462        context.restart();
463    }
464
465    /**
466     * Requests the context to close, shutting down the main loop
467     * and making necessary cleanup operations.
468     *
469     * Same as calling stop(false)
470     *
471     * @see #stop(boolean)
472     */
473    public void stop(){
474        stop(false);
475    }
476
477    /**
478     * Requests the context to close, shutting down the main loop
479     * and making necessary cleanup operations.
480     * After the application has stopped, it cannot be used anymore.
481     */
482    public void stop(boolean waitFor){
483        logger.log(Level.FINE, "Closing application: {0}", getClass().getName());
484        context.destroy(waitFor);
485    }
486
487    /**
488     * Do not call manually.
489     * Callback from ContextListener.
490     * <p>
491     * Initializes the <code>Application</code>, by creating a display and
492     * default camera. If display settings are not specified, a default
493     * 640x480 display is created. Default values are used for the camera;
494     * perspective projection with 45° field of view, with near
495     * and far values 1 and 1000 units respectively.
496     */
497    public void initialize(){
498        if (assetManager == null){
499            initAssetManager();
500        }
501
502        initDisplay();
503        initCamera();
504
505        if (inputEnabled){
506            initInput();
507        }
508        initAudio();
509
510        // update timer so that the next delta is not too large
511//        timer.update();
512        timer.reset();
513
514        // user code here..
515    }
516
517    /**
518     * Internal use only.
519     */
520    public void handleError(String errMsg, Throwable t){
521        logger.log(Level.SEVERE, errMsg, t);
522        // user should add additional code to handle the error.
523        stop(); // stop the application
524    }
525
526    /**
527     * Internal use only.
528     */
529    public void gainFocus(){
530        if (pauseOnFocus) {
531            paused = false;
532            context.setAutoFlushFrames(true);
533            if (inputManager != null) {
534                inputManager.reset();
535            }
536        }
537    }
538
539    /**
540     * Internal use only.
541     */
542    public void loseFocus(){
543        if (pauseOnFocus){
544            paused = true;
545            context.setAutoFlushFrames(false);
546        }
547    }
548
549    /**
550     * Internal use only.
551     */
552    public void requestClose(boolean esc){
553        context.destroy(false);
554    }
555
556    /**
557     * Enqueues a task/callable object to execute in the jME3
558     * rendering thread.
559     * <p>
560     * Callables are executed right at the beginning of the main loop.
561     * They are executed even if the application is currently paused
562     * or out of focus.
563     */
564    public <V> Future<V> enqueue(Callable<V> callable) {
565        AppTask<V> task = new AppTask<V>(callable);
566        taskQueue.add(task);
567        return task;
568    }
569
570    /**
571     * Do not call manually.
572     * Callback from ContextListener.
573     */
574    public void update(){
575        // Make sure the audio renderer is available to callables
576        AudioContext.setAudioRenderer(audioRenderer);
577
578        AppTask<?> task = taskQueue.poll();
579        toploop: do {
580            if (task == null) break;
581            while (task.isCancelled()) {
582                task = taskQueue.poll();
583                if (task == null) break toploop;
584            }
585            task.invoke();
586        } while (((task = taskQueue.poll()) != null));
587
588        /* I think the above is really just doing this:
589        AppTask<?> task;
590        while( (task = taskQueue.poll()) != null ) {
591            if (!task.isCancelled()) {
592                task.invoke();
593            }
594        }
595        //...but it's hard to say for sure.  It's so twisted
596        //up that I don't trust my eyes.  -pspeed
597        */
598
599        if (speed == 0 || paused)
600            return;
601
602        timer.update();
603
604        if (inputEnabled){
605            inputManager.update(timer.getTimePerFrame());
606        }
607
608        if (audioRenderer != null){
609            audioRenderer.update(timer.getTimePerFrame());
610        }
611
612        // user code here..
613    }
614
615    protected void destroyInput(){
616        if (mouseInput != null)
617            mouseInput.destroy();
618
619        if (keyInput != null)
620            keyInput.destroy();
621
622        if (joyInput != null)
623            joyInput.destroy();
624
625        if (touchInput != null)
626            touchInput.destroy();
627
628        inputManager = null;
629    }
630
631    /**
632     * Do not call manually.
633     * Callback from ContextListener.
634     */
635    public void destroy(){
636        stateManager.cleanup();
637
638        destroyInput();
639        if (audioRenderer != null)
640            audioRenderer.cleanup();
641
642        timer.reset();
643    }
644
645    /**
646     * @return The GUI viewport. Which is used for the on screen
647     * statistics and FPS.
648     */
649    public ViewPort getGuiViewPort() {
650        return guiViewPort;
651    }
652
653    public ViewPort getViewPort() {
654        return viewPort;
655    }
656
657}
658