import EntityManager from './entity-manager';
import Promise from 'bluebird';
import Symbol from 'es6-symbol';
import EventEmitter from 'eventemitter3';
import values from 'lodash/values';
import forEach from 'lodash/forEach';
import set from 'lodash/set';
import {
requires,
assertDisposable,
assertEntityManager,
assertString,
assertNumber,
assertFunction,
} from './asserts';
import createClass from './util/create-class';
import decorateObject from './util/decorate-object';
import subscribableMixin from './mixins/subscribable';
import disposableMixin from './mixins/disposable';
import disposableDecorator from './mixins/decorator/disposable-decorator';
const FIELDS = {
name: Symbol('name'),
requirements: Symbol('requirements'),
state: Symbol('state'),
interval: Symbol('interval'),
executedOn: Symbol('executedOn'),
latency: Symbol('latency'),
manager: Symbol('manager'),
executor: Symbol('executor'),
emitter: Symbol('emitter'),
};
function onError(error) {
this[FIELDS.emitter].emit('error', error);
}
function onDispose() {
this[FIELDS.emitter].emit('system:dispose');
this[FIELDS.emitter].removeAllListeners();
}
function onSourceAdd(entity) {
this.watchEntity(entity);
}
function onSourceAddRequirement(entity) {
this.watchEntity(entity);
}
function onScopeChangeRequirement(entity, component, current, previous) {
// TODO: Add capabilities to index entities.
this[FIELDS.emitter].emit('entity:change', entity, component, current, previous);
}
function onScopeRemoveRequirement(entity) {
this.unwatchEntity(entity);
}
/**
* @class
* A system defines an operation that should be performed each tick of the engine. A system will
* only affect entities that own the required system components. A system will watch and unwatch
* an entity as the required components are added and removed from it. A system is created based
* on a configuration that is passed to the constructor.
* @example
* #Creating a new system.
* const system = new System({
* name: 'spawn',
* requirements: ['player', 'spawn'],
* interval: 16,
* executor: ({ entity }) => {
* entity.addComponents(new Component('position', { x: 0, y: 0, z:0 }));
* entity.removeComponents('spawn');
* const name = entity.getComponents('player').getState('name');
* console.log(`${name} has come into the world.`);
* },
* });
* @param {Object} config - The system configuration.
* @param {!string} config.name - The system name.
* @param {string[]} [config.requirements] - The component types that are required by the system.
* @param {number} [config.interval=16] - The number of milliseconds between executions.
* @param {!function} config.executor - The function to execute.
* @access public
* @mixes Subscribable
* @mixes Disposable
*/
const System = createClass({
/**
* A system:watch event occurs when the systems watches an entity.
* @event system:watch
* @param {Entity} entity - The entity that has been watched.
* @instance
* @memberof System
*/
/**
* A system:unwatch event occurs when the systems unwatches an entity.
* @event system:unwatch
* @param {Entity} entity - The entity that has been unwatched.
* @instance
* @memberof System
*/
mixins: [
decorateObject(subscribableMixin(FIELDS.emitter), disposableDecorator),
disposableMixin(values(FIELDS), onDispose),
],
constructor({ name, requirements = [], manager = new EntityManager(), interval = 16, executor }) {
requires('name', name);
requires('executor', executor);
assertString(name);
forEach(requirements, assertString);
assertFunction(executor);
assertEntityManager(manager);
assertNumber(interval);
/**
* A literal that identifies the system.
* @member {string} name
* @instance
* @memberof System
* @private
*/
this[FIELDS.name] = name;
/**
* The component types that an entity must have to be watched by the system.
* @member {string[]} requirements
* @instance
* @memberof System
* @private
*/
this[FIELDS.requirements] = requirements;
/**
* The state of the system.
* @member {string} state
* @instance
* @memberof System
* @private
*/
this[FIELDS.state] = 'PENDING';
/**
* The number milliseconds between executions.
* @member {number} interval
* @instance
* @memberof System
* @private
*/
this[FIELDS.interval] = interval;
/**
* The number of milliseconds that the system has fallen behind.
* @member {number} latency
* @instance
* @memberof System
* @private
*/
this[FIELDS.latency] = null;
/**
* The last time the system executed.
* @member {number} executedOn
* @instance
* @memberof System
* @private
*/
this[FIELDS.executedOn] = null;
/**
* The entity manager for the scope of entities the system is watching.
* @member {EntityManager} manager
* @instance
* @memberof System
* @private
*/
this[FIELDS.manager] = manager;
/**
* The function that is executed each update.
* @member {function} executor
* @instance
* @memberof System
* @private
*/
this[FIELDS.executor] = executor;
/**
* The event emitter.
* @member {EventEmitter} emitter
* @instance
* @memberof System
* @private
* @ignore
*/
this[FIELDS.emitter] = new EventEmitter();
manager.subscribe('error', onError.bind(this));
},
/**
* Get the system name.
* @returns {string} The system name.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getName() {
assertDisposable(this);
return this[FIELDS.name];
},
/**
* Get the system requirements.
* @returns {string[]} The system requirements.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getRequirements() {
assertDisposable(this);
return this[FIELDS.requirements];
},
/**
* Get the system state.
* @returns {string} The system state.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getState() {
assertDisposable(this);
return this[FIELDS.state];
},
/**
* Get the execution interval.
* @returns {number} The execution interval.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getInterval() {
assertDisposable(this);
return this[FIELDS.interval];
},
/**
* Get the time last executed.
* @returns {number} The last execution time.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getExecutedOn() {
assertDisposable(this);
return this[FIELDS.executedOn];
},
/**
* Get the entities in scope of the system.
* @returns {Entity[]} The entity scope of the system.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
getScope() {
assertDisposable(this);
return this[FIELDS.manager].getEntities();
},
/**
* Will add the entity to the scope of entities that are updated by the system, if the entity meets
* the system requirements.
* @param {Entity} entity - The entity to watch.
* @returns {System} Itself
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @emits System#system:watch
* @instance
* @memberof System
*/
watchEntity(entity) {
const requirements = this[FIELDS.requirements];
const manager = this[FIELDS.manager];
const emitter = this[FIELDS.emitter];
if (entity.hasComponents(...requirements)) {
manager.addEntities(entity);
emitter.emit('system:watch', entity);
}
return this;
},
/**
* Will remove the entity from the scope of entities that are updated by the system, if
* the entity no longer meets the system requirements.
* @param {Entity} entity - The entity to watch.
* @returns {System} Itself
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @emits System#system:watch
* @instance
* @memberof System
*/
unwatchEntity(entity) {
const requirements = this[FIELDS.requirements];
const manager = this[FIELDS.manager];
const emitter = this[FIELDS.emitter];
if (!entity.hasComponents(...requirements)) {
manager.removeEntities(entity);
emitter.emit('system:unwatch', entity);
}
return this;
},
/**
* Initialize the system by providing it with a source entity manager. The system sets up the
* appropriate listeners that is will need to identify the entities that are within scope of
* the system requirements and then identify the initial scope from the already existing entities.
* @param {EntityManager} source - The source entity manager for the system.
* @returns {Promise} The system initialization.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @throws {TypeError} Will throw this error is the source is not an EntityManager.
* @instance
* @memberof System
*/
init(source) {
assertDisposable(this);
const self = this;
const requirements = this[FIELDS.requirements];
const manager = this[FIELDS.manager];
return new Promise((resolve) => {
assertEntityManager(source);
source.subscribe('manager:add', onSourceAdd.bind(this));
forEach(requirements, (requirement) => {
source.subscribe(`entity:add:${requirement}`, onSourceAddRequirement.bind(self));
manager.subscribe(`entity:change:${requirement}`, onScopeChangeRequirement.bind(self));
manager.subscribe(`entity:remove:${requirement}`, onScopeRemoveRequirement.bind(self));
});
if (source.length() > 0) {
source.forEach((entity) => this.watchEntity(entity));
}
self[FIELDS.executedOn] = Date.now();
resolve();
});
},
/**
* Executes the system executor for each entity within scope. The system will pass a context object to
* the executor that exposes resources to be used within the executor.
* @param {Object} context - A context from a parent scopes.
* @returns {Promise} The system execution.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof System
*/
execute(context = {}) {
assertDisposable(this);
const currentTime = Date.now();
const executedOn = this[FIELDS.executedOn];
const delta = currentTime - executedOn;
const interval = this[FIELDS.interval];
const manager = this[FIELDS.manager];
const latency = delta - interval;
const executor = this[FIELDS.executor];
const executions = [];
if (delta < interval) {
return Promise.resolve();
}
set(context, 'system.entityService.get', this[FIELDS.manager].getEntities);
set(context, 'system.entityService.find', this[FIELDS.manager].findEntities);
set(context, 'system.entityService.forEach', this[FIELDS.manager].forEach);
set(context, 'system.entityService.length', this[FIELDS.manager].length);
set(context, 'time.delta', delta);
set(context, 'time.latency', latency);
manager.forEach((entity) => {
set(context, 'entity', entity);
executions.push(executor(context));
});
return Promise
.all(executions)
.then(() => {
this[FIELDS.latency] = latency;
this[FIELDS.executedOn] = Date.now();
// TODO: Emit a report for profiling.
});
},
});
export default System;