UNPKG

entity-component-system

Version:

An implementation of the Entity component system (ECS) pattern used commonly in video games.

256 lines (234 loc) 7.97 kB
var ObjectPool = require("./object-pool"); function EntityPool() { this.entities = {}; this.nextId = 0; this.entityPool = new ObjectPool(function() { return { id: this.nextId++ }; }.bind(this)); this.componentPools = {}; this.resetFunctions = {}; this.searchToComponents = {}; this.componentToSearches = {}; this.searchResults = {}; this.callbacks = {}; } EntityPool.prototype.create = function() { var entity = this.entityPool.alloc(); this.entities[entity.id] = entity; return entity.id; }; EntityPool.prototype.destroy = function(id) { var entity = this.entities[id]; Object.keys(entity).forEach(function(component) { if (component === "id") { return; } this.removeComponent(id, component); }.bind(this)); delete this.entities[id]; this.entityPool.free(entity); }; EntityPool.prototype.registerComponent = function(component, factory, reset, size) { this.componentPools[component] = new ObjectPool(factory, size); this.resetFunctions[component] = reset; }; // private EntityPool.prototype.resetComponent = function(id, component) { var reset = this.resetFunctions[component]; if (typeof reset === "function") { reset(this.entities[id][component]); } }; EntityPool.prototype.getComponent = function(id, component) { return this.entities[id][component]; }; EntityPool.prototype.removeComponent = function(id, component) { var oldValue = this.entities[id][component]; if (oldValue === undefined) { return; } for (var i = 0; i < this.componentToSearches[component].length; i++) { var search = this.componentToSearches[component][i]; removeFromArray(this.searchResults[search], id); } this.fireCallback("remove", id, component, oldValue); if (!isPrimitive(oldValue)) { this.resetComponent(id, component); this.componentPools[component].free(oldValue); } delete this.entities[id][component]; }; EntityPool.prototype.addComponent = function(id, component) { if (!this.componentPools[component]) { throw new Error( "You can't call EntityPool.prototype.addComponent(" + id + ", \"" + component + "\") " + "for a component name that hasn't been registered with " + "EntityPool.prototype.registerComponent(component, factory[, reset][, size])." ); } var predefinedValue = this.entities[id][component]; if (predefinedValue && !isPrimitive(predefinedValue)) { this.resetComponent(id, component); return predefinedValue; } var value = this.componentPools[component].alloc(); this.setComponentValue(id, component, value); return value; }; EntityPool.prototype.setComponent = function(id, component, value) { if (!isPrimitive(value)) { throw new TypeError( "You can't call EntityPool.prototype.setComponent(" + id + ", \"" + component + "\", " + JSON.stringify(value) + ") with " + "a value that isn't of a primitive type (i.e. null, undefined, boolean, " + "number, string, or symbol). For objects or arrays, use " + "EntityPool.prototype.addComponent(id, component) and modify " + "the result it returns." ); } if (!isPrimitive(this.entities[id][component])) { throw new Error( "You can't set a non-primitive type component \"" + component + "\" to a primitive value. " + "If you must do this, remove the existing component first with " + "EntityPool.prototype.removeComponent(id, component)." ); } if (typeof value === "undefined") { this.removeComponent(id, component); } else { this.setComponentValue(id, component, value); } }; // private EntityPool.prototype.setComponentValue = function(id, component, value) { var existingValue = this.entities[id][component]; if (typeof existingValue !== "undefined" && existingValue === value) { return; } this.entities[id][component] = value; if (typeof existingValue === "undefined") { if (this.searchToComponents[component] === undefined) { this.mapSearch(component, [component]); } for (var i = 0; i < this.componentToSearches[component].length; i++) { var search = this.componentToSearches[component][i]; if (objectHasProperties(this.searchToComponents[search], this.entities[id])) { this.searchResults[search].push(id); } } this.fireCallback("add", id, component, value); } }; // private EntityPool.prototype.addCallback = function(type, component, callback) { this.callbacks[type] = this.callbacks[type] || {}; this.callbacks[type][component] = this.callbacks[type][component] || []; this.callbacks[type][component].push(callback); }; // private EntityPool.prototype.fireCallback = function(type, id, component) { if (this.callbackQueue) { this.callbackQueue.push(Array.prototype.slice.call(arguments, 0)); return; } var cbs = this.callbacks[type] || {}; var ccbs = cbs[component] || []; var args = Array.prototype.slice.call(arguments, 3); for (var i = 0; i < ccbs.length; i++) { ccbs[i].apply(this, [id, component].concat(args)); } }; // private EntityPool.prototype.fireQueuedCallbacks = function() { var queue = this.callbackQueue || []; delete this.callbackQueue; for (var i = 0; i < queue.length; i++) { this.fireCallback.apply(this, queue[i]); } }; EntityPool.prototype.onAddComponent = function(component, callback) { this.addCallback("add", component, callback); }; EntityPool.prototype.onRemoveComponent = function(component, callback) { this.addCallback("remove", component, callback); }; EntityPool.prototype.find = function(search) { return this.searchResults[search] || []; }; // private EntityPool.prototype.mapSearch = function(search, components) { if (this.searchToComponents[search] !== undefined) { throw "the search \"" + search + "\" was already registered"; } this.searchToComponents[search] = components.slice(0); for (var i = 0; i < components.length; i++) { var c = components[i]; if (this.componentToSearches[c] === undefined) { this.componentToSearches[c] = [search]; } else { this.componentToSearches[c].push(search); } } this.searchResults[search] = []; }; EntityPool.prototype.registerSearch = function(search, components) { this.mapSearch(search, components); this.searchResults[search] = objectValues(this.entities) .filter(objectHasProperties.bind(undefined, components)) .map(entityId); }; EntityPool.prototype.load = function(entities) { this.callbackQueue = []; entities.forEach(function(entity) { var id = entity.id; var allocatedEntity = this.entityPool.alloc(); allocatedEntity.id = id; this.entities[id] = allocatedEntity; if (this.nextId <= id) { this.nextId = id + 1; } Object.keys(entity).forEach(function(component) { if (component === "id") { return; } var valueToLoad = entity[component]; if (isPrimitive(valueToLoad)) { this.setComponent(id, component, valueToLoad); return; } var newComponentObject = this.addComponent(id, component); Object.keys(valueToLoad).forEach(function(key) { newComponentObject[key] = valueToLoad[key]; }); }.bind(this)); }.bind(this)); this.fireQueuedCallbacks(); }; EntityPool.prototype.save = function() { return objectValues(this.entities); }; function removeFromArray(array, item) { var i = array.indexOf(item); if (i !== -1) { array.splice(i, 1); } return array; } function entityId(entity) { return entity.id; } function objectHasProperties(properties, obj) { return properties.every(Object.prototype.hasOwnProperty.bind(obj)); } function objectValues(obj) { return Object.keys(obj).map(function(key) { return obj[key]; }); } /* returns true if the value is a primitive * type a.k.a. null, undefined, boolean, * number, string, or symbol. */ function isPrimitive(value) { return typeof value !== "object" || value === null; } module.exports = EntityPool;