UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

257 lines (224 loc) • 9.24 kB
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 }