API Docs for: 0.5.5
Show:

File: kick/core/Engine.js

define(["require", "./GLState", "./Project", "./Constants", "./ResourceLoader", "./MouseInput", "./KeyInput", "./Config", "./Util", "./EventQueue", "kick/scene/Scene", "kick/math", "./Time", "./WebGLDebugUtils", "./EngineSingleton", "./Observable", "./Shim"],
    function (require, GLState, Project, Constants, ResourceLoader, MouseInput, KeyInput, Config, Util, EventQueue, Scene, math, Time, WebGLDebugUtils, EngineSingleton, Observable, Shim_NotUsed) {
        "use strict";

        var ASSERT = Constants._ASSERT,
            engineInstance = null,
            engine;

        /**
         * @module kick.core
         */

        /**
         * Game engine object
         * @example
         *      <canvas id="3dCanvas" width="50" height="50"></canvas>
         *      <script src="require.js"></script>
         *      <script type="text/javascript">
         *          var req = require.config({
         *                  paths: {
         *                      kick: 'kick-debug' // loads kick-debug.js (must be in same path)
         *                  }
         *              });
         *          req(['kick'],
         *                  function (kick) {
         *                      // init engine (create 3d context)
         *                      var engine = new kick.core.Engine('3dCanvas');
         *                  }
         *          );
         *      </script>
         * @class Engine
         * @namespace kick.core
         * @constructor
         * @param {String|canvas} idOrElement elementid of canvas tag or the canvas element
         * @param {kick.core.Config} config={} Configuration object
         */
        engine = function (idOrElement, config) {
            var glState,
                gl = null,
                canvas = typeof idOrElement === 'string' ? document.getElementById(idOrElement) : idOrElement,
                webGlContextNames = ["experimental-webgl", "webgl"],
                thisObj = this,
                lastTime = Date.now() - 16, // ensures valid delta time in next frame
                deltaTime = 0,
                timeObj = new Time(),
                timeSinceStart = 0,
                frame = 0,
                timeScale = 1,
                eventQueue,
                project = new Project(this),
                mouseInput = null,
                keyInput = null,
                activeScene,
                canvasSize = new Float32Array(2),
                activeSceneNull = {updateAndRender: function () {}},
                animationFrameObj = {},
                wrapperFunctionToMethodOnObject = function (time_) {
                    if (time_ < 1e12) { // if highres timer. see http://updates.html5rocks.com/2012/05/requestAnimationFrame-API-now-with-sub-millisecond-precision
                        time_ = Date.now();
                    }
                    thisObj._gameLoop(time_);
                };

            Observable.call(this, [
                /**
                 * Fired when gl-context is lost
                 * @event contextLost
                 */
                "contextLost",
                /**
                 * Fired when gl-context is restored (after context has been lost).
                 * @event contextRestored
                 * @param {WebGLRenderingContext} glContext
                 */
                "contextRestored",
                /**
                 * Fired before script updates methods has been run invoked
                 * @event preUpdateListener
                 */
                "preUpdateListener",
                /**
                 * Fired after script updates methods has been run invoked
                 * @event postUpdateListener
                 */
                "postUpdateListener",
                /**
                 * Fired when canvas is resized.
                 * @event canvasResized
                 * @param {kick.math.Vec2} newCanvasDimensions
                 */
                "canvasResized"
            ]
            );

            Object.defineProperties(this, {
                /**
                 * The current version of KickJS
                 * @property version
                 * @type String
                 * @final
                 */
                version: {
                    value: Constants._VERSION
                },
                /**
                 * Resource manager of the engine. Loads and cache resources.
                 * @property resourceLoader
                 * @type kick.core.ResourceLoader
                 * @final
                 */
                resourceLoader: {
                    value: new ResourceLoader(thisObj)
                },
                /**
                 * Project describes the resources available for a given projects (such as Scenes, Materials, Shader and Meshes)
                 * @property project
                 * @type kick.core.Project
                 * @final
                 */
                project: {
                    value: project
                },
                /**
                 * The WebGL state(readonly). (Only used to keep track on webgl state across different objects)
                 * @property glState
                 * @type kick.core.GLState
                 * @protected
                 * @final
                 */
                glState: {
                    get: function () { return glState; }
                },
                /**
                 * The WebGL context (readonly)
                 * @property gl
                 * @type WebGLRenderingContext
                 * @final
                 */
                gl: {
                    get: function () { return gl; }
                },
                /**
                 * The canvas element (readonly)
                 * @property canvas
                 * @type HTML-Element
                 * @final
                 */
                canvas: {
                    value: canvas
                },
                /**
                 * If null then nothing is rendered
                 * @property activeScene
                 * @type kick.scene.Scene
                 */
                activeScene: {
                    get: function () {
                        if (activeScene === activeSceneNull) {
                            return null;
                        }
                        return activeScene;
                    },
                    set: function (value) {
                        if (value === null || value === undefined) {
                            activeScene = activeSceneNull;
                        } else {
                            activeScene = value;
                        }
                    }
                },
                /**
                 * Returns a mouseInput object. This object is used to detect mouse input.
                 * @property mouseInput
                 * @type kick.core.MouseInput
                 */
                mouseInput: {
                    get: function () {
                        if (!mouseInput) {
                            mouseInput = new MouseInput(thisObj);
                            thisObj.addEventListener('postUpdateListener', mouseInput.frameUpdated);
                        }
                        return mouseInput;
                    }
                },
                /**
                 * Returns a keyInput object. This object is used to detect key input.
                 * @property keyInput
                 * @type kick.core.KeyInput
                 */
                keyInput: {
                    get: function () {
                        if (!keyInput) {
                            keyInput = new KeyInput();
                            thisObj.addEventListener('postUpdateListener', keyInput.frameUpdated);
                        }
                        return keyInput;
                    }
                },
                /**
                 * @property eventQueue
                 * @type kick.core.EventQueue
                 * @final
                 */
                eventQueue: {
                    get: function () {
                        return eventQueue;
                    }
                },
                /**
                 * The width and height of the canvas
                 * @property canvasDimension
                 * @type kick.math.Vec2
                 */
                canvasDimension: {
                    get: function () {
                        return canvasSize;
                    }
                },
                /**
                 * Time object of the engine. Is updated every frame
                 * @property time
                 * @type kick.core.Time
                 */
                time: {
                    value: timeObj
                },
                /**
                 * Configuration of the engine
                 * @property config
                 * @type kick.core.Config
                 */
                config: {
                    value: new Config(config || {})
                },
                /**
                 * Controls is the gameloop is running
                 * @property paused
                 * @type boolean
                 */
                paused: {
                    get: function () {
                        return animationFrameObj === null;
                    },
                    set: function (pause) {
                        var currentValue = thisObj.paused;
                        if (pause !== currentValue) {
                            if (pause) {
                                window.cancelAnimationFrame(animationFrameObj);
                                animationFrameObj = null;
                            } else {
                                lastTime = Date.now() - 16; // ensures valid delta time in next frame
                                animationFrameObj = window.requestAnimationFrame(wrapperFunctionToMethodOnObject, thisObj.canvas);
                            }
                        }
                    }
                }
            });

            /**
             * @method isFullScreenSupported
             * @return Boolean
             */
            this.isFullScreenSupported = function () {
                return canvas.requestFullscreen || canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen;
            };

            /**
             * Query WebGL for a specific extension. If found, the extension object is returned.
             * "WEBKIT\_", "MOZ\_" vendor prefixes are used.
             * @method getGLExtension
             * @param {String} extensionName
             * @return Object|null
             */
            this.getGLExtension = function (extensionName) {
                var vendorPrefixes = ["", "WEBKIT_", "MOZ_"],
                    i,
                    ext;
                for(i = 0;i < vendorPrefixes.length; i++) {
                    ext = gl.getExtension(vendorPrefixes[i] + extensionName);
                    if (ext) {
                        return ext;
                    }
                }
                return null;
            };

            /**
             * Note that fullscreen needs to be invoked directly from a keyboard event or a mouse event from outside the
             * gameLoop. This means it is currently not possible to set fullscreen from a Component's update method.
             * @method setFullscreen
             * @param {Boolean} fullscreen
             */
            this.setFullscreen = function (fullscreen) {
                if (thisObj.isFullScreenSupported()) {
                    if (fullscreen) {
                        if (canvas.requestFullscreen) {
                            canvas.requestFullscreen();
                        } else if (canvas.webkitRequestFullScreen) {
                            canvas.onwebkitfullscreenchange = function () {
                                if (document.webkitIsFullScreen) {
                                    canvas.originalWidth = canvas.width;
                                    canvas.originalHeight = canvas.height;
                                    canvas.width = screen.width;
                                    canvas.height = screen.height;
                                } else {
                                    canvas.width = canvas.originalWidth;
                                    canvas.height = canvas.originalHeight;
                                }
                            };
                            canvas.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
                        } else if (canvas.mozRequestFullScreen) {
                            canvas.mozRequestFullScreen();
                        }
                    } else {
                        if (document.exitFullscreen) {
                            document.exitFullscreen();
                        } else if (document.webkitCancelFullScreen) {
                            document.webkitCancelFullScreen();
                        } else if (document.webkitCancelFullScreen) {
                            document.webkitCancelFullScreen();
                        }
                    }
                }
            };

            /**
             * @method _gameLoop
             * @param {Number} time current time in milliseconds
             * @private
             */
            this._gameLoop = function (time) {
                if (canvas.width !== canvasSize[0] || canvas.height !== canvasSize[1]){
                    canvasSize[0] = canvas.width;
                    canvasSize[1] = canvas.height;
                    thisObj.fireEvent("canvasResized",canvasSize);
                }
                deltaTime = time - lastTime;
                lastTime = time;
                deltaTime *= timeScale;
                timeSinceStart += deltaTime;
                frame += 1;
                eventQueue.run();

                activeScene.updateAndRender();
                thisObj.fireEvent("postUpdateListener");

                if (animationFrameObj !== null) {
                    animationFrameObj = window.requestAnimationFrame(wrapperFunctionToMethodOnObject, thisObj.canvas);
                }
            };

            /**
             * Add a framelistener. Frame listeners are invoked last thing in update loop.<br>
             * Frame listener object must define the method frameUpdated()
             * @method addFrameListener
             * @param {Object} frameListener
             * @deprecated Use addEventListener("postUpdateListener", frameListener) instead
             */
            this.addFrameListener = function (frameListener) {
                Util.fail("Use addEventListener('postUpdateListener', frameListener) instead");
                thisObj.addEventListener("postUpdateListener", frameListener);
            };

            /**
             * @method removeFrameListener
             * @param {Object} frameListener
             * @deprecated
             */
            this.removeFrameListener = function (frameListener) {
                Util.fail("Use removeEventListener('postUpdateListener', frameListener) instead");
                thisObj.removeEventListener("postUpdateListener", frameListener);
            };

            /**
             * @method addContextListener
             * @param {Object} contextLostListener implements contextLost() and contextRestored(gl)
             * @deprecated
             */
            this.addContextListener = function (contextLostListener) {
                Util.fail("Use addEventListener('contextLost', fn) / addEventListener('contextRestored', fn)  instead");
                thisObj.addEventListener("contextLost", contextLostListener.contextLost);
                thisObj.addEventListener("contextRestored", contextLostListener.contextRestored);
            };

            /**
             * @method removeContextListener
             * @param contextLostListener
             * @deprecated
             */
            this.removeContextListener = function (contextLostListener) {
                Util.fail("Use removeEventListener('contextLost', fn) / removeEventListener('contextRestored', fn)  instead");
                thisObj.removeEventListener("contextLost", contextLostListener.contextLost);
                thisObj.removeEventListener("contextRestored", contextLostListener.contextRestored);
            };


            /**
             * Creates a uniq id
             * @method createUID
             * @return {Number} uniq id
             */
            this.createUID = function () {
                return ++project.maxUID;
            };

            /**
             * Get the uid of a component (or creates the uid if not defined)
             * @method getUID
             * @param {Object} object
             * @return {String}
             */
            this.getUID = function (object) {
                if (!object.uid) {
                    object.uid = thisObj.createUID();
                }
                return object.uid;
            };

            /**
             * This method should be invoked when the canvas is resized.<br>
             * This will change the viewport size of the WebGL state.<br>
             * Instead of calling this method explicit, the configuration parameter
             * checkCanvasResizeInterval can also be set to support automatically checks
             * @method canvasResized
             * @deprecated
             */
            /*this.canvasResized = function () {
                Util.warn("kick.core.Engine.canvasResized is deprecated");
                glState.viewportSize = thisObj.canvasDimension;
                if (mouseInput) {
                    mouseInput.updateCanvasElementPosition();
                }
            };*/

            /**
             * @method init
             * @private
             */
            (function init() {
                var c = Constants,
                    i,
                    success,
                    wasPaused,
                    initGL = function () {
                        if (thisObj.config.highDPISupport) {
                            var devicePixelRatio = window.devicePixelRatio || 1;

                            // set the size of the drawingBuffer based on the size it's displayed.
                            canvas.width = canvas.clientWidth * devicePixelRatio;
                            canvas.height = canvas.clientHeight * devicePixelRatio;
                        }
                        for (i = webGlContextNames.length - 1; i >= 0; i--) {
                            try {
                                gl = canvas.getContext(webGlContextNames[i], thisObj.config);
                                if (gl) {
                                    break;
                                }
                            } catch (e) {
                                // ignore
                                alert(e);
                            }
                        }
                        if (!gl) {
                            return false;
                        }
                        if (thisObj.config.enableDebugContext) {
                            if (WebGLDebugUtils && Constants._DEBUG) {
                                // Checking that none of the WebGL arguments are undefined
                                // http://www.khronos.org/webgl/wiki/Debugging#Checking_that_none_of_your_arguments_are_undefined
                                var validateNoneOfTheArgsAreUndefined = function (functionName, args) {
                                    var ii;
                                    for (ii = 0; ii < args.length; ++ii) {
                                        if (args[ii] === undefined) {
                                            console.error("undefined passed to gl." + functionName + "(" +
                                                WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")");
                                        }
                                    }
                                };
                                gl = WebGLDebugUtils.makeDebugContext(gl, undefined, validateNoneOfTheArgsAreUndefined);
                            } else {
                                console.log("webgl-debug.js not included - cannot find WebGLDebugUtils");
                            }
                        }
                        glState = new GLState(thisObj);
                        Object.freeze(gl);
                        gl.enable(c.GL_DEPTH_TEST);
                        gl.enable(c.GL_SCISSOR_TEST);
                        return true;
                    };
                EngineSingleton.engine = thisObj;
                engineInstance = thisObj;

                success = initGL();
                if (!success) {
                    thisObj.config.webglNotFoundFn(canvas);
                    return;
                }
                if (window.console && window.console.log){
                    console.log("KickJS "+thisObj.version);
                }
                canvas.addEventListener("webglcontextlost", function (event) {
                    wasPaused = thisObj.paused;
                    thisObj.paused = true;
                    thisObj.fireEvent("contextLost");
                    event.preventDefault();
                    gl = null;
                }, false);
                canvas.addEventListener("webglcontextrestored", function (event) {
                    glState.clear();
                    initGL();
                    thisObj.fireEvent("contextRestored", gl);
                    // restart rendering loop
                    if (!wasPaused) {
                        thisObj.paused = false;
                    }
                    event.preventDefault();
                }, false);

                // API documentation of Time is found in kick.core.Time
                Object.defineProperties(timeObj, {
                    time: {
                        get: function () { return timeSinceStart; }
                    },
                    deltaTime: {
                        get: function () { return deltaTime; }
                    },
                    frame: {
                        get: function () { return frame; }
                    },
                    scale: {
                        get: function () {
                            return timeScale;
                        },
                        set: function (newValue) {
                            timeScale = newValue;
                        }
                    }
                });
                Object.freeze(timeObj);

                activeScene = new Scene();
                eventQueue = new EventQueue(thisObj);

                timeSinceStart = 0;
                frame = 0;

                thisObj._gameLoop(lastTime);
            }());
        };
        Object.defineProperties(engine, {
            /**
             * Returns the singleton engine object
             * @property instance
             * @type kick.core.Engine
             * @static
             */
            instance: {
                get: function () {
                    return engineInstance;
                }
            }
        });
        return engine;
    }
    );