Feature/Refactor to use ecs architecture (#14)
Reviewed-on: #14 Co-authored-by: Juan Sebastian Montoya <juansmm@outlook.com> Co-committed-by: Juan Sebastian Montoya <juansmm@outlook.com>
This commit is contained in:
parent
e0436e7769
commit
cec1fccc22
23 changed files with 1709 additions and 650 deletions
53
src/ecs/System.js
Normal file
53
src/ecs/System.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Base class for all systems.
|
||||
* Systems contain logic that operates on entities with specific components.
|
||||
*
|
||||
* @typedef {import('./World.js').EntityId} EntityId
|
||||
* @typedef {import('./World.js').ComponentClass} ComponentClass
|
||||
*/
|
||||
export class System {
|
||||
constructor() {
|
||||
/** @type {import('./World.js').World|null} */
|
||||
this.world = null;
|
||||
|
||||
/** @type {boolean} */
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when the system is added to the world
|
||||
*/
|
||||
init() {}
|
||||
|
||||
/**
|
||||
* Called every frame
|
||||
* @param {number} deltaTime - Time since last frame in seconds
|
||||
*/
|
||||
update(_deltaTime) {}
|
||||
|
||||
/**
|
||||
* Called when the system is removed or world is cleaned up
|
||||
*/
|
||||
cleanup() {}
|
||||
|
||||
/**
|
||||
* Helper to get entities with specific components
|
||||
* @param {...ComponentClass} componentClasses - The component classes to filter by
|
||||
* @returns {EntityId[]} Array of entity IDs
|
||||
*/
|
||||
getEntities(...componentClasses) {
|
||||
return this.world.getEntitiesWithComponents(...componentClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get a component from an entity
|
||||
* @template T
|
||||
* @param {EntityId} entityId - The entity ID
|
||||
* @param {new (...args: any[]) => T} componentClass - The component class
|
||||
* @returns {T|undefined} The component instance or undefined
|
||||
*/
|
||||
getComponent(entityId, componentClass) {
|
||||
return this.world.getComponent(entityId, componentClass);
|
||||
}
|
||||
}
|
||||
|
||||
170
src/ecs/World.js
Normal file
170
src/ecs/World.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* World manages all entities, components, and systems in the ECS architecture.
|
||||
*
|
||||
* @typedef {number} EntityId - Unique identifier for an entity
|
||||
* @typedef {Function} ComponentClass - A component class constructor
|
||||
* @typedef {Object} Component - A component instance (data only)
|
||||
*/
|
||||
export class World {
|
||||
constructor() {
|
||||
/** @type {Map<EntityId, Set<ComponentClass>>} */
|
||||
this.entities = new Map(); // entityId -> Set of component classes
|
||||
|
||||
/** @type {Map<ComponentClass, Map<EntityId, Component>>} */
|
||||
this.components = new Map(); // componentClass -> Map(entityId -> component)
|
||||
|
||||
/** @type {import('./System.js').System[]} */
|
||||
this.systems = [];
|
||||
|
||||
/** @type {EntityId} */
|
||||
this.nextEntityId = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity and return its ID
|
||||
* @returns {EntityId} The newly created entity ID
|
||||
*/
|
||||
createEntity() {
|
||||
const id = this.nextEntityId++;
|
||||
this.entities.set(id, new Set());
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entity and all its components
|
||||
* @param {EntityId} entityId - The entity to remove
|
||||
*/
|
||||
removeEntity(entityId) {
|
||||
if (!this.entities.has(entityId)) return;
|
||||
|
||||
// Remove all components for this entity
|
||||
const componentClasses = this.entities.get(entityId);
|
||||
for (const componentClass of componentClasses) {
|
||||
const componentMap = this.components.get(componentClass);
|
||||
if (componentMap) {
|
||||
componentMap.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
this.entities.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a component instance to an entity. Component class is inferred from the instance.
|
||||
* @param {EntityId} entityId - The entity to add the component to
|
||||
* @param {Component} component - The component instance (must have a constructor)
|
||||
* @throws {Error} If entity doesn't exist
|
||||
*/
|
||||
addComponent(entityId, component) {
|
||||
if (!this.entities.has(entityId)) {
|
||||
throw new Error(`Entity ${entityId} does not exist`);
|
||||
}
|
||||
|
||||
const componentClass = component.constructor;
|
||||
|
||||
// Track that this entity has this component class
|
||||
this.entities.get(entityId).add(componentClass);
|
||||
|
||||
// Store the component instance
|
||||
if (!this.components.has(componentClass)) {
|
||||
this.components.set(componentClass, new Map());
|
||||
}
|
||||
this.components.get(componentClass).set(entityId, component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a component from an entity
|
||||
* @template T
|
||||
* @param {EntityId} entityId - The entity ID
|
||||
* @param {new (...args: any[]) => T} componentClass - The component class
|
||||
* @returns {T|undefined} The component instance or undefined
|
||||
*/
|
||||
getComponent(entityId, componentClass) {
|
||||
const componentMap = this.components.get(componentClass);
|
||||
return componentMap ? componentMap.get(entityId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has a component
|
||||
* @param {EntityId} entityId - The entity ID
|
||||
* @param {ComponentClass} componentClass - The component class
|
||||
* @returns {boolean} True if the entity has the component
|
||||
*/
|
||||
hasComponent(entityId, componentClass) {
|
||||
return this.entities.get(entityId)?.has(componentClass) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a component from an entity
|
||||
* @param {EntityId} entityId - The entity ID
|
||||
* @param {ComponentClass} componentClass - The component class to remove
|
||||
*/
|
||||
removeComponent(entityId, componentClass) {
|
||||
const entityComponents = this.entities.get(entityId);
|
||||
if (entityComponents) {
|
||||
entityComponents.delete(componentClass);
|
||||
}
|
||||
|
||||
const componentMap = this.components.get(componentClass);
|
||||
if (componentMap) {
|
||||
componentMap.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities that have ALL of the specified component classes
|
||||
* @param {...ComponentClass} componentClasses - The component classes to filter by
|
||||
* @returns {EntityId[]} Array of entity IDs that have all specified components
|
||||
*/
|
||||
getEntitiesWithComponents(...componentClasses) {
|
||||
const result = [];
|
||||
|
||||
for (const [entityId, entityComponentClasses] of this.entities.entries()) {
|
||||
const hasAll = componentClasses.every(cls => entityComponentClasses.has(cls));
|
||||
if (hasAll) {
|
||||
result.push(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system to the world
|
||||
* @param {import('./System.js').System} system - The system to add
|
||||
*/
|
||||
addSystem(system) {
|
||||
system.world = this;
|
||||
this.systems.push(system);
|
||||
if (system.init) {
|
||||
system.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all systems with delta time
|
||||
* @param {number} deltaTime - Time elapsed since last frame (in seconds)
|
||||
*/
|
||||
update(deltaTime) {
|
||||
for (const system of this.systems) {
|
||||
if (system.enabled !== false) {
|
||||
system.update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all systems and clear all entities/components
|
||||
*/
|
||||
cleanup() {
|
||||
for (const system of this.systems) {
|
||||
if (system.cleanup) {
|
||||
system.cleanup();
|
||||
}
|
||||
}
|
||||
this.entities.clear();
|
||||
this.components.clear();
|
||||
this.systems = [];
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue