silphius/app/ecs/ecs.js
2024-06-11 19:10:57 -05:00

402 lines
11 KiB
JavaScript

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());
}
}
}