API Docs for: 0.5.5
Show:

File: kick/scene/Scene.js

define(["require", "kick/core/ProjectAsset", "./SceneLights", "kick/core/Constants", "kick/core/Util", "./Camera", "./Light", "./GameObject", "kick/core/EngineSingleton", "kick/core/Observable"],
    function (require, ProjectAsset, SceneLights, Constants, Util, Camera, Light, GameObject, EngineSingleton, Observable) {
        "use strict";

        var warn = Util.warn,
            DEBUG = Constants._DEBUG,
            ASSERT = Constants._ASSERT,
            Scene;

        /**
         * A scene objects contains a list of GameObjects
         * @class Scene
         * @namespace kick.scene
         * @constructor
         * @param {Object} config
         * @extends kick.core.ProjectAsset
         */
        Scene = function (config) {
            // extend ProjectAsset
            ProjectAsset(this, config, "kick.scene.Scene");
            if (ASSERT){
                if (config === EngineSingleton.engine){
                    Util.fail("Scene constructor changed - engine parameter is removed");
                }
            }
            var engine = EngineSingleton.engine,
                objectsById = {},
                gameObjects = [],
                activeGameObjects = [],
                gameObjectsNew = [],
                gameObjectsDelete = [],
                updateableComponents = [],
                componentsNew = [],
                componentsDelete = [],
                componentsAll = [],
                cameras = [],
                renderableComponents = [],
                sceneLightObj = new SceneLights(engine.config.maxNumerOfLights),
                _name = "Scene",
                thisObj = this,
                addLight = function (light) {
                    if (light.type === Light.TYPE_AMBIENT) {
                        sceneLightObj.ambientLight = light;
                    } else if (light.type === Light.TYPE_DIRECTIONAL) {
                        sceneLightObj.directionalLight = light;
                    } else {
                        sceneLightObj.addPointLight(light);
                    }
                },
                removeLight = function (light) {
                    if (light.type === Light.TYPE_AMBIENT) {
                        sceneLightObj.ambientLight = null;
                    } else if (light.type === Light.TYPE_DIRECTIONAL) {
                        sceneLightObj.directionalLight = null;
                    } else {
                        sceneLightObj.removePointLight(light);
                    }
                },
                /**
                 * Compares two objects based on scriptPriority
                 * @method sortByScriptPriority
                 * @param {kick.scene.Component} a
                 * @param {kick.scene.Component} b
                 * @return {Number} order of a,b
                 * @private
                 */
                sortByScriptPriority = function (a, b) {
                    return a.scriptPriority - b.scriptPriority;
                },
                /**
                 * Compares two camera objects by their cameraIndex attribute
                 * @method cameraSortFunc
                 * @param {kick.scene.Camera} a
                 * @param {kick.scene.Camera} b
                 * @param {Number} difference
                 * @private
                 */
                cameraSortFunc = function (a, b) {
                    return b.cameraIndex - a.cameraIndex;
                },
                /**
                 * Handle insertions of new gameobjects and components. This is done in a separate step to avoid problems
                 * with missed updates (or multiple updates) due to modifying the array while iterating it.
                 * @method addNewGameObjects
                 * @private
                 */
                addNewGameObjects = function () {
                    var i,
                        component,
                        componentsNewCopy;
                    if (gameObjectsNew.length > 0) {
                        activeGameObjects = activeGameObjects.concat(gameObjectsNew);
                        gameObjectsNew.length = 0;
                    }
                    if (componentsNew.length > 0) {
                        componentsNewCopy = componentsNew;
                        componentsNew = [];
                        for (i = componentsNewCopy.length - 1; i >= 0; i--) {
                            component = componentsNewCopy[i];
                            componentsAll.push(component);
                            if (typeof (component.activated) === "function") {
                                component.activated();
                            }
                            if (typeof (component.update) === "function") {
                                Util.insertSorted(component, updateableComponents, sortByScriptPriority);
                            }
                            if (typeof (component.render) === "function") {
                                renderableComponents.push(component);
                            }
                            if (typeof (component.render) === "function") {
                                Util.removeElementFromArray(renderableComponents, component);
                            }
                            if (component instanceof Camera) {
                                Util.insertSorted(component, cameras, cameraSortFunc);
                            } else if (component instanceof Light) {
                                addLight(component);
                            }
                            thisObj.fireEvent("componentAdded", component);
                        }
                    }
                },/**
                 * Handle deletion of new gameobjects and components. This is done in a separate step to avoid problems
                 * with missed updates (or multiple updates) due to modifying the array while iterating it.
                 * @method cleanupGameObjects
                 * @private
                 */
                cleanupGameObjects = function () {
                    var i,
                        component,
                        componentsDeleteCopy;
                    if (gameObjectsDelete.length > 0) {
                        Util.removeElementsFromArray(activeGameObjects, gameObjectsDelete);
                        Util.removeElementsFromArray(gameObjects, gameObjectsDelete);
                        gameObjectsDelete.length = 0;
                    }
                    if (componentsDelete.length > 0) {
                        componentsDeleteCopy = componentsDelete;
                        componentsDelete = [];
                        for (i = componentsDeleteCopy.length - 1; i >= 0; i--) {
                            component = componentsDeleteCopy[i];
                            Util.removeElementFromArray(componentsAll, component);
                            if (typeof (component.deactivated) === "function") {
                                component.deactivated();
                            }
                            if (typeof (component.update) === "function") {
                                Util.removeElementFromArray(updateableComponents, component);
                            }
                            if (component instanceof Camera) {
                                Util.removeElementFromArray(cameras, component);
                            } else if (component instanceof Light) {
                                removeLight(component);
                            }
                            thisObj.fireEvent("componentRemoved", component);
                        }
                    }
                },
                insertAndRemoveComponents = function () {
                    var count = 0;
                    while (gameObjectsDelete.length ||
                            componentsDelete.length ||
                            gameObjectsNew.length ||
                            componentsNew.length) {
                        cleanupGameObjects();
                        addNewGameObjects();
                        if (ASSERT) {
                            count++;
                            if (count > 10) {
                                Util.fail("Recursion detected in Component.activated or Component.deactivated.");
                                return;
                            }
                        }
                    }
                },
                updateComponents = function () {
                    insertAndRemoveComponents();
                    var i;
                    for (i = updateableComponents.length - 1; i >= 0; i--) {
                        updateableComponents[i].update();
                    }
                    insertAndRemoveComponents();
                },
                renderComponents = function () {
                    var i;
                    for (i = cameras.length - 1; i >= 0; i--) {
                        cameras[i].renderScene(sceneLightObj);
                    }
                    engine.gl.flush();
                },
                componentAddedListener = function (component) {
                    Util.insertSorted(component, componentsNew, sortByScriptPriority);
                    var uid = engine.getUID(component);
                    if (ASSERT) {
                        if (objectsById[uid]) {
                            Util.fail("Component with uid " + uid + " already exist");
                        }
                    }
                    objectsById[uid] = component;
                },
                componentRemovedListener = function (component) {
                    Util.removeElementFromArray(componentsNew, component);
                    componentsDelete.push(component);
                    delete objectsById[component.uid];
                },
                createGameObjectPrivate = function (config) {
                    var gameObject = new GameObject(thisObj, config);
                    gameObjectsNew.push(gameObject);
                    gameObjects.push(gameObject);
                    objectsById[gameObject.uid] = gameObject;
                    gameObject.addEventListener("componentAdded", componentAddedListener);
                    gameObject.addEventListener("componentRemoved", componentRemovedListener);
                    return gameObject;
                };

            Observable.call(this, [
            /**
             * Fired when a new component is added to scene
             * @event componentAdded
             * @param {kick.scene.Component} component
             */
                "componentAdded",
            /**
             * Fired when a new component is removed from scene
             * @event componentRemoved
             * @param {kick.scene.Component} component
             */
                "componentRemoved"
            ]
            );

            /**
             * @method destroy
             */
            this.destroy = function () {
                engine.project.removeResourceDescriptor(thisObj.uid);
                if (thisObj === engine.activeScene) {
                    engine.activeScene = null;
                }
            };

            /**
             * Search the scene for components of the specified type in the scene. Note that this
             * method is slow - do not run in the the update function.
             * @method findComponentsOfType
             * @param {Function} componentType
             * @return {Array_kick.scene.Component} components
             */
            this.findComponentsOfType = function (componentType) {
                if (ASSERT) {
                    if (typeof componentType !== 'function') {
                        Util.fail("Scene.findComponentsOfType expects a function");
                    }
                }
                var res = [],
                    i,
                    j,
                    component;
                for (i = gameObjects.length - 1; i >= 0; i--) {
                    component = gameObjects[i].getComponentsOfType(componentType);
                    for (j = 0; j < component.length; j++) {
                        res.push(component[j]);
                    }
                }
                return res;
            };

            /**
             * Search the scene for components of the specified type in the scene. Note that this
             * method is slow - do not run in the the update function.
             * @method findComponentsWithMethod
             * @param {string} methodName
             * @return {Array_kick.scene.Component} components
             */
            this.findComponentsWithMethod = function (methodName) {
                if (ASSERT) {
                    if (typeof methodName !== 'string') {
                        Util.fail("Scene.findComponentsWithMethod expects a string");
                    }
                }
                var res = [],
                    i,
                    j,
                    component;
                for (i = gameObjects.length - 1; i >= 0; i--) {
                    component = gameObjects[i].getComponentsWithMethod(methodName);
                    for (j = 0; j < component.length; j++) {
                        res.push(component[j]);
                    }
                }
                return res;
            };

            /**
             * @method getObjectByUID
             * @param {Number} uid
             * @return {Object} GameObject or component
             */
            this.getObjectByUID = function (uid) {
                return objectsById[uid];
            };

            /**
             * Returns a GameObject identified by name
             * @method getGameObjectByName
             * @param {String} name
             * @return {kick.scene.GameObject} GameObject or undefined if not found
             */
            this.getGameObjectByName = function (name) {
                var i,
                    gameObject;
                for (i = gameObjects.length - 1; i >= 0; i--) {
                    gameObject = gameObjects[i];
                    if (gameObject.name === name) {
                        return gameObject;
                    }
                }
            };

            Object.defineProperties(this, {
                /**
                 * Reference to the engine
                 * @property engine
                 * @type kick.core.Engine
                 */
                engine: {
                    value: engine
                },
                /**
                 * Name of the scene
                 * @property name
                 * @type String
                 */
                name: {
                    get: function () {
                        return _name;
                    },
                    set: function (newValue) {
                        _name = newValue;
                    }

                }
            });

            /**
             * @method createGameObject
             * @param {Object} [config] Optionally configuration passed to the game objects
             * @return {kick.scene.GameObject}
             */
            this.createGameObject = function (config) {
                var gameObject = createGameObjectPrivate(config),
                    transform = gameObject.transform;
                objectsById[engine.getUID(transform)] = transform;
                return gameObject;
            };

            /**
             * Destroys the game object and delete it from the scene.
             * This call will call destroy on the gameObject
             * @method destroyObject
             * @param {kick.scene.GameObject} gameObject
             * @deprecated
             */
            this.destroyObject = function (gameObject) {
                var isMarkedForDeletion = Util.contains(gameObjectsDelete, gameObject);

                if (!isMarkedForDeletion) {
                    gameObjectsDelete.push(gameObject);
                    delete objectsById[gameObject.uid];
                }
                if (!gameObject.destroyed) {
                    gameObject.destroy();
                }
            };

            /**
             * Destroys the game object and delete it from the scene.
             * This call will call destroy on the gameObject
             * @method destroyGameObject
             * @param {kick.scene.GameObject} gameObject
             */
            this.destroyGameObject = function (gameObject) {
                var isMarkedForDeletion = Util.contains(gameObjectsDelete, gameObject);
                if (!isMarkedForDeletion) {
                    gameObjectsDelete.push(gameObject);
                    delete objectsById[gameObject.uid];
                }
                if (!gameObject.destroyed) {
                    gameObject.destroy();
                }
            };

            /**
             * @method getNumberOfGameObjects
             * @return {Number} number of gameobjects
             */
            this.getNumberOfGameObjects = function () {
                return gameObjects.length;
            };

            /**
             * @method getGameObject
             * @param {Number} index
             * @return {kick.scene.GameObject}
             */
            this.getGameObject = function (index) {
                return gameObjects[index];
            };

            /**
             * Called by engine every frame. Updates and render scene
             * @method updateAndRender
             */
            this.updateAndRender = function () {
                updateComponents();
                renderComponents();
            };

            /**
             * @method toJSON
             * @param {Function} [filterFn] Optional. Filter with function(object): return boolean, where true means include in export.
             * @return {Object}
             */
            this.toJSON = function (filterFn) {
                var gameObjectsCopy = [],
                    i,
                    gameObject;
                filterFn = filterFn || function () { return true; };
                for (i = 0; i < gameObjects.length; i++) {
                    gameObject = gameObjects[i];
                    if (filterFn(gameObject)) {
                        gameObjectsCopy.push(gameObject.toJSON());
                    }
                }
                return {
                    uid: thisObj.uid,
                    gameObjects: gameObjectsCopy,
                    name: _name
                };
            };

            /**
             * Configures the object using the configuration data.
             * @method init
             * @param config {Object} configuration data in JSON format
             */
            this.init = function(config){
                var gameObject,
                    mappingUidToObject,
                    newGameObjects,
                    configs,
                    hasProperty = Util.hasProperty,
                    applyConfig = Util.applyConfig,
                    i,
                    createConfigWithReferences = function (config) {
                        var configCopy = {},
                            name,
                            value;
                        for (name in config) {
                            if (config.hasOwnProperty(name) && hasProperty(config, name)) {
                                value = config[name];
                                value = Util.deserializeConfig(value, thisObj);
                                configCopy[name] = value;
                            }
                        }
                        return configCopy;
                    };
                if (config) {
                    _name = config.name || "Scene";
                    gameObjects = [];
                    mappingUidToObject = {};
                    configs = {};
                    // create game objects
                    (function createGameObjects() {
                        if (config && config.gameObjects){
                            for (i = 0; i < config.gameObjects.length; i++) {
                                gameObject = config.gameObjects[i];
                                gameObjects[i] = createGameObjectPrivate(gameObject);
                                mappingUidToObject[gameObject.uid] = gameObjects[i];
                            }
                        }
                    }());

                    (function createComponents() {
                        var component,
                            componentObj,
                            Type,
                            gameObjectConfig,
                            j,
                            i,
                            uid,
                            originalConf,
                            conf,
                            obj;

                        for (j = 0; j < gameObjects.length; j++) {
                            gameObjectConfig = config.gameObjects[j];
                            gameObject = gameObjects[j];
                            // build components
                            for (i = 0; gameObjectConfig.components && i < gameObjectConfig.components.length; i++) {
                                component = gameObjectConfig.components[i];
                                if (component.type === "kick.scene.Transform") {
                                    componentObj = gameObject.transform;
                                    componentObj.uid = component.uid;
                                    // register transform object to objectsById
                                    objectsById[componentObj.uid] = componentObj;
                                } else {
                                    Type = require(component.type.replace(/\./g,"/"));
                                    if (typeof Type === 'function') {
                                        componentObj = new Type({uid: component.uid});
                                        componentObj.uid = component.uid;
                                        gameObject.addComponent(componentObj);
                                    } else {
                                        Util.warn("Cannot find Class " + component.type);
                                        continue;
                                    }
                                }
                                mappingUidToObject[component.uid] = componentObj;
                                configs[component.uid] = component.config;
                            }
                        }

                        // apply config
                        for (uid in mappingUidToObject) {
                            if (mappingUidToObject.hasOwnProperty(uid) && hasProperty(mappingUidToObject, uid)) {
                                originalConf = configs[uid];
                                if (originalConf) {
                                    conf = createConfigWithReferences(originalConf);
                                    obj = mappingUidToObject[uid];
                                    applyConfig(obj, conf);
                                }
                            }
                        }
                    }());
                }
            };
            this.init(config);
        };

        /**
         * Create empty scene with camera
         * @method createDefault
         * @static
         * @return {kick.scene.Scene}
         */
        Scene.createDefault = function () {
            var engine = EngineSingleton.engine,
                newScene = new Scene(),
                gameObject = newScene.createGameObject();
            gameObject.addComponent(new Camera());
            return newScene;
        };

        return Scene;
    });