import EntityFactory from './entity-factory.js'; import Schema from './schema.js'; import {Encoder, Decoder} from '@msgpack/msgpack'; const decoder = new Decoder(); const encoder = new Encoder(); export default class Ecs { $$caret = 1; Components = {}; diff = {}; Systems = {}; $$entities = {}; $$entityFactory = new EntityFactory(); constructor({Systems, Components} = {}) { for (const componentName in Components) { this.Components[componentName] = new Components[componentName](this); } for (const systemName in Systems) { this.Systems[systemName] = new Systems[systemName](this); } } apply(patch) { const creating = []; const destroying = []; const removing = []; const updating = []; for (const entityIdString in patch) { const entityId = parseInt(entityIdString); const components = patch[entityId]; if (false === components) { destroying.push(entityId); continue; } const componentsToRemove = []; const componentsToUpdate = {}; for (const componentName in components) { if (false === components[componentName]) { componentsToRemove.push(componentName); } else { componentsToUpdate[componentName] = components[componentName]; } } 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); } changed(criteria) { const it = Object.entries(this.diff).values(); return { [Symbol.iterator]() { return this; }, next: () => { let result = it.next(); while (!result.done) { for (const componentName of criteria) { if (!(componentName in result.value[1])) { result = it.next(); continue; } } break; } if (result.done) { return {done: true, value: undefined}; } return {done: false, value: this.get(result.value[0])}; }, }; } 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 componentNames = []; for (const componentName in components) { if (this.Components[componentName]) { componentNames.push(componentName); } } entityIds.push(entityId); this.rebuild(entityId, () => componentNames); for (const componentName of componentNames) { if (!creating[componentName]) { creating[componentName] = []; } creating[componentName].push([entityId, components[componentName]]); } this.markChange(entityId, components); } for (const i in creating) { this.Components[i].createMany(creating[i]); } this.reindex(entityIds); return entityIds; } createSpecific(entityId, components) { return this.createManySpecific([[entityId, components]]); } deindex(entityIds) { for (const systemName in this.Systems) { this.Systems[systemName].deindex(entityIds); } } static deserialize(ecs, view) { const componentNames = Object.keys(ecs.Components); const {entities, systems} = decoder.decode(view.buffer); for (const system of systems) { const System = ecs.system(system); if (System) { System.active = true; } } const specifics = []; let max = 1; for (const id in entities) { max = Math.max(max, parseInt(id)); specifics.push([ parseInt(id), Object.fromEntries( Object.entries(entities[id]) .filter(([componentName]) => componentNames.includes(componentName)), ), ]); } ecs.$$caret = max + 1; ecs.createManySpecific(specifics); 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 componentName of this.$$entities[entityId].constructor.componentNames) { if (!destroying[componentName]) { destroying[componentName] = []; } destroying[componentName].push(entityId); } this.$$entities[entityId] = undefined; this.diff[entityId] = false; } for (const i in destroying) { this.Components[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, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]); const diff = {}; for (const componentName in components) { if (!inserting[componentName]) { inserting[componentName] = []; } diff[componentName] = {}; inserting[componentName].push([entityId, components[componentName]]); } unique.add(entityId); this.markChange(entityId, diff); } for (const componentName in inserting) { this.Components[componentName].insertMany(inserting[componentName]); } 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 componentName in components) { filtered[componentName] = false === components[componentName] ? false : components[componentName]; } this.diff[entityId] = filtered; } // Otherwise, merge. else { for (const componentName in components) { this.diff[entityId][componentName] = false === components[componentName] ? false : this.Components[componentName].mergeDiff( this.diff[entityId][componentName] || {}, components[componentName], ); } } } rebuild(entityId, componentNames) { let existing = []; if (this.$$entities[entityId]) { existing.push(...this.$$entities[entityId].constructor.componentNames); } const Class = this.$$entityFactory.makeClass(componentNames(existing), this.Components); this.$$entities[entityId] = new Class(entityId); } reindex(entityIds) { for (const systemName in this.Systems) { this.Systems[systemName].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 componentName of components) { diff[componentName] = false; if (!removing[componentName]) { removing[componentName] = []; } removing[componentName].push(entityId); } this.markChange(entityId, diff); this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type))); } for (const componentName in removing) { this.Components[componentName].destroyMany(removing[componentName]); } this.reindex(unique.values()); } static serialize(ecs, view) { const buffer = encoder.encode(ecs.toJSON()); if (!view) { view = new DataView(new ArrayBuffer(buffer.length)); } (new Uint8Array(view.buffer)).set(buffer); return view; } setClean() { this.diff = {}; } size() { let size = 0; // # of components. size += 2; for (const type in this.Components) { size += Schema.sizeOf(type, {type: 'string'}); } // # of entities. size += 4; for (const entityId of this.entities) { size += this.get(entityId).size(); } return size; } system(systemName) { return this.Systems[systemName]; } tick(elapsed) { for (const systemName in this.Systems) { if (this.Systems[systemName].active) { this.Systems[systemName].tick(elapsed); } } for (const systemName in this.Systems) { if (this.Systems[systemName].active) { this.Systems[systemName].finalize(elapsed); } } this.tickDestruction(); } tickDestruction() { const unique = new Set(); for (const systemName in this.Systems) { const System = this.Systems[systemName]; if (System.active) { for (let j = 0; j < System.destroying.length; j++) { unique.add(System.destroying[j]); } System.tickDestruction(); } } if (unique.size > 0) { this.destroyMany(unique.values()); } } toJSON() { const entities = {}; for (const id in this.$$entities) { if (this.$$entities[id]) { entities[id] = this.$$entities[id].toJSON(); } } const systems = []; for (const systemName in this.Systems) { if (this.Systems[systemName].active) { systems.push(systemName); } } return { entities, systems, }; } }