2024-06-10 22:42:30 -05:00
|
|
|
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 = [];
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const entityId in patch) {
|
|
|
|
const components = patch[entityId];
|
2024-06-10 22:42:30 -05:00
|
|
|
if (false === components) {
|
2024-06-11 19:10:57 -05:00
|
|
|
destroying.push(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
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) {
|
2024-06-11 19:10:57 -05:00
|
|
|
removing.push([entityId, componentsToRemove]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
if (this.$$entities[entityId]) {
|
|
|
|
updating.push([entityId, componentsToUpdate]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
else {
|
2024-06-11 19:10:57 -05:00
|
|
|
creating.push([entityId, componentsToUpdate]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this.destroyMany(destroying);
|
|
|
|
this.insertMany(updating);
|
|
|
|
this.removeMany(removing);
|
|
|
|
this.createManySpecific(creating);
|
|
|
|
}
|
|
|
|
|
|
|
|
create(components = {}) {
|
2024-06-11 19:10:57 -05:00
|
|
|
const [entityId] = this.createMany([components]);
|
|
|
|
return entityId;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
createMany(componentsList) {
|
|
|
|
const specificsList = [];
|
|
|
|
for (const components of componentsList) {
|
|
|
|
specificsList.push([this.$$caret++, components]);
|
|
|
|
}
|
|
|
|
return this.createManySpecific(specificsList);
|
|
|
|
}
|
|
|
|
|
|
|
|
createManySpecific(specificsList) {
|
2024-06-11 19:10:57 -05:00
|
|
|
const entityIds = [];
|
2024-06-10 22:42:30 -05:00
|
|
|
const creating = {};
|
|
|
|
for (let i = 0; i < specificsList.length; i++) {
|
2024-06-11 19:10:57 -05:00
|
|
|
const [entityId, components] = specificsList[i];
|
2024-06-10 22:42:30 -05:00
|
|
|
const componentKeys = [];
|
|
|
|
for (const key of Object.keys(components)) {
|
|
|
|
if (this.Types[key]) {
|
|
|
|
componentKeys.push(key);
|
|
|
|
}
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
entityIds.push(entityId);
|
|
|
|
this.rebuild(entityId, () => componentKeys);
|
2024-06-10 22:42:30 -05:00
|
|
|
for (const component of componentKeys) {
|
|
|
|
if (!creating[component]) {
|
|
|
|
creating[component] = [];
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
creating[component].push([entityId, components[component]]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
this.markChange(entityId, components);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
for (const i in creating) {
|
|
|
|
this.Types[i].createMany(creating[i]);
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
this.reindex(entityIds);
|
|
|
|
return entityIds;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
createSpecific(entityId, components) {
|
|
|
|
return this.createManySpecific([[entityId, components]]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
deindex(entityIds) {
|
2024-06-10 22:42:30 -05:00
|
|
|
for (let i = 0; i < this.$$systems.length; i++) {
|
2024-06-11 19:10:57 -05:00
|
|
|
this.$$systems[i].deindex(entityIds);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2024-06-11 19:10:57 -05:00
|
|
|
const entityId = view.getUint32(cursor, true);
|
|
|
|
if (!ecs.$$entities[entityId]) {
|
|
|
|
creating.set(entityId, {});
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
cursor += 4;
|
|
|
|
const componentCount = view.getUint16(cursor, true);
|
|
|
|
cursor += 2;
|
2024-06-11 19:10:57 -05:00
|
|
|
cursors.set(entityId, {});
|
2024-06-10 22:42:30 -05:00
|
|
|
const addedComponents = [];
|
|
|
|
for (let j = 0; j < componentCount; ++j) {
|
2024-06-11 19:10:57 -05:00
|
|
|
const componentId = view.getUint16(cursor, true);
|
2024-06-10 22:42:30 -05:00
|
|
|
cursor += 2;
|
2024-06-11 19:10:57 -05:00
|
|
|
const component = keys[componentId];
|
2024-06-10 22:42:30 -05:00
|
|
|
if (!component) {
|
2024-06-11 19:10:57 -05:00
|
|
|
throw new Error(`can't decode component ${componentId}`);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
if (!ecs.$$entities[entityId]) {
|
|
|
|
creating.get(entityId)[component] = false;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
else if (!ecs.$$entities[entityId].constructor.types.includes(component)) {
|
2024-06-10 22:42:30 -05:00
|
|
|
addedComponents.push(component);
|
|
|
|
if (!updating.has(component)) {
|
|
|
|
updating.set(component, []);
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
updating.get(component).push([entityId, false]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
cursors.get(entityId)[component] = cursor;
|
2024-06-10 22:42:30 -05:00
|
|
|
cursor += ecs.Types[component].constructor.schema.readSize(view, cursor);
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
if (addedComponents.length > 0 && ecs.$$entities[entityId]) {
|
|
|
|
ecs.rebuild(entityId, (types) => types.concat(addedComponents));
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
ecs.createManySpecific(Array.from(creating.entries()));
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const [component, entityIds] of updating) {
|
|
|
|
ecs.Types[component].createMany(entityIds);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const [entityId, components] of cursors) {
|
2024-06-10 22:42:30 -05:00
|
|
|
for (const component in components) {
|
2024-06-11 19:10:57 -05:00
|
|
|
ecs.Types[component].deserialize(entityId, view, components[component]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return ecs;
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
destroy(entityId) {
|
|
|
|
this.destroyMany([entityId]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
destroyAll() {
|
|
|
|
this.destroyMany(this.entities);
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
destroyMany(entityIds) {
|
2024-06-10 22:42:30 -05:00
|
|
|
const destroying = {};
|
2024-06-11 19:10:57 -05:00
|
|
|
this.deindex(entityIds);
|
|
|
|
for (const entityId of entityIds) {
|
|
|
|
if (!this.$$entities[entityId]) {
|
|
|
|
throw new Error(`can't destroy non-existent entity ${entityId}`);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const component of this.$$entities[entityId].constructor.types) {
|
2024-06-10 22:42:30 -05:00
|
|
|
if (!destroying[component]) {
|
|
|
|
destroying[component] = [];
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
destroying[component].push(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
this.$$entities[entityId] = undefined;
|
|
|
|
this.diff[entityId] = false;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
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};
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
get(entityId) {
|
|
|
|
return this.$$entities[entityId];
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
insert(entityId, components) {
|
|
|
|
this.insertMany([[entityId, components]]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
insertMany(entities) {
|
|
|
|
const inserting = {};
|
|
|
|
const unique = new Set();
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const [entityId, components] of entities) {
|
|
|
|
this.rebuild(entityId, (types) => [...new Set(types.concat(Object.keys(components)))]);
|
2024-06-10 22:42:30 -05:00
|
|
|
const diff = {};
|
|
|
|
for (const component in components) {
|
|
|
|
if (!inserting[component]) {
|
|
|
|
inserting[component] = [];
|
|
|
|
}
|
|
|
|
diff[component] = {};
|
2024-06-11 19:10:57 -05:00
|
|
|
inserting[component].push([entityId, components[component]]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
unique.add(entityId);
|
|
|
|
this.markChange(entityId, diff);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
for (const component in inserting) {
|
|
|
|
this.Types[component].insertMany(inserting[component]);
|
|
|
|
}
|
|
|
|
this.reindex(unique.values());
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
markChange(entityId, components) {
|
2024-06-10 22:42:30 -05:00
|
|
|
// Deleted?
|
|
|
|
if (false === components) {
|
2024-06-11 19:10:57 -05:00
|
|
|
this.diff[entityId] = false;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
// Created?
|
2024-06-11 19:10:57 -05:00
|
|
|
else if (!this.diff[entityId]) {
|
2024-06-10 22:42:30 -05:00
|
|
|
const filtered = {};
|
|
|
|
for (const type in components) {
|
|
|
|
filtered[type] = false === components[type]
|
|
|
|
? false
|
|
|
|
: this.Types[type].constructor.filterDefaults(components[type]);
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
this.diff[entityId] = filtered;
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
// Otherwise, merge.
|
|
|
|
else {
|
|
|
|
for (const type in components) {
|
2024-06-11 19:10:57 -05:00
|
|
|
this.diff[entityId][type] = false === components[type]
|
2024-06-10 22:42:30 -05:00
|
|
|
? false
|
|
|
|
: this.Types[type].mergeDiff(
|
2024-06-11 19:10:57 -05:00
|
|
|
this.diff[entityId][type] || {},
|
2024-06-10 22:42:30 -05:00
|
|
|
components[type],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
rebuild(entityId, types) {
|
2024-06-10 22:42:30 -05:00
|
|
|
let existing = [];
|
2024-06-11 19:10:57 -05:00
|
|
|
if (this.$$entities[entityId]) {
|
|
|
|
existing.push(...this.$$entities[entityId].constructor.types);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
const Class = this.$$entityFactory.makeClass(types(existing), this.Types);
|
2024-06-11 19:10:57 -05:00
|
|
|
this.$$entities[entityId] = new Class(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
reindex(entityIds) {
|
2024-06-10 22:42:30 -05:00
|
|
|
for (let i = 0; i < this.$$systems.length; i++) {
|
2024-06-11 19:10:57 -05:00
|
|
|
this.$$systems[i].reindex(entityIds);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-11 19:10:57 -05:00
|
|
|
remove(entityId, components) {
|
|
|
|
this.removeMany([[entityId, components]]);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
removeMany(entities) {
|
|
|
|
const removing = {};
|
|
|
|
const unique = new Set();
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const [entityId, components] of entities) {
|
|
|
|
unique.add(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
const diff = {};
|
|
|
|
for (const component of components) {
|
|
|
|
diff[component] = false;
|
|
|
|
if (!removing[component]) {
|
|
|
|
removing[component] = [];
|
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
removing[component].push(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
2024-06-11 19:10:57 -05:00
|
|
|
this.markChange(entityId, diff);
|
|
|
|
this.rebuild(entityId, (types) => types.filter((type) => !components.includes(type)));
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
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);
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const entityId of ecs.entities) {
|
|
|
|
const entity = ecs.get(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
entitiesWritten += 1;
|
2024-06-11 19:10:57 -05:00
|
|
|
view.setUint32(cursor, entityId, true);
|
2024-06-10 22:42:30 -05:00
|
|
|
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;
|
2024-06-11 19:10:57 -05:00
|
|
|
instance.serialize(entityId, view, cursor);
|
|
|
|
cursor += instance.sizeOf(entityId);
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
view.setUint16(componentsWrittenIndex, entityComponents.length, true);
|
|
|
|
}
|
|
|
|
view.setUint32(0, entitiesWritten, true);
|
|
|
|
return view;
|
|
|
|
}
|
|
|
|
|
|
|
|
setClean() {
|
|
|
|
this.diff = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
size() {
|
|
|
|
// # of entities.
|
|
|
|
let size = 4;
|
2024-06-11 19:10:57 -05:00
|
|
|
for (const entityId of this.entities) {
|
|
|
|
size += this.get(entityId).size();
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|