src/systems/EntitySystem/index.js
import System from '../System';
import Entity from './Entity';
import Component from './Component';
import { mat4 } from '../../utils/gl-matrix';
export {
Entity,
Component
};
const identityMatrix = mat4.create();
/**
* Manages entities on scene.
*
* @example
* const system = new EntitySystem();
* class Hello extends Component { constructor() { this.hello = null; } onAction(name) { console.log(name); } }
* system.registerComponent('Hello', () => new Hello());
* system.root = system.buildEntity({ name: 'hey', components: { Hello: { hello: 'world' } } });
* system.updateTransforms();
* system.performAction('hi');
*/
export default class EntitySystem extends System {
/** @type {Entity|null} */
get root() {
return this._root;
}
/** @type {Entity|null} */
set root(value) {
if (!!value && !(value instanceof Entity)) {
throw new Error('`value` is not type of Entity!');
}
const { _root } = this;
if (!!_root) {
_root.dispose();
}
this._root = value;
if (!!value) {
value._setOwner(this);
}
}
/** @type {boolean} */
get triggerEvents() {
return this._triggerEvents;
}
/**
* Constructor.
*
* @param {boolean} triggerEvents - Tells if system should trigger events.
*/
constructor(triggerEvents = true) {
super();
this._root = new Entity();
this._components = new Map();
this._triggerEvents = !!triggerEvents;
}
/**
* Destructor (dispose internal resources).
*
* @example
* system.dispose();
* system = null;
*/
dispose() {
const { _root } = this;
if (!!_root) {
_root.dispose();
}
this._components.clear();
this._root = null;
}
/**
* Register new component factory.
*
* @param {string} typename - Component type name.
* @param {Function} componentConstructor - Component factory function (it should return new instance of component).
*
* @example
* class MyComponent extends Component { static factory() { return new MyComponent(); } }
* system.registerComponent('MyComponent', MyComponent.factory);
*/
registerComponent(typename, componentConstructor) {
if (typeof typename !== 'string') {
throw new Error('`typename` is not type of String!');
}
if (!(componentConstructor instanceof Function)) {
throw new Error('`componentConstructor` is not type of Function!');
}
const { _components } = this;
if (_components.has(typename)) {
throw new Error(`There is already registered component: ${typename}`);
}
_components.set(typename, componentConstructor);
}
/**
* Unregister component factory.
*
* @param {string} typename - Component type name.
*
* @example
* system.unregisterComponent('MyComponent');
*/
unregisterComponent(typename) {
if (typeof typename !== 'string') {
throw new Error('`typename` is not type of String!');
}
if (!this._components.delete(typename)) {
throw new Error(`There is no registered component: ${typename}`);
}
}
/**
* Create component instnace by it's type name and apply properties to it.
*
* @param {string} typename - Component type name.
* @param {*} properties - Object with key-value pairs of component properties.
*
* @return {Component} Component instance.
*
* @example
* class Hello extends Component { constructor() { this.hello = null; } }
* system.registerComponent('Hello', () => new Hello());
* const hello = system.createComponent('Hello', { hello: 'world' });
* console.log(hello.hello);
*/
createComponent(typename, properties) {
if (typeof typename !== 'string') {
throw new Error('`typename` is not type of String!');
}
const factory = this._components.get(typename);
if (!factory) {
throw new Error(`There is no registered component: ${typename}`);
}
const component = factory();
if (!component) {
throw new Error(`Cannot create proper component: ${typename}`);
}
if (!!properties) {
for (const name in properties) {
component.onPropertySetup(name, properties[name]);
}
}
return component;
}
/**
* Build entity from JSON data.
*
* @param {*} data - JSON representation of serialized entity data.
*
* @return {Entity} Entity instance.
*
* @example
* class Hello extends Component { constructor() { this.hello = null; } }
* system.registerComponent('Hello', () => new Hello());
* const entity = system.buildEntity({ name: 'hey', components: { Hello: { hello: 'world' } } });
* const hello = entity.getComponent('Hello');
* console.log(hello.hello);
*/
buildEntity(data) {
if (!data) {
throw new Error('`data` cannot be null!');
}
const { components, children } = data;
const result = new Entity();
const options = {};
if ('name' in data) {
options.name = data.name;
}
if ('tag' in data) {
options.tag = data.tag;
}
if ('active' in data) {
options.active = data.active;
}
if ('meta' in data) {
options.meta = data.meta;
}
if ('transform' in data) {
options.transform = data.transform;
}
result.deserialize(options);
if (!!components) {
for (const name in components) {
result.attachComponent(name, this.createComponent(name, components[name]));
}
}
if (!!children) {
for (const meta of children.values()) {
this.buildEntity(meta).parent = result;
}
}
return result;
}
/**
* Perform action on root entity.
* Actions are events that are performed by entity components and are passed down to it's children.
*
* @param {string} name - action name.
* @param {*} args - Action arguments.
*
* @example
* class Hi extends Component { onAction(name, what) { if (name === 'hi') console.log(what); } }
* system.registerComponent('Hi', () => new Hi());
* system.root = system.buildEntity({ components: { Hi: {} } });
* system.performAction('hi', 'hello');
*/
performAction(name, ...args) {
const { _root } = this;
if (!!_root) {
_root.performAction(name, ...args);
}
}
/**
* Update entities transforms.
*/
updateTransforms() {
const { _root } = this;
if (!!_root) {
_root.updateTransforms(identityMatrix);
}
}
}