/** * Linking System takes care of linking components and dependencies (after they have loaded) - * and managing the relationships between objects - ex. what happens when a parent entity changes, * or a parent scene changes. * @param apiSystem GameLib.API.System * @constructor */ GameLib.System.Linking = function( apiSystem ) { GameLib.System.call( this, apiSystem ); /** * The dependencies of each component is tracked through this dependencies object - * it maps the id of the object on which a component depends back to the component which depends on it, * ex. texture.image = 'abcdefghi', then this.dependencies = {'abcdefghi' : [texture]} * @type {{}} */ this.dependencies = {}; this.resolved = []; /** * Components */ this.componentCreatedSubscription = null; this.componentClonedSubscription = null; this.registerDependenciesSubscription = null; this.componentRemoveSubscription = null; /** * Parents */ this.parentSceneChangeSubscription = null; this.parentWorldChangeSubscription = null; this.parentEntityChangeSubscription = null; /** * Instances */ this.instanceCreatedSubscription = null; this.instanceClonedSubscription = null; /** * Meshes */ this.removeMeshSubscription = null; /** * Images */ this.imageChangedSubscription = null; /** * Materials */ this.materialTypeChangedSubscription = null; /** * Arrays */ this.arrayItemAddedSubscription = null; }; GameLib.System.Linking.prototype = Object.create(GameLib.System.prototype); GameLib.System.Linking.prototype.constructor = GameLib.System.Linking; GameLib.System.Linking.prototype.start = function() { GameLib.System.prototype.start.call(this); /** * Components */ this.componentCreatedSubscription = this.subscribe( GameLib.Event.COMPONENT_CREATED, this.componentCreated.bind(this) ); this.componentClonedSubscription = this.subscribe( GameLib.Event.COMPONENT_CLONED, this.componentCloned.bind(this) ); this.registerDependenciesSubscription = this.subscribe( GameLib.Event.REGISTER_DEPENDENCIES, this.registerDependenciesDirect ); this.componentRemoveSubscription = this.subscribe( GameLib.Event.REMOVE_COMPONENT, this.removeComponent ); /** * Parents */ this.parentSceneChangeSubscription = this.subscribe( GameLib.Event.PARENT_SCENE_CHANGE, this.onParentSceneChange ); this.parentWorldChangeSubscription = this.subscribe( GameLib.Event.PARENT_WORLD_CHANGE, this.onParentWorldChange ); this.parentEntityChangeSubscription = this.subscribe( GameLib.Event.PARENT_ENTITY_CHANGE, this.onParentEntityChange ); /** * Instances */ this.instanceCreatedSubscription = this.subscribe( GameLib.Event.INSTANCE_CREATED, this.instanceCreated ); this.instanceClonedSubscription = this.subscribe( GameLib.Event.INSTANCE_CLONED, this.instanceCloned ); /** * Meshes */ this.removeMeshSubscription = this.subscribe( GameLib.Event.REMOVE_MESH, this.removeMesh ); /** * Images */ this.imageChangedSubscription = this.subscribe( GameLib.Event.IMAGE_CHANGED, this.imageChanged ); /** * Materials */ this.materialTypeChangedSubscription = this.subscribe( GameLib.Event.MATERIAL_TYPE_CHANGED, this.materialTypeChanged ); /** * Arrays */ this.arrayItemAddedSubscription = this.subscribe( GameLib.Event.ARRAY_ITEM_ADDED, this.arrayItemAdded ); }; GameLib.System.Linking.prototype.link = function(component, data) { for (var property in component.linkedObjects) { if (component.linkedObjects.hasOwnProperty(property)) { if (component.linkedObjects[property] instanceof Array) { var linked = []; component[property] = component[property].map(function (entry) { if (entry === data.component.id) { linked.push({ parent : component, property : property, child : data.component }); return data.component; } else { return entry; } }); linked.map(function(link) { GameLib.Event.Emit( GameLib.Event.COMPONENT_LINKED, link ); }) } else { if (component[property] && component[property] === data.component.id) { component[property] = data.component; GameLib.Event.Emit( GameLib.Event.COMPONENT_LINKED, { parent : component, property : property, child : data.component } ); } } } } }; GameLib.System.Linking.prototype.resolveDependencies = function(component) { if (!component.loaded) { /** * This component has not fully loaded - we should resolve dependencies to it later */ return false; } /** * Now find all the components which depend on this component */ var parentComponents = this.dependencies[component.id]; /** * If we don't have any components which depend on this component, simply return */ if (GameLib.Utils.UndefinedOrNull(parentComponents)) { /** * We don't know about components which depend on this component - but it could still load. * However, it is stored in the register and dependency list for later use */ return false; } /** * Otherwise, process them all */ parentComponents.map( function (parentComponent) { /** * Link the parent component to this component */ this.link(parentComponent, {component: component}); /** * We record that we linked a child component to a parent component */ GameLib.Utils.PushUnique(this.resolved, component); /** * First check if the dependencies have already been met */ if ( GameLib.Utils.UndefinedOrNull(parentComponent.dependencies) || ( parentComponent.dependencies instanceof Array && parentComponent.dependencies.length === 0 ) ) { /** * This means - a parent component instance could maybe have been delayed to be created * because the component constructor or linking system did not know at time of 'createInstance' * that it required another object to fully become active */ if ( !parentComponent.loaded || GameLib.Utils.UndefinedOrNull(parentComponent.instance) ) { try { parentComponent.performInstanceCreation(); } catch (error) { console.error(error); } } else { /** * These dependencies have already been met - the parentComponent properties have changed. * It is time to 'update' this instance with this information (if any of it is relevant - depends * on the component) */ // parentComponent.updateInstance(); } } else { /** * Remove the actual dependency */ var index = parentComponent.dependencies.indexOf(component.id); if (index !== -1) { parentComponent.dependencies.splice(index, 1); } /** * If we now managed to link the objects, and this object has no more dependencies */ if (parentComponent.dependencies.length === 0) { parentComponent.performInstanceCreation(); } } }.bind(this) ); /** * We now linked all the components which depends on this component, to this component. Time to cleanup our * dependencies */ delete this.dependencies[component.id]; /** * For now this essentially only notifies the Editor - We have some more work to do however */ GameLib.Event.Emit( GameLib.Event.UNRESOLVED_DEPENDENCIES_UPDATE, { dependencies : this.dependencies } ); /** * If we happen to have no more dependencies - we linked a bunch of components which are ready to use */ if (GameLib.Utils.IsEmpty(this.dependencies)) { /** * This also only notifies the Editor - We still have some more work to here */ GameLib.Event.Emit( GameLib.Event.COMPONENTS_LINKED, { components: this.resolved.map( function(component) { return component; } ) } ); this.resolved = []; } //else { // var keys = Object.keys(this.dependencies); /** * And this is it - we need to check if the dependencies array contains any 'resolved' components - * If it does - resolve the dependencies of this newly 'resolved' component */ // this.resolved = this.resolved.reduce( // // function(result, component) { // // if (keys.indexOf(component.id) !== -1) { // /** // * We found a resolved component - which is a dependency for another component. // * Resolve the dependencies of this component - this is recursive and should be done carefully // */ // this.resolveDependencies(component); // } else { // result.push(component); // } // // return result; // // }.bind(this), // [] // ); //} }; GameLib.System.Linking.prototype.registerDependencies = function(component) { /** * We only care about components with unloaded dependencies - * other components will have already had their instance objects created */ if (component.dependencies && component.dependencies.length > 0) { component.dependencies = component.dependencies.reduce( function(result, id) { /** * Check if we already processed a component on which this component is dependent */ var processedComponent = GameLib.EntityManager.Instance.findComponentById(id); if (processedComponent && processedComponent.loaded) { /** * Link the component */ this.link(component, {component: processedComponent}); GameLib.Utils.PushUnique(this.resolved, processedComponent); } else { /** * Create a new link if none exists */ if (GameLib.Utils.UndefinedOrNull(this.dependencies[id])) { this.dependencies[id] = []; } /** * Don't store duplicate dependencies */ if (this.dependencies[id].indexOf(component) === -1) { this.dependencies[id].push(component); GameLib.Event.Emit( GameLib.Event.UNRESOLVED_DEPENDENCIES_UPDATE, { dependencies : this.dependencies } ); } /** * Also - we remember that this component has a dependency */ result.push(id); } return result; }.bind(this), [] ); if (component.dependencies.length === 0) { component.performInstanceCreation(); } } }; /** * When a component is created, register its dependencies, and try to resolve them * @param data */ GameLib.System.Linking.prototype.componentCreated = function(data) { /** * Shorthand */ var component = data.component; /** * Register any dependencies of this component */ this.registerDependencies(component); /** * Resolve any dependencies to this component */ this.resolveDependencies(component); }; GameLib.System.Linking.prototype.componentCloned = function(data) { this.componentCreated(data); if (data.component instanceof GameLib.D3.Mesh) { if (!(data.parent instanceof GameLib.D3.Mesh)){ throw new Error('no scene parent'); } if (data.parent.parentScene) { data.parent.parentScene.addClone(data.component); } } }; /** * When you want to register dependencies directly - Component constructor does this when it knows the * component instance cannot be created because it has a bunch of dependencies. So it tells the linking * system about it, so the linking system can create the instance when the dependency loads or already exists * @param data */ GameLib.System.Linking.prototype.registerDependenciesDirect = function(data) { this.registerDependencies(data.component); }; GameLib.System.Linking.prototype.removeComponent = function(data) { if (!data.component) { console.error('no component to remove'); return; } var component = data.component; if (component.parentEntity instanceof GameLib.Entity) { component.parentEntity.removeComponent(component); } if (component instanceof GameLib.D3.Mesh && component.parentScene instanceof GameLib.D3.Scene) { component.removeHelper(); component.parentScene.removeObject(component); } if (component instanceof GameLib.D3.Light && component.parentScene instanceof GameLib.D3.Scene) { component.parentScene.removeObject(component); } if (component instanceof GameLib.Entity) { GameLib.EntityManager.Instance.removeEntity(component); } }; GameLib.System.Linking.prototype.imageChanged = function(data) { var materials = GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Material); materials.map(function(material){ var textures = material.getTextures(); if (textures.indexOf(data.texture) !== -1) { material.updateInstance(); } }); }; GameLib.System.Linking.prototype.arrayItemAdded = function(data) { if ( data.component instanceof GameLib.D3.PhysicsWorld && data.item instanceof GameLib.D3.RigidBody ) { data.component.addRigidBody(data.item); } if (data.component instanceof GameLib.D3.Mesh && data.item instanceof GameLib.D3.Material ) { data.component.addMaterial(data.item); } }; GameLib.System.Linking.prototype.instanceCloned = function(data) { if (data.component instanceof GameLib.D3.Particle) { var mesh = data.component.mesh; if (mesh.parentScene && mesh.parentScene.instance) { data.instance.userData.scene = mesh.parentScene.instance; mesh.parentScene.instance.add(data.instance); } } }; GameLib.System.Linking.prototype.instanceCreated = function(data) { this.resolveDependencies(data.component); if (data.component instanceof GameLib.D3.Image) { /** * Find all textures which use this image */ GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Texture).map( function(texture) { if (texture.image === data.component || texture.images.indexOf(data.component) !== -1 ) { /** * Ok - this image is in use - this should notify materials when its image changes */ texture.updateInstance('image'); } } ); } /** * Link all scenes */ if (data.component instanceof GameLib.D3.Scene) { /** * Check ALL components for 'parentScenes' - this is expensive so it checks the register directly */ GameLib.EntityManager.Instance.register.map( function(component) { if (component.parentScene === data.component.id) { component.parentScene = data.component; } } ); } if ( data.component.parentScene && typeof data.component.parentScene === 'string' ) { GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Scene).map( function (scene) { if (data.component.parentScene === scene.id) { data.component.parentScene = scene; scene.addObject(data.component); } } ); } /** * Link all meshes */ if (data.component instanceof GameLib.D3.Mesh) { /** * Check if this mesh is a parentMesh to any component- this is an expensive call, so check if we should call it * Also - it inspects the register directly instead of querying it twice (since it checks ALL components) */ if (!data.preventParentMeshCheck) { GameLib.EntityManager.Instance.register.map( function (component) { if (component.parentMesh && component.parentMesh === data.component.id ) { component.parentMesh = data.component; /** * Check if a component has this mesh as a parent */ if (component instanceof GameLib.D3.Mesh) { component.setParentMesh(data.component); } } } ); } } /** * Maybe this component has a parent mesh */ if ( data.component.parentMesh && typeof data.component.parentMesh === 'string' ) { GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Mesh).map( function (mesh) { if (data.component.parentMesh === mesh.id) { data.component.parentMesh = mesh; if (data.component instanceof GameLib.D3.Mesh) { data.component.setParentMesh(mesh); } } } ); } if ( data.component.parentWorld && typeof data.component.parentWorld === 'string' ) { GameLib.EntityManager.Instance.queryComponents(GameLib.D3.PhysicsWorld).map( function (world) { if (data.component.parentWorld === world.id) { data.component.parentWorld = world; if (typeof data.component.instance.addToWorld === 'function') { data.component.instance.addToWorld(world.instance); console.log('instance added to physics world'); } } } ); } }; GameLib.System.Linking.prototype.materialTypeChanged = function(data) { var meshes = GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Mesh); meshes.map( function(mesh){ var inUse = mesh.materials.reduce( function(result, material) { if (material === data.material) { result = true; } return result; }, false ); if (inUse) { if (mesh.materials.length === 1) { mesh.instance.material = mesh.materials[0].instance } else { mesh.instance.material = mesh.materials.map( function(material) { return material.instance; } ) } mesh.instance.geometry.uvsNeedUpdate = true; mesh.instance.material.needsUpdate = true; } } ); }; /** * * @param data */ GameLib.System.Linking.prototype.onParentWorldChange = function(data) { if ( data.object instanceof GameLib.D3.RigidBody ) { if (data.originalWorld instanceof GameLib.D3.PhysicsWorld) { data.originalWorld.removeRigidBody(data.object); } if (data.newWorld instanceof GameLib.D3.PhysicsWorld) { data.newWorld.addRigidBody(data.object); } } }; /** * Defines what should happen when a parent scene changes * @param data */ GameLib.System.Linking.prototype.onParentSceneChange = function(data) { if ( data.object instanceof GameLib.D3.Mesh || data.object instanceof GameLib.D3.Light ) { /** * We remove the helper (if any) from the old scene and add it to the new scene */ var helper = GameLib.EntityManager.Instance.findHelperByObject(data.object); if (helper) { if (data.originalScene && data.originalScene.instance) { data.originalScene.instance.remove(helper.instance); } data.newScene.instance.add(helper.instance); } /** * We remove the mesh from the old scene and add it to the new scene */ if (data.originalScene && data.originalScene.removeObject) { data.originalScene.removeObject(data.object); } if (data.newScene) { data.newScene.addObject(data.object); } } }; /** * Change parent entity * @param data */ GameLib.System.Linking.prototype.onParentEntityChange = function(data) { if (data.originalEntity instanceof GameLib.Entity) { data.originalEntity.removeComponent(data.object); } if (data.newEntity instanceof GameLib.Entity) { data.newEntity.addComponent(data.object); } GameLib.Event.Emit( GameLib.Event.PARENT_ENTITY_CHANGED, { originalEntity : data.originalEntity, newEntity : data.newEntity, component : data.object } ) }; /** * When a mesh is deleted - build a list of all the mesh children objects - also - find out if any of these * children objects are in use by another object - if it is - don't delete it, otherwise, do * @param data */ GameLib.System.Linking.prototype.removeMesh = function(data) { /** * First we get the list of all components we would like to delete */ var componentsToDelete = data.meshes.reduce( function(result, mesh) { result.push(mesh); var components = mesh.getChildrenComponents(); components.map(function(component){ result.push(component); }); return result; }, [] ); /** * Now, we want to get a list of all the meshes which we don't want to delete, and all their children */ var meshes = GameLib.EntityManager.Instance.queryComponents(GameLib.D3.Mesh); meshes = meshes.filter(function(mesh){ return data.meshes.indexOf(mesh) === -1; }); /** * Now we have a list of meshes still in use in meshes, now find all their children */ var componentsInUse = meshes.reduce( function(result, mesh) { result.push(mesh); var components = mesh.getChildrenComponents(); components.map(function(component){ result.push(component); }); return result; }, [] ); /** * Now we don't want to remove any children in use, so filter out the components in use */ componentsToDelete = componentsToDelete.filter( function(component) { return componentsInUse.indexOf(component) === -1; } ); /** * componentsToDelete should now be the final list of components to delete */ componentsToDelete.map( function(component){ component.remove(); } ); }; GameLib.System.Linking.prototype.stop = function() { GameLib.System.prototype.stop.call(this); /** * Components */ this.componentCreatedSubscription.remove(); this.componentClonedSubscription.remove(); this.registerDependenciesSubscription.remove(); this.componentRemoveSubscription.remove(); /** * Parents */ this.parentSceneChangeSubscription.remove(); this.parentWorldChangeSubscription.remove(); this.parentEntityChangeSubscription.remove(); /** * Instances */ this.instanceCreatedSubscription.remove(); this.instanceClonedSubscription.remove(); /** * Meshes */ this.removeMeshSubscription.remove(); /** * Images */ this.imageChangedSubscription.remove(); /** * Materials */ this.materialTypeChangedSubscription.remove(); /** * Arrays */ this.arrayItemAddedSubscription.remove(); };