@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
257 lines (224 loc) • 9.24 kB
JavaScript
import { array_copy_unique } from "../../core/collection/array/array_copy_unique.js";
import { array_push_if_unique } from "../../core/collection/array/array_push_if_unique.js";
import { noop } from "../../core/function/noop.js";
import ObservedValue from "../../core/model/ObservedValue.js";
import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";
/**
* Base class to extend ECS systems from.
* A System defines some behavior over a tuple of components.
* Override base methods as needed to achieve desired behavior. The system already offers {@link update} and {@link fixedUpdate} methods for time-based simulation.
* If you have event-driven requirements - make use of {@link link} and {@link unlink} methods, as well as messaging facilities of {@link EntityComponentDataset}.
*
* @template C
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*
* @example
* class FallDamageSystem{
* dependencies = [Health, Physics]
*
* listeners = []; // internal storage
*
* link(health: Health, physics: Physics, entity: number){
* const listener = () => health.value -= 10; // receive damage on fall
* physics.onContact.add(listener);
* this.listeners[entity] = listener; // remember for later
* }
*
* unlink(health: Health, physics: Physics, entity: number){
* const listener = this.listeners[entity];
* physics.onContact.remove(listener);
* delete this.listeners[entity]; // cleanup
* }
* }
*/
export class System {
/**
* Reference to the attached {@link EntityManager}, usually set during {@link startup}
* Useful for gaining access to other Systems, via {@link EntityManager.getSystem} as well as the associated dataset via {@link EntityManager.dataset}
* @protected
* @type {EntityManager}
*/
entityManager = null;
/**
* Managed internally by {@link EntityManager}, do not modify manually
* @readonly
* @type {ObservedValue.<SystemState>}
*/
state = new ObservedValue(SystemState.INITIAL);
/**
* Component classes which have to be present before the system links an entity.
* NOTE: do not modify this while the system is running.
* @see link
* @see unlink
* @readonly
* @type {Array}
*/
dependencies = [];
/**
* Component types that are used internally by the system and how they are used
* Main benefit of doing so is twofold:
* - Helps the engine figure out the best execution order for system to make sure that updates propagate as quickly as possible
* - Declaring this helps EntityManager to ensure that all relevant component types are properly registered for the system
*
* NOTE: specifying this is optional. The engine will still work.
* NOTE: do not modify this while the system is running
* @readonly
* @type {ResourceAccessSpecification[]}
*/
components_used = [];
/**
* @returns {Array} Component classes
*/
get referenced_components() {
const result = [];
array_copy_unique(this.dependencies, 0, result, result.length, this.dependencies.length);
const used = this.components_used;
const use_count = used.length;
for (let i = 0; i < use_count; i++) {
const ref = used[i];
array_push_if_unique(result, ref.resource);
}
return result;
}
/**
* Does not check if the component is present, will return 0 if not present.
* @template T
* @param {T} Klass
* @return {number} bitmask of {@link ResourceAccessKind}
*/
getAccessForComponent(Klass) {
let result = 0;
const used = this.components_used;
const use_count = used.length;
for (let i = 0; i < use_count; i++) {
const ref = used[i];
if (ref.resource === Klass) {
result |= ref.access;
break;
}
}
const dependencies = this.dependencies;
const dependency_count = dependencies.length;
for (let i = 0; i < dependency_count; i++) {
const dependency = dependencies[i];
if (dependency === Klass) {
// at the very least it's going to read
result |= ResourceAccessKind.Read;
break;
}
}
return result;
}
/**
* Invoked before {@link System} is used for the first time.
* Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
* @param {EntityManager} entityManager
* @returns {Promise<void>}
* @see shutdown
*/
async startup(entityManager) {
// override as necessary
}
/**
* Invoked when {@link System} is no longer needed.
* Good place to clean up any held resourced and terminate owned processes.
* Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
* @param {EntityManager} entityManager
* @returns {Promise<void>}
* @see startup
*/
async shutdown(entityManager) {
// override as necessary
}
/**
* Called automatically when an entity gets a complete component tuple {@link this.dependencies}.
* Inputs are the same as dependencies, only instances instead of classes, plus there is an extra argument at the end, which is the entity ID.
* see {@link EntityObserver} for more implementation detail
*/
link(component, entity) {
// override as necessary
}
/**
* Called automatically when an entity breaks component tuple {@link this.dependencies}.
* Inputs are the same as dependencies, only instances instead of classes, plus there is an extra argument at the end, which is the entity ID.
* see {@link EntityObserver} for more implementation detail
*/
unlink(component, entity) {
// override as necessary
}
/**
* Invoked when a dataset is attached to the system, this happens before any entities are linked.
* Happens as a result of {@link EntityManager.attachDataset}.
* It is generally advised *NOT* to modify the dataset here.
* Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
*
* This method is entirely optional, it provides advanced functionality for situations where datasets are being switched on the fly.
*
* see {@link handleDatasetDetached} for the reverse.
* @param {EntityComponentDataset} dataset
*/
handleDatasetAttached(dataset) {
// override as necessary
}
/**
* Invoked when dataset is about to be detached from the system, this happens after all entities have been unlinked.
* Happens as a result of {@link EntityManager.detachDataset}.
* It is generally advised *NOT* to modify the dataset here.
* Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
*
* This method is entirely optional, it provides advanced functionality for situations where datasets are being switched on the fly.
*
* see {@link handleDatasetAttached} for the reverse.
* @param {EntityComponentDataset} dataset
*/
handleDatasetDetached(dataset) {
// override as necessary
}
}
/**
* @readonly
* @type {boolean}
*/
System.prototype.isSystem = true;
/**
* Fixed update function, every step happens with the same exact time increment
* useful for systems that must have a fixed time step to be predictable and stable, such as physics
* @param {number} timeDelta in seconds, will always be the same value, controlled via {@link EntityManager.fixedUpdateStepSize}
*/
System.prototype.fixedUpdate = noop; // by assigning NO-OP we enable a simple check, whether running the update would be useful
/**
* This update is generally synchronized with the render loop.
* Note that this time step can vary depending on system conditions and hardware we are running on.
* It is generally safe to assume that this update will happen once per frame, but it is not guaranteed.
* Also, note that when the application window/tab is suspended, the next update step can have a very large value.
* @param {number} timeDelta in seconds
*/
System.prototype.update = noop; // by assigning NO-OP we enable a simple check, whether running the update would be useful
/**
* @readonly
* @enum {number}
*/
export const SystemState = {
INITIAL: 0,
/**
* System is currently in the process of starting up.
* @see System.startup
*/
STARTING: 1,
/**
* System is in the main running state, after startup has successfully finished.
*/
RUNNING: 2,
/**
* System is currently in the process of shutting down.
* @see System.shutdown
*/
STOPPING: 3,
/**
* System has finished shutting down.
* @see System.shutdown
*/
STOPPED: 4
}