import Component from './component.js'; import EntityFactory from './entity-factory.js'; import System from './system.js'; export default class Ecs { $$caret = 1; diff = {}; static Types = {}; Types = {}; $$entities = {}; $$entityFactory = new EntityFactory(); $$systems = []; constructor() { const {Types} = this.constructor; for (const i in Types) { this.Types[i] = Component(Types[i]).wrap(i, this); } } addSystem(source) { const system = System.wrap(source, this); this.$$systems.push(system); system.reindex(this.entities); } apply(patch) { const creating = []; const destroying = []; const removing = []; const updating = []; for (const entityId in patch) { const components = patch[entityId]; if (false === components) { destroying.push(entityId); continue; } const componentsToRemove = []; const componentsToUpdate = {}; for (const i in components) { if (false === components[i]) { componentsToRemove.push(i); } else { componentsToUpdate[i] = components[i]; } } if (componentsToRemove.length > 0) { removing.push([entityId, componentsToRemove]); } if (this.$$entities[entityId]) { updating.push([entityId, componentsToUpdate]); } else { creating.push([entityId, componentsToUpdate]); } } this.destroyMany(destroying); this.insertMany(updating); this.removeMany(removing); this.createManySpecific(creating); } create(components = {}) { const [entityId] = this.createMany([components]); return entityId; } createMany(componentsList) { const specificsList = []; for (const components of componentsList) { specificsList.push([this.$$caret++, components]); } return this.createManySpecific(specificsList); } createManySpecific(specificsList) { const entityIds = []; const creating = {}; for (let i = 0; i < specificsList.length; i++) { const [entityId, components] = specificsList[i]; const componentKeys = []; for (const key of Object.keys(components)) { if (this.Types[key]) { componentKeys.push(key); } } entityIds.push(entityId); this.rebuild(entityId, () => componentKeys); for (const component of componentKeys) { if (!creating[component]) { creating[component] = []; } creating[component].push([entityId, components[component]]); } this.markChange(entityId, components); } for (const i in creating) { this.Types[i].createMany(creating[i]); } this.reindex(entityIds); return entityIds; } createSpecific(entityId, components) { return this.createManySpecific([[entityId, components]]); } deindex(entityIds) { for (let i = 0; i < this.$$systems.length; i++) { this.$$systems[i].deindex(entityIds); } } static deserialize(view) { const ecs = new this(); let cursor = 0; const count = view.getUint32(cursor, true); const keys = Object.keys(ecs.Types); cursor += 4; const creating = new Map(); const updating = new Map(); const cursors = new Map(); for (let i = 0; i < count; ++i) { const entityId = view.getUint32(cursor, true); if (!ecs.$$entities[entityId]) { creating.set(entityId, {}); } cursor += 4; const componentCount = view.getUint16(cursor, true); cursor += 2; cursors.set(entityId, {}); const addedComponents = []; for (let j = 0; j < componentCount; ++j) { const componentId = view.getUint16(cursor, true); cursor += 2; const component = keys[componentId]; if (!component) { throw new Error(`can't decode component ${componentId}`); } if (!ecs.$$entities[entityId]) { creating.get(entityId)[component] = false; } else if (!ecs.$$entities[entityId].constructor.types.includes(component)) { addedComponents.push(component); if (!updating.has(component)) { updating.set(component, []); } updating.get(component).push([entityId, false]); } cursors.get(entityId)[component] = cursor; cursor += ecs.Types[component].constructor.schema.readSize(view, cursor); } if (addedComponents.length > 0 && ecs.$$entities[entityId]) { ecs.rebuild(entityId, (types) => types.concat(addedComponents)); } } ecs.createManySpecific(Array.from(creating.entries())); for (const [component, entityIds] of updating) { ecs.Types[component].createMany(entityIds); } for (const [entityId, components] of cursors) { for (const component in components) { ecs.Types[component].deserialize(entityId, view, components[component]); } } return ecs; } destroy(entityId) { this.destroyMany([entityId]); } destroyAll() { this.destroyMany(this.entities); } destroyMany(entityIds) { const destroying = {}; this.deindex(entityIds); for (const entityId of entityIds) { if (!this.$$entities[entityId]) { throw new Error(`can't destroy non-existent entity ${entityId}`); } for (const component of this.$$entities[entityId].constructor.types) { if (!destroying[component]) { destroying[component] = []; } destroying[component].push(entityId); } this.$$entities[entityId] = undefined; this.diff[entityId] = false; } for (const i in destroying) { this.Types[i].destroyMany(destroying[i]); } } get entities() { const it = Object.values(this.$$entities).values(); return { [Symbol.iterator]() { return this; }, next: () => { let result = it.next(); while (!result.done && !result.value) { result = it.next(); } if (result.done) { return {done: true, value: undefined}; } return {done: false, value: result.value.id}; }, }; } get(entityId) { return this.$$entities[entityId]; } insert(entityId, components) { this.insertMany([[entityId, components]]); } insertMany(entities) { const inserting = {}; const unique = new Set(); for (const [entityId, components] of entities) { this.rebuild(entityId, (types) => [...new Set(types.concat(Object.keys(components)))]); const diff = {}; for (const component in components) { if (!inserting[component]) { inserting[component] = []; } diff[component] = {}; inserting[component].push([entityId, components[component]]); } unique.add(entityId); this.markChange(entityId, diff); } for (const component in inserting) { this.Types[component].insertMany(inserting[component]); } this.reindex(unique.values()); } markChange(entityId, components) { // Deleted? if (false === components) { this.diff[entityId] = false; } // Created? else if (!this.diff[entityId]) { const filtered = {}; for (const type in components) { filtered[type] = false === components[type] ? false : this.Types[type].constructor.filterDefaults(components[type]); } this.diff[entityId] = filtered; } // Otherwise, merge. else { for (const type in components) { this.diff[entityId][type] = false === components[type] ? false : this.Types[type].mergeDiff( this.diff[entityId][type] || {}, components[type], ); } } } rebuild(entityId, types) { let existing = []; if (this.$$entities[entityId]) { existing.push(...this.$$entities[entityId].constructor.types); } const Class = this.$$entityFactory.makeClass(types(existing), this.Types); this.$$entities[entityId] = new Class(entityId); } reindex(entityIds) { for (let i = 0; i < this.$$systems.length; i++) { this.$$systems[i].reindex(entityIds); } } remove(entityId, components) { this.removeMany([[entityId, components]]); } removeMany(entities) { const removing = {}; const unique = new Set(); for (const [entityId, components] of entities) { unique.add(entityId); const diff = {}; for (const component of components) { diff[component] = false; if (!removing[component]) { removing[component] = []; } removing[component].push(entityId); } this.markChange(entityId, diff); this.rebuild(entityId, (types) => types.filter((type) => !components.includes(type))); } for (const component in removing) { this.Types[component].destroyMany(removing[component]); } this.reindex(unique.values()); } removeSystem(SystemLike) { const index = this.$$systems.findIndex((system) => SystemLike === system.source); if (-1 !== index) { this.$$systems.splice(index, 1); } } static serialize(ecs, view) { if (!view) { view = new DataView(new ArrayBuffer(ecs.size())); } let cursor = 0; let entitiesWritten = 0; cursor += 4; const keys = Object.keys(ecs.Types); for (const entityId of ecs.entities) { const entity = ecs.get(entityId); entitiesWritten += 1; view.setUint32(cursor, entityId, true); cursor += 4; const entityComponents = entity.constructor.types; view.setUint16(cursor, entityComponents.length, true); const componentsWrittenIndex = cursor; cursor += 2; for (const component of entityComponents) { const instance = ecs.Types[component]; view.setUint16(cursor, keys.indexOf(component), true); cursor += 2; instance.serialize(entityId, view, cursor); cursor += instance.sizeOf(entityId); } view.setUint16(componentsWrittenIndex, entityComponents.length, true); } view.setUint32(0, entitiesWritten, true); return view; } setClean() { this.diff = {}; } size() { // # of entities. let size = 4; for (const entityId of this.entities) { size += this.get(entityId).size(); } return size; } system(SystemLike) { return this.$$systems.find((system) => SystemLike === system.source) } tick(elapsed) { for (let i = 0; i < this.$$systems.length; i++) { this.$$systems[i].tick(elapsed); } for (let i = 0; i < this.$$systems.length; i++) { this.$$systems[i].finalize(elapsed); } this.tickDestruction(); } tickDestruction() { const unique = new Set(); for (let i = 0; i < this.$$systems.length; i++) { for (let j = 0; j < this.$$systems[i].destroying.length; j++) { unique.add(this.$$systems[i].destroying[j]); } this.$$systems[i].tickDestruction(); } if (unique.size > 0) { this.destroyMany(unique.values()); } } }