silphius/app/ecs/ecs.js
2024-06-23 07:35:56 -05:00

401 lines
10 KiB
JavaScript

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,
};
}
}