r3-legacy/src/game-lib-system-linking.js

980 lines
27 KiB
JavaScript

/**
* 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.componentUpdateSubcription = null;
this.componentClonedSubscription = null;
this.registerDependenciesSubscription = null;
this.componentRemoveSubscription = null;
/**
* Parents
*/
this.parentSceneChangeSubscription = null;
this.parentPhysicsWorldChangeSubscription = 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.componentUpdateSubcription = this.subscribe(
GameLib.Event.COMPONENT_UPDATE,
this.componentUpdate.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.parentPhysicsWorldChangeSubscription = 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);
};
/**
* Trigger a component update
* @param data
*/
GameLib.System.Linking.prototype.componentUpdate = function(data){
var component = GameLib.EntityManager.Instance.findComponentByName(data.name);
if (GameLib.Utils.UndefinedOrNull(data.property)) {
console.warn('invalid data format - we expect data.property');
return;
}
if (GameLib.Utils.UndefinedOrNull(data.value)) {
console.warn('invalid data format - we expect data.value');
return;
}
if (GameLib.Utils.UndefinedOrNull(data.subProperty)) {
component[data.property] = data.value;
} else {
component[data.property][data.subProperty] = data.value;
}
component.updateInstance(data.property);
};
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);
}
// if (component instanceof GameLib.D3.Particle) {
// // component.mesh.parentScene.removeObject(component.mesh);
// }
};
GameLib.System.Linking.prototype.imageChanged = function(data) {
var materials = GameLib.EntityManager.Instance.queryComponents(GameLib.Component.MATERIAL);
materials.map(function(material){
var textures = material.getTextures();
if (textures.indexOf(data.texture) !== -1) {
if (material.loaded) {
material.updateInstance('diffuseMap');
}
}
});
};
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.Image) {
/**
* Find all textures which use this image
*/
GameLib.EntityManager.Instance.queryComponents(GameLib.Component.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
*/
Object.keys(GameLib.EntityManager.Instance.idRegister).map(
function(componentId) {
if (GameLib.EntityManager.Instance.idRegister[componentId].parentScene === data.component.id) {
GameLib.EntityManager.Instance.idRegister[componentId].parentScene = data.component;
}
}
);
}
if (
data.component.parentScene &&
typeof data.component.parentScene === 'string'
) {
GameLib.EntityManager.Instance.queryComponents(GameLib.Component.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) {
Object.keys(GameLib.EntityManager.Instance.idRegister).map(
function(componentId) {
if (GameLib.EntityManager.Instance.idRegister[componentId].parentMesh === data.component.id) {
GameLib.EntityManager.Instance.idRegister[componentId].parentMesh = data.component;
/**
* Check if a component has this mesh as a parent
*/
if (GameLib.EntityManager.Instance.idRegister[componentId] instanceof GameLib.D3.Mesh) {
GameLib.EntityManager.Instance.idRegister[componentId].setParentMesh(data.component);
}
}
}
);
}
}
/**
* Maybe this component has a parent mesh
*/
if (
data.component.parentMesh &&
typeof data.component.parentMesh === 'string'
) {
GameLib.EntityManager.Instance.queryComponents(GameLib.Component.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.parentPhysicsWorld &&
typeof data.component.parentPhysicsWorld === 'string'
) {
GameLib.EntityManager.Instance.queryComponents(GameLib.Component.PHYSICS_WORLD).map(
function (world) {
if (data.component.parentPhysicsWorld === world.id) {
data.component.parentPhysicsWorld = 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.Component.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.Component.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.componentUpdateSubcription.remove();
this.componentClonedSubscription.remove();
this.registerDependenciesSubscription.remove();
this.componentRemoveSubscription.remove();
/**
* Parents
*/
this.parentSceneChangeSubscription.remove();
this.parentPhysicsWorldChangeSubscription.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();
};