Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
6.9 kB · 234 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235/** * Entity Component System module */
import type { ColliderDesc, World } from "@thorium-sim/rapier3d-node";import RAPIER from "@thorium-sim/rapier3d-node";import type { ServerDataModel } from "@thorium/.server/classes/ServerDataModel";import type { DataContext } from "@thorium/.server/DataContext";import type { AllSends, SendInputs } from "@thorium/.server/init/router";import type { TimelineBlock } from "@thorium/components/timelineBuilder/TimelineBlockTypes";import type { ComponentIds } from "@thorium/ecs-components";import type { BlockMetadata } from "@thorium/utils/.server/executeBlocks";import { type RNG, createRNG } from "@thorium/utils/rng";
import type Entity from "./entity";import performance from "./performance";import type System from "./system";
class ECS { /** * Store all entities of the ECS. */ entities = new Map<number, Entity>(); /** * Store entities which need to be tested at beginning of next tick. */ entitiesSystemsDirty = new Set<Entity>(); /** * Store all systems of the ECS. */ systems = new Set<System>(); /** * Count how many updates have been done. */ updateCounter = 0; lastUpdate = this.now(); rng: RNG; maxEntityId = 1; componentCache = new Map<ComponentIds, Set<Entity>>(); colliderCache = new Map<string, ColliderDesc>(); shipSystemCache = new Map<number, Map<string, Entity | Entity[]>>(); changeBatch = new Set<`${number}-${ComponentIds}`>(); removeBatch = new Map<number, Pick<Entity, "id" | "components">>(); // The key is the sector number based on the location of this physics world worlds = new Map<string, World>();
constructor( public server: ServerDataModel, seed: string | number = "thorium", skip?: number, ) { this.rng = createRNG(seed, skip); } /** * Retrieve an entity by id */ getEntityById(id: number) { return this.entities.get(id) || null; } /** * Add an entity to the ecs. */ addEntity(entity: Entity) { this.entities.set(entity.id, entity); entity.addToECS(this); this.maxEntityId = Math.max(this.maxEntityId, entity.id); Object.keys(entity.components).forEach((componentName) => { this.batchChange(entity.id, componentName as ComponentIds); }); } /** * Remove an entity from the ecs by reference. */ removeEntity(entity: Entity) { const hasEntity = this.entities.has(entity.id); // if the entity is not found, do nothing if (hasEntity) { entity.dispose(); this.removeEntityIfDirty(entity); this.entities.delete(entity.id); }
this.removeBatch.set(entity.id, entity.toJSON()); return entity; } /** * Remove an entity from the ecs by entity id. */ removeEntityById(entityId: number) { const entity = this.entities.get(entityId); if (entity) { this.removeEntity(entity); return entity; } return null; } /** * Remove an entity from dirty entities by reference. */ removeEntityIfDirty(entity: Entity) { this.entitiesSystemsDirty.delete(entity); } /** * Add a system to the ecs. */ addSystem(system: System) { system.ecs = this; this.systems.add(system); system.attach();
// iterate over all entities to eventually add system for (const [, entity] of this.entities) { if (system.test(entity)) { system.addEntity(entity); } } } /** * Remove a system from the ecs. */ removeSystem(system: System) { this.systems.delete(system); system.dispose(); } /** * "Clean" entities flagged as dirty by removing unnecessary systems and * adding missing systems. */ cleanDirtyEntities() { for (const entity of this.entitiesSystemsDirty) { for (const system of this.systems) { // for each dirty entity for each system const entityHasSystem = entity.systems.has(system); const entityTest = system.test(entity);
if (!entityHasSystem && entityTest) { // if the entity is not added to the system yet and should be, add it system.addEntity(entity); } else if (entityHasSystem && !entityTest) { // if the entity is added to the system but should not be, remove it system.removeEntity(entity); } // else we do nothing the current state is OK }
entity.systemsDirty = false; }
this.entitiesSystemsDirty.clear(); } now() { if (typeof Bun === "undefined") { return performance.now(); } // Convert nanoseconds to milliseconds return Bun.nanoseconds() / 1_000_000; } /** * Update the ecs. * * @method update */ update(testElapsed?: number) { const now = this.now(); const elapsed = testElapsed ?? now - this.lastUpdate; if (this.entitiesSystemsDirty.size > 0) { this.cleanDirtyEntities(); } for (const system of this.systems) { if (this.entitiesSystemsDirty.size > 0) { // if the last system flagged some entities as dirty check that case this.cleanDirtyEntities(); } system.updateAll(elapsed, this.updateCounter); } this.updateCounter += 1; this.lastUpdate = now; } batchChange(entityId: number, component: ComponentIds) { this.changeBatch.add(`${entityId}-${component}`); } dispose() { for (const sys of this.systems) { this.removeSystem(sys); } for (const [, entity] of this.entities) { this.removeEntity(entity); } this.colliderCache.clear(); this.componentCache.clear(); this.shipSystemCache.clear(); this.changeBatch.clear(); this.removeBatch.clear(); } getWorld(key: string) { if (!this.worlds.has(key)) { this.worlds.set(key, new RAPIER.World({ x: 0, y: 0, z: 0 })); } return this.worlds.get(key)!; } async executeBlocks( // oxlint-disable-next-line no-unused-vars blocks: TimelineBlock[], // oxlint-disable-next-line no-unused-vars blocksMetadata: BlockMetadata = {}, ): Promise<{ waiting: boolean }> { // We have to keep this blank and register it when we init ECS // to prevent weird dependency loops when executing blocks from // ECS systems throw new Error("executeBlocks has not been properly registered. Have you run initEcs yet?"); } async triggerAction<A extends AllSends>( // oxlint-disable-next-line no-unused-vars path: A, // oxlint-disable-next-line no-unused-vars input: SendInputs<A>, // oxlint-disable-next-line no-unused-vars ctx?: DataContext | Record<string, any>, ): Promise<unknown> { // We have to keep this blank and register it when we init ECS // to prevent weird dependency loops when executing blocks from // ECS systems throw new Error("triggerAction has not been properly registered. Have you run initEcs yet?"); } // oxlint-disable-next-line no-unused-vars async processTriggers(event?: { event: string; values: any }): Promise<void> { // We have to keep this blank and register it when we init ECS // to prevent weird dependency loops when executing blocks from // ECS systems throw new Error("processTriggers has not been properly registered. Have you run initEcs yet?"); }}
export default ECS;