silphius/app/ecs/ecs.js

613 lines
16 KiB
JavaScript
Raw Normal View History

2024-06-14 15:18:55 -05:00
import {Encoder, Decoder} from '@msgpack/msgpack';
2024-07-21 01:37:40 -05:00
import {LRUCache} from 'lru-cache';
2024-08-01 13:22:48 -05:00
import {withResolvers} from '@/util/promise.js';
2024-07-21 01:37:40 -05:00
import Script from '@/util/script.js';
2024-07-21 03:01:12 -05:00
import EntityFactory from './entity-factory.js';
2024-07-21 01:37:40 -05:00
const cache = new LRUCache({
max: 128,
});
2024-06-14 15:18:55 -05:00
const decoder = new Decoder();
const encoder = new Encoder();
2024-06-10 22:42:30 -05:00
2024-07-21 01:37:40 -05:00
const textDecoder = new TextDecoder();
2024-06-10 22:42:30 -05:00
export default class Ecs {
$$caret = 1;
2024-06-15 19:38:49 -05:00
Components = {};
2024-07-02 17:46:31 -05:00
deferredChanges = {}
2024-07-26 10:31:58 -05:00
$$destructionDependencies = new Map();
2024-07-21 03:54:03 -05:00
2024-08-03 15:25:30 -05:00
$$detached = new Set();
2024-06-10 22:42:30 -05:00
diff = {};
$$entities = {};
$$entityFactory = new EntityFactory();
2024-08-04 20:54:56 -05:00
Systems = {};
2024-06-15 19:38:49 -05:00
constructor({Systems, Components} = {}) {
for (const componentName in Components) {
this.Components[componentName] = new Components[componentName](this);
2024-06-14 15:18:55 -05:00
}
2024-06-15 19:38:49 -05:00
for (const systemName in Systems) {
this.Systems[systemName] = new Systems[systemName](this);
2024-06-10 22:42:30 -05:00
}
}
2024-07-26 10:31:58 -05:00
addDestructionDependency(id, promise) {
if (!this.$$destructionDependencies.has(id)) {
2024-07-26 11:36:32 -05:00
this.$$destructionDependencies.set(id, {promises: new Set()})
2024-07-26 10:31:58 -05:00
}
2024-07-26 11:36:32 -05:00
const {promises} = this.$$destructionDependencies.get(id);
promises.add(promise);
2024-07-26 10:31:58 -05:00
promise.then(() => {
2024-07-26 11:36:32 -05:00
promises.delete(promise);
if (!this.$$destructionDependencies.get(id)?.resolvers) {
2024-07-26 10:31:58 -05:00
this.$$destructionDependencies.delete(id);
}
});
}
2024-06-27 06:28:00 -05:00
async apply(patch) {
2024-06-10 22:42:30 -05:00
const creating = [];
2024-08-03 15:25:30 -05:00
const destroying = new Set();
2024-07-30 11:46:04 -05:00
const inserting = [];
2024-06-10 22:42:30 -05:00
const removing = [];
const updating = [];
2024-06-15 20:59:11 -05:00
for (const entityIdString in patch) {
const entityId = parseInt(entityIdString);
2024-06-11 19:10:57 -05:00
const components = patch[entityId];
2024-06-10 22:42:30 -05:00
if (false === components) {
2024-08-03 15:25:30 -05:00
destroying.add(entityId);
2024-06-10 22:42:30 -05:00
continue;
}
const componentsToRemove = [];
const componentsToUpdate = {};
2024-06-15 19:38:49 -05:00
for (const componentName in components) {
if (false === components[componentName]) {
componentsToRemove.push(componentName);
2024-06-10 22:42:30 -05:00
}
else {
2024-06-15 19:38:49 -05:00
componentsToUpdate[componentName] = components[componentName];
2024-06-10 22:42:30 -05:00
}
}
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-08-05 02:23:41 -05:00
if (this.$$entities[entityIdString]) {
const entity = this.$$entities[entityIdString];
let isInserting = false;
2024-07-30 11:46:04 -05:00
const entityInserts = {};
2024-08-05 02:23:41 -05:00
let isUpdating = false;
2024-07-30 11:46:04 -05:00
const entityUpdates = {};
for (const componentName in componentsToUpdate) {
if (entity[componentName]) {
entityUpdates[componentName] = componentsToUpdate[componentName];
2024-08-05 02:23:41 -05:00
isUpdating = true;
2024-07-30 11:46:04 -05:00
}
else {
entityInserts[componentName] = componentsToUpdate[componentName];
2024-08-05 02:23:41 -05:00
isInserting = true;
2024-07-30 11:46:04 -05:00
}
}
2024-08-05 02:23:41 -05:00
if (isInserting) {
2024-07-30 11:46:04 -05:00
inserting.push([entityId, entityInserts]);
}
2024-08-05 02:23:41 -05:00
if (isUpdating) {
2024-07-30 11:46:04 -05:00
updating.push([entityId, entityUpdates]);
}
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
}
}
2024-07-30 11:46:04 -05:00
const promises = [];
if (inserting.length > 0) {
promises.push(this.insertMany(inserting));
}
2024-07-02 20:57:35 -05:00
if (updating.length > 0) {
2024-07-30 11:46:04 -05:00
promises.push(this.updateMany(updating));
2024-07-02 20:57:35 -05:00
}
2024-07-30 11:46:04 -05:00
if (creating.length > 0) {
promises.push(this.createManySpecific(creating));
}
await Promise.all(promises);
2024-08-05 02:23:41 -05:00
if (destroying.size > 0) {
this.destroyMany(destroying);
}
2024-07-02 20:57:35 -05:00
if (removing.length > 0) {
this.removeMany(removing);
}
2024-06-10 22:42:30 -05:00
}
applyDeferredChanges(entityId) {
const changes = this.deferredChanges[entityId];
delete this.deferredChanges[entityId];
for (const components of changes) {
this.markChange(entityId, components);
}
}
2024-08-03 15:25:30 -05:00
attach(entityIds) {
for (const entityId of entityIds) {
this.$$detached.delete(entityId);
this.applyDeferredChanges(entityId);
2024-08-03 15:25:30 -05:00
}
2024-08-07 14:27:55 -05:00
this.reindex(entityIds);
2024-08-03 15:25:30 -05:00
}
2024-06-22 12:30:25 -05:00
changed(criteria) {
const it = Object.entries(this.diff).values();
return {
[Symbol.iterator]() {
return this;
},
next: () => {
let result = it.next();
2024-06-26 07:39:28 -05:00
hasResult: while (!result.done) {
2024-06-27 07:28:46 -05:00
if (false === result.value[1]) {
result = it.next();
continue;
}
2024-06-22 12:30:25 -05:00
for (const componentName of criteria) {
if (!(componentName in result.value[1])) {
result = it.next();
2024-06-26 07:39:28 -05:00
continue hasResult;
2024-06-22 12:30:25 -05:00
}
}
break;
}
if (result.done) {
return {done: true, value: undefined};
}
return {done: false, value: this.get(result.value[0])};
},
};
}
2024-06-27 06:28:00 -05:00
async create(components = {}) {
const [entityId] = await this.createMany([components]);
2024-06-11 19:10:57 -05:00
return entityId;
2024-06-10 22:42:30 -05:00
}
2024-08-03 15:25:30 -05:00
async createDetached(components = {}) {
const [entityId] = await this.createManyDetached([components]);
return entityId;
}
2024-06-27 06:28:00 -05:00
async createMany(componentsList) {
2024-06-10 22:42:30 -05:00
const specificsList = [];
for (const components of componentsList) {
2024-08-03 15:25:30 -05:00
specificsList.push([this.$$caret, components]);
this.$$caret += 1;
}
return this.createManySpecific(specificsList);
}
async createManyDetached(componentsList) {
const specificsList = [];
for (const components of componentsList) {
specificsList.push([this.$$caret, components]);
this.$$detached.add(this.$$caret);
this.deferredChanges[this.$$caret] = [];
2024-08-03 15:25:30 -05:00
this.$$caret += 1;
2024-06-10 22:42:30 -05:00
}
return this.createManySpecific(specificsList);
}
2024-06-27 06:28:00 -05:00
async createManySpecific(specificsList) {
2024-07-02 20:57:35 -05:00
if (0 === specificsList.length) {
return;
}
2024-08-03 15:25:30 -05:00
const entityIds = new Set();
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-08-03 15:25:30 -05:00
if (!this.$$detached.has(entityId)) {
this.deferredChanges[entityId] = [];
}
2024-06-15 19:38:49 -05:00
const componentNames = [];
for (const componentName in components) {
if (this.Components[componentName]) {
componentNames.push(componentName);
2024-06-10 22:42:30 -05:00
}
}
2024-08-03 15:25:30 -05:00
entityIds.add(entityId);
2024-06-15 19:38:49 -05:00
for (const componentName of componentNames) {
if (!creating[componentName]) {
creating[componentName] = [];
2024-06-10 22:42:30 -05:00
}
2024-06-15 19:38:49 -05:00
creating[componentName].push([entityId, components[componentName]]);
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
}
2024-06-27 06:28:00 -05:00
const promises = [];
2024-06-10 22:42:30 -05:00
for (const i in creating) {
2024-06-27 06:28:00 -05:00
promises.push(this.Components[i].createMany(creating[i]));
2024-06-10 22:42:30 -05:00
}
2024-06-27 06:28:00 -05:00
await Promise.all(promises);
2024-07-02 17:46:31 -05:00
for (let i = 0; i < specificsList.length; i++) {
const [entityId, components] = specificsList[i];
this.rebuild(entityId, () => Object.keys(components));
2024-08-03 15:25:30 -05:00
if (this.$$detached.has(entityId)) {
continue;
}
this.applyDeferredChanges(entityId);
2024-07-02 17:46:31 -05:00
}
2024-08-07 14:27:55 -05:00
this.reindex(entityIds);
2024-06-11 19:10:57 -05:00
return entityIds;
2024-06-10 22:42:30 -05:00
}
2024-06-27 06:28:00 -05:00
async createSpecific(entityId, components) {
2024-08-03 15:25:30 -05:00
const [created] = await this.createManySpecific([[entityId, components]]);
return created;
2024-06-10 22:42:30 -05:00
}
2024-06-11 19:10:57 -05:00
deindex(entityIds) {
2024-08-03 15:25:30 -05:00
// Stage 4 Draft / July 6, 2024
// const attached = entityIds.difference(this.$$detached);
const attached = new Set(entityIds);
for (const detached of this.$$detached) {
attached.delete(detached);
}
if (0 === attached.size) {
return;
}
2024-06-15 19:38:49 -05:00
for (const systemName in this.Systems) {
2024-07-23 17:03:53 -05:00
const System = this.Systems[systemName];
if (!System.active) {
continue;
}
2024-08-03 15:25:30 -05:00
System.deindex(attached);
2024-06-10 22:42:30 -05:00
}
}
2024-06-27 06:28:00 -05:00
static async deserialize(ecs, view) {
2024-06-15 19:38:49 -05:00
const componentNames = Object.keys(ecs.Components);
2024-06-14 15:18:55 -05:00
const {entities, systems} = decoder.decode(view.buffer);
for (const system of systems) {
2024-06-21 06:04:22 -05:00
const System = ecs.system(system);
if (System) {
System.active = true;
}
2024-06-14 01:11:14 -05:00
}
2024-06-14 15:18:55 -05:00
const specifics = [];
let max = 1;
for (const id in entities) {
max = Math.max(max, parseInt(id));
specifics.push([
parseInt(id),
2024-06-15 19:38:49 -05:00
Object.fromEntries(
Object.entries(entities[id])
.filter(([componentName]) => componentNames.includes(componentName)),
),
2024-06-14 15:18:55 -05:00
]);
2024-06-10 22:42:30 -05:00
}
2024-06-14 15:18:55 -05:00
ecs.$$caret = max + 1;
2024-06-27 06:28:00 -05:00
await ecs.createManySpecific(specifics);
2024-06-10 22:42:30 -05:00
return ecs;
}
2024-06-11 19:10:57 -05:00
destroy(entityId) {
2024-07-26 10:31:58 -05:00
if (!this.$$destructionDependencies.has(entityId)) {
2024-07-26 11:36:32 -05:00
this.$$destructionDependencies.set(entityId, {promises: new Set()});
2024-07-26 10:31:58 -05:00
}
2024-07-26 11:36:32 -05:00
const dependencies = this.$$destructionDependencies.get(entityId);
if (!dependencies.resolvers) {
dependencies.resolvers = withResolvers();
}
return dependencies.resolvers.promise;
2024-06-10 22:42:30 -05:00
}
2024-06-11 19:10:57 -05:00
destroyMany(entityIds) {
2024-06-10 22:42:30 -05:00
const destroying = {};
2024-08-07 14:27:55 -05:00
this.deindex(entityIds);
2024-06-11 19:10:57 -05:00
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-15 19:38:49 -05:00
for (const componentName of this.$$entities[entityId].constructor.componentNames) {
if (!destroying[componentName]) {
destroying[componentName] = [];
2024-06-10 22:42:30 -05:00
}
2024-06-15 19:38:49 -05:00
destroying[componentName].push(entityId);
2024-06-10 22:42:30 -05:00
}
}
for (const i in destroying) {
2024-06-15 19:38:49 -05:00
this.Components[i].destroyMany(destroying[i]);
2024-06-10 22:42:30 -05:00
}
2024-07-23 15:05:55 -05:00
for (const entityId of entityIds) {
2024-07-25 06:44:02 -05:00
delete this.$$entities[entityId];
2024-08-07 14:27:55 -05:00
delete this.deferredChanges[entityId];
2024-07-23 15:05:55 -05:00
this.diff[entityId] = false;
}
2024-06-10 22:42:30 -05:00
}
2024-08-03 15:25:30 -05:00
detach(entityIds) {
for (const entityId of entityIds) {
this.$$detached.add(entityId);
}
2024-08-07 14:27:55 -05:00
this.deindex(entityIds);
2024-08-03 15:25:30 -05:00
}
2024-06-10 22:42:30 -05:00
get entities() {
2024-07-25 06:44:02 -05:00
const ids = [];
for (const entity of Object.values(this.$$entities)) {
ids.push(entity.id);
}
return ids;
2024-06-10 22:42:30 -05:00
}
2024-06-11 19:10:57 -05:00
get(entityId) {
return this.$$entities[entityId];
2024-06-10 22:42:30 -05:00
}
2024-06-27 06:28:00 -05:00
async insert(entityId, components) {
return this.insertMany([[entityId, components]]);
2024-06-10 22:42:30 -05:00
}
2024-06-27 06:28:00 -05:00
async insertMany(entities) {
2024-06-10 22:42:30 -05:00
const inserting = {};
const unique = new Set();
2024-06-11 19:10:57 -05:00
for (const [entityId, components] of entities) {
2024-06-10 22:42:30 -05:00
const diff = {};
2024-06-15 19:38:49 -05:00
for (const componentName in components) {
if (!inserting[componentName]) {
inserting[componentName] = [];
2024-06-10 22:42:30 -05:00
}
2024-06-15 19:38:49 -05:00
diff[componentName] = {};
inserting[componentName].push([entityId, components[componentName]]);
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
}
2024-06-27 06:28:00 -05:00
const promises = [];
2024-06-15 19:38:49 -05:00
for (const componentName in inserting) {
2024-06-27 06:28:00 -05:00
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
2024-06-10 22:42:30 -05:00
}
2024-06-27 06:28:00 -05:00
await Promise.all(promises);
for (const [entityId, components] of entities) {
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
}
2024-08-07 14:27:55 -05:00
this.reindex(unique);
2024-06-10 22:42:30 -05:00
}
2024-06-11 19:10:57 -05:00
markChange(entityId, components) {
2024-07-02 17:46:31 -05:00
if (this.deferredChanges[entityId]) {
this.deferredChanges[entityId].push(components);
2024-07-02 17:51:58 -05:00
return;
2024-07-02 17:46:31 -05:00
}
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-08-04 20:54:56 -05:00
this.diff[entityId] = components;
2024-06-10 22:42:30 -05:00
}
// Otherwise, merge.
else {
2024-06-15 19:38:49 -05:00
for (const componentName in components) {
this.diff[entityId][componentName] = false === components[componentName]
2024-06-10 22:42:30 -05:00
? false
2024-06-15 19:38:49 -05:00
: this.Components[componentName].mergeDiff(
this.diff[entityId][componentName] || {},
components[componentName],
2024-06-10 22:42:30 -05:00
);
}
}
}
2024-09-05 07:15:55 -05:00
predict(entity, elapsed) {
for (const systemName in this.Systems) {
const System = this.Systems[systemName];
if (!System.predict) {
continue;
}
System.predict(entity, elapsed);
}
}
2024-07-21 01:37:40 -05:00
async readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
2024-07-22 00:13:03 -05:00
const {promise, resolve, reject} = withResolvers();
2024-07-21 01:37:40 -05:00
cache.set(key, promise);
this.readAsset(uri)
.then((chars) => {
resolve(
chars.byteLength > 0
? JSON.parse(textDecoder.decode(chars))
: {},
);
})
.catch(reject);
}
return cache.get(key);
}
async readScript(uriOrCode, context = {}) {
if (!uriOrCode) {
return undefined;
}
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = textDecoder.decode(buffer);
}
}
if (!code) {
return undefined;
}
return Script.fromCode(code, context);
}
2024-06-15 19:38:49 -05:00
rebuild(entityId, componentNames) {
2024-07-13 00:34:07 -05:00
let Class;
if (componentNames) {
let existing = [];
if (this.$$entities[entityId]) {
existing.push(...this.$$entities[entityId].constructor.componentNames);
}
Class = this.$$entityFactory.makeClass(componentNames(existing), this.Components);
}
else {
Class = this.$$entities[entityId].constructor;
2024-06-10 22:42:30 -05:00
}
2024-06-27 02:57:28 -05:00
if (this.$$entities[entityId] && Class === this.$$entities[entityId].constructor) {
// Eventually - memoizable.
}
2024-07-13 00:34:07 -05:00
return 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-08-03 15:25:30 -05:00
// Stage 4 Draft / July 6, 2024
// const attached = entityIds.difference(this.$$detached);
const attached = new Set(entityIds);
for (const detached of this.$$detached) {
attached.delete(detached);
}
if (0 === attached.size) {
return;
}
2024-06-15 19:38:49 -05:00
for (const systemName in this.Systems) {
2024-07-23 17:03:53 -05:00
const System = this.Systems[systemName];
if (!System.active) {
continue;
}
2024-08-03 15:25:30 -05:00
System.reindex(attached);
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 = {};
2024-06-15 19:38:49 -05:00
for (const componentName of components) {
diff[componentName] = false;
if (!removing[componentName]) {
removing[componentName] = [];
2024-06-10 22:42:30 -05:00
}
2024-06-15 19:38:49 -05:00
removing[componentName].push(entityId);
2024-06-10 22:42:30 -05:00
}
2024-06-11 19:10:57 -05:00
this.markChange(entityId, diff);
2024-06-10 22:42:30 -05:00
}
2024-06-15 19:38:49 -05:00
for (const componentName in removing) {
this.Components[componentName].destroyMany(removing[componentName]);
2024-06-10 22:42:30 -05:00
}
for (const [entityId, components] of entities) {
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
}
2024-08-07 14:27:55 -05:00
this.reindex(unique);
2024-06-10 22:42:30 -05:00
}
static serialize(ecs, view) {
2024-06-14 15:18:55 -05:00
const buffer = encoder.encode(ecs.toJSON());
2024-06-10 22:42:30 -05:00
if (!view) {
2024-06-14 15:18:55 -05:00
view = new DataView(new ArrayBuffer(buffer.length));
2024-06-14 01:11:14 -05:00
}
2024-06-14 15:18:55 -05:00
(new Uint8Array(view.buffer)).set(buffer);
2024-06-10 22:42:30 -05:00
return view;
}
setClean() {
this.diff = {};
}
2024-06-15 19:38:49 -05:00
system(systemName) {
return this.Systems[systemName];
2024-06-10 22:42:30 -05:00
}
tick(elapsed) {
// tick systems
for (const systemName in this.Systems) {
const System = this.Systems[systemName];
if (!System.active) {
continue;
}
if (!System.frequency) {
System.tick(elapsed);
continue;
}
System.elapsed += elapsed;
while (System.elapsed >= System.frequency) {
System.tick(System.frequency);
System.elapsed -= System.frequency;
}
}
2024-08-04 20:54:56 -05:00
// destroy entities
const destroying = new Set();
for (const [entityId, {promises}] of this.$$destructionDependencies) {
if (0 === promises.size) {
destroying.add(entityId);
}
}
if (destroying.size > 0) {
this.destroyMany(destroying);
for (const entityId of destroying) {
this.$$destructionDependencies.get(entityId).resolvers.resolve();
this.$$destructionDependencies.delete(entityId);
}
}
2024-06-10 22:42:30 -05:00
}
2024-06-14 15:18:55 -05:00
toJSON() {
const entities = {};
for (const id in this.$$entities) {
2024-06-21 18:15:15 -05:00
if (this.$$entities[id]) {
entities[id] = this.$$entities[id].toJSON();
}
2024-06-14 15:18:55 -05:00
}
const systems = [];
2024-06-15 19:38:49 -05:00
for (const systemName in this.Systems) {
if (this.Systems[systemName].active) {
systems.push(systemName);
2024-06-14 15:18:55 -05:00
}
}
return {
entities,
systems,
};
}
2024-07-30 11:46:04 -05:00
async updateMany(entities) {
const updating = {};
const unique = new Set();
for (const [entityId, components] of entities) {
for (const componentName in components) {
if (!updating[componentName]) {
updating[componentName] = [];
}
updating[componentName].push([entityId, components[componentName]]);
}
unique.add(entityId);
}
const promises = [];
for (const componentName in updating) {
promises.push(this.Components[componentName].updateMany(updating[componentName]));
}
await Promise.all(promises);
}
2024-06-10 22:42:30 -05:00
}