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
JavaScript
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;