ecsy

repository·dev·Indexed 22 days ago

https://github.com/ecsyjs/ecsy

A lightweight, high-performance Entity Component System (ECS) framework for JavaScript. Designed to be framework-agnostic, ecsy focuses on a simple API and minimized garbage collection to support high-performance applications like games. It utilizes a World instance to manage entities, components, and systems, employing schemas and component pooling to ensure data integrity and performance.

Tokens
15.8K
Snippets
53
Records
68
Agent score
76%

What's inside ecsy

  1. Core ECS Concepts in ECSY

    dev

    ECSY follows the Entity Component System (ECS) pattern, which favors composition over class hierarchies. Understanding these five core abstractions is essential:

    • World: The central container that holds all entities, components, systems, and queries. Most applications use a single world.
    • Entities: Objects identified by a unique ID. They act as containers for components.
    • Components: Pure data structures that represent facets of an entity (e.g., Position, Acceleration). They do not contain logic.
    • Systems: The logic layer. Systems process entities and modify their components. They are executed every frame.
    • Queries: Filters used by systems to identify specific sets of entities based on the components they possess.
  2. Manage Entities in ECSY

    dev

    An entity is an object with a unique ID used to group components together. Entities must be created within a World context.

    Adding Components

    You can add components to an entity using addComponent(ComponentClass, [initialValues]). You can either use the component's default constructor values or provide an object to override them.

    Removing Components

    Use removeComponent(ComponentClass) to remove a component.

    Note on Deferred Removal: By default, component removal is deferred until the end of the frame. This allows other systems to still access the component data during the current frame. If you need to remove a component immediately, pass true as the second argument: entity.removeComponent(ComponentClass, true). However, this is not recommended as it may cause side effects in other systems.

    Accessing Removed Components

    If a system is listening for removed entities (via a reactive query), you can access the data of the component that was just removed using getRemovedComponent(ComponentClass).

    // Create an entity
    let entity = world.createEntity();
    
    // Add component with defaults
    entity.addComponent(ComponentA);
    
    // Add component with custom values
    entity.addComponent(ComponentA, {number: 20, string: "Hi"});
    
    // Remove component
    entity.removeComponent(ComponentA);
    
    // Immediate removal (not recommended)
    entity.removeComponent(ComponentA, true);
  3. Manage Entity and Component removal lifecycle

    dev

    ECSY uses deferred removal by default. When you call entity.removeComponent(Component) or entity.remove(), the entity/component is not immediately destroyed. Instead, it is marked for removal and becomes available in removed reactive queries. The actual deallocation happens at the end of the frame.

    Immediate Removal

    You can bypass deferred removal by passing true as the second argument. Warning: This is not recommended as it can cause side effects and prevents other systems in the same frame from accessing the removed data.

    Accessing Removed Components

    When reacting to a removed event, you can access the data of the component that was just removed using entity.getRemovedComponent(Component).

    // Deferred removal (Recommended)
    entity.removeComponent(Player);
    entity.remove();
    
    // Immediate removal (Use with caution)
    entity.removeComponent(Player, true);
    entity.remove(true);
    
    // Accessing data from a removed component in a system
    class SystemWolfReactions extends System {
      execute(delta, elapsedTime) {
        this.queries.sleepingWolves.removed.forEach(wolf => {
          if (wolf.hasRemovedComponent(Sleeping)) {
            let sleeping = wolf.getRemovedComponent(Sleeping);
            // Use 'sleeping' data here
          }
        });
      }
    }
  4. Manage resources with System State Components

    dev

    System State Components (SSC) are used by systems to hold internal resources (like GPU meshes or network handles) for an entity. Unlike standard components, SSCs are not automatically removed when an entity is deleted; they must be explicitly removed by the system.

    This allows a system to detect when an entity has been removed from its query (by checking for the presence of the SSC while the primary component is missing) and perform necessary cleanup (e.g., disposing of a mesh).

    import { Component, SystemStateComponent, System, Not } from 'ecsy';
    
    // The primary component
    class Geometry extends Component {
      schema = { primitive: { type: Types.String, default: "box" } };
    }
    
    // The System State Component used for resource tracking
    class StateComponentGeometry extends SystemStateComponent {
      schema = { meshReference: { type: Types.Ref } };
    }
    
    class GeometrySystem extends System {
      init() {
        return {
          queries: {
            added: { components: [Geometry, Not(StateComponentGeometry)] },
            remove: { components: [Not(Geometry), StateComponentGeometry] },
            normal: { components: [Geometry, StateComponentGeometry] },
          }
        };
      }
    
      execute() {
        // Handle new entities: create resources and attach SSC
        added.forEach(entity => {
          const mesh = new Mesh(entity.getComponent(Geometry).primitive);
          entity.addComponent(StateComponentGeometry, { meshReference: mesh });
        });
    
        // Handle removed entities: detect missing primary component via SSC and cleanup
        remove.forEach(entity => {
          const state = entity.getComponent(StateComponentGeometry);
          state.meshReference.dispose(); // Cleanup resource
          entity.removeComponent(StateComponentGeometry);
        });
      }
    }
  5. Define Queries in Systems

    dev

    Systems use static queries to define which entities they should operate on. A query specifies a list of required components.

    SystemName.queries = {
      boxes: { components: [ Box ] },
      spheres: { components: [ Sphere ] }
    };

    Inside the execute method, you can access the results of these queries via this.queries.<queryName>.results.

    Iterating and Mutating Results

    If you plan to mutate the results of a query while iterating (e.g., removing an entity or a component that changes the entity's membership in the query), you must traverse the results in reverse order to avoid skipping elements due to array mutation.

    // Correct way to mutate during iteration
    let results = this.queries.queryA.results;
    for (var i = results.length - 1; i >= 0; i--) {
      let entity = results[i];
      if (someCondition) {
        entity.remove(); // Safe because we are moving backwards
      }
    }
    class SystemName extends System {
      execute(delta, time) {
        this.queries.boxes.results.forEach(entity => {
          let box = entity.getComponent(Box);
          // Process box
        });
      }
    }
    
    SystemName.queries = {
      boxes: { components: [ Box ] }
    };
  6. Implement Single-value components

    dev

    When a component only contains one attribute, it is a best practice to name that attribute value rather than repeating the component's name. This avoids redundant access patterns like entity.getComponent(Acceleration).acceleration.

    class Acceleration extends Component {}
    
    Acceleration.schema = {
      value: { type: Types.Number, default: 0.1 }
    };
    
    // Accessing the value is cleaner:
    let acceleration = entity.getComponent(Acceleration).value;
  7. Optimize performance with Component Pooling

    dev

    To minimize garbage collection in performance-sensitive applications, ECSY uses component pooling. When entity.addComponent(ComponentA) is called, the engine attempts to reuse an existing instance from a pool. When entity.removeComponent(ComponentA) is called, the instance is returned to the pool.

    Customizing Pooling

    Overriding Component Methods

    You can manually implement constructor, copy, and reset to handle complex data structures or optimize performance. The reset method is critical as it is called frequently when components are returned to the pool; avoid memory allocation inside reset and reuse existing data structures where possible.

    Disabling Pooling

    If a component cannot be safely copied or reset, disable pooling by passing false as the second argument to world.registerComponent().

    Custom ObjectPools

    You can provide a custom ObjectPool instance to registerComponent to control how instances are acquired, released, or expanded.

    import { Component, ObjectPool } from 'ecsy';
    
    // 1. Overriding methods for custom logic/performance
    class ColorArray extends Component {
      constructor(props) {
        super(false); // Disable schema-based defaults
        this.value = [];
      }
    
      copy(src) {
        this.value.length = src.value.length;
        for (let i = 0; i < src.value.length; i++) {
          const srcColor = src.value[i];
          const destColor = this.value[i];
          destColor.r = srcColor.r;
          destColor.g = srcColor.g;
          destColor.b = srcColor.b;
        }
        return this;
      }
    
      reset() {
        this.value.forEach(color => {
          color.r = 0;
          color.g = 0;
          color.b = 0;
        });
      }
    }
    
    // 2. Disabling pooling for specific components
    world.registerComponent(AudioListener, false);
    
    // 3. Using a custom ObjectPool with a specific initial size
    world.registerComponent(MyComponent, new ObjectPool(MyComponent, 1000));
  8. Define Queries in Systems

    dev

    A query is a collection of entities that match specific component conditions. Defining queries within a System is the recommended approach, as it allows the engine to optimize execution and reuse queries via the QueryManager.

    A query requires a components attribute, which is an array of components an entity must possess to be included.

    var query = {
      positions: {
        components: [ Position, Velocity ]
      }
    };
  9. Understand the ECSY architecture and workflow

    dev

    ECSY follows the Entity Component System (ECS) pattern. To build an application, you follow this workflow:

    1. Define Components: Create data structures that represent facets of your objects (e.g., geometry, physics).
    2. Create Entities: Create objects with unique IDs and attach components to them.
    3. Create Systems: Write logic that processes entities by reading and transforming their component data.
    4. Execute Systems: Run your systems every frame to drive the application logic.

    Core Concepts

    • Entities: Objects with a unique ID that act as containers for components.
    • Components: Pure data containers representing facets of an entity.
    • Systems: The logic layer that processes entities and modifies components.
    • Queries: Filters used by systems to select specific entities based on the components they possess.
    • World: The central container that manages entities, components, systems, and queries.
    /* Typical ECSY Workflow Example */
    
    // 1. Define components
    // 2. Create entities and attach components
    // 3. Create systems to use components
    // 4. Execute systems each frame
  10. How ECSY works: Core Concepts

    dev

    ECSY is an Entity Component System (ECS) framework where all logic and data are scoped within a World instance.

    • World: The central container that manages entities, components, and systems. You register components and systems to a world and then call world.execute(delta, time) to run the simulation.
    • Entities: Objects managed by the world. They act as containers for components.
    • Components: Data containers. You define them by extending the Component class and providing a schema using Types (e.g., Types.Number, Types.String).
    • TagComponent: A special type of component used for marking entities without storing additional data.
    • Systems: Logic containers. You extend the System class and implement an execute(delta, time) method. Systems use Queries to find entities that possess specific sets of components.
    • Queries: Defined on the System class, queries allow a system to filter entities based on the components they have (e.g., components: [Velocity, Position]).
    // Example of the core pattern
    class Position extends Component {
      schema = { x: { type: Types.Number }, y: { type: Types.Number } };
    }
    
    class MovableSystem extends System {
      execute(delta, time) {
        this.queries.moving.results.forEach(entity => {
          const pos = entity.getMutableComponent(Position);
          // logic here...
        });
      }
    }
    
    MovableSystem.queries = {
      moving: { components: [Position, Velocity] }
    };
    
    const world = new World()
      .registerComponent(Position)
      .registerSystem(MovableSystem);
  11. Use Reactive Queries to Listen for Changes

    dev

    A reactive system is a system that listens for entities being added to, removed from, or modified within its queries.

    To make a query reactive, add removed: true to the query definition. This allows the system to react to the event of a component being removed from an entity.

    When an entity is removed from a query (e.g., via entity.removeComponent(Component)), the system can access the component's data using entity.getRemovedComponent(Component) during the frame in which the removal was processed.

    class SystemFoo extends System {
      execute() {
        // Access entities that just had a component removed
        this.queries.boxes.removed.forEach(entity => {
          let component = entity.getRemovedComponent(Box);
          console.log('Component removed:', component);
        });
    
        // Access entities that still match the query
        this.queries.boxes.results.forEach(entity => {
          console.log('Iterating on entity: ', entity.id);
        });
      }
    }
    
    SystemFoo.queries = {
      boxes: {
        components: [ Box ],
        removed: true // Enables listening for removals
      }
    }
  12. Use ECSY Boilerplates for Project Setup

    dev

    To jumpstart your development, use these community-maintained boilerplates for different environments and languages:

    • Build Tools & Languages:
      • ecsy-webpack-boilerplate: Standard Webpack setup.
      • ecsy-three-webpack: Webpack setup pre-configured with Three.js.
      • ecsy-typescript-boilerplate: For TypeScript-based projects.
    • Framework Integrations:
      • ecsypixi: Integration with PixiJS.
      • react-ecs: Integration with React.