jecs

repository·main·Indexed 19 days ago

https://github.com/ukendio/jecs

A high-performance, type-safe Entity Component System (ECS) designed for Luau. Optimized for massive entity counts and cache-friendly operations using an archetype/SoA approach, jecs supports entity relationships as first-class citizens and provides a type-safe API with zero external dependencies. Key features include cached queries for performance-critical loops, bulk component operations, and lifecycle observers for component changes.

Tokens
6.8K
Snippets
25
Records
34
Agent score
63%

What's inside @rbxts/jecs

  1. Overview of jecs features and performance

    main

    jecs is a high-performance Entity Component System (ECS) designed for Luau. Key features include:

    • Entity Relationships: Supported as first-class citizens.
    • High Performance: Capable of iterating 800,000 entities at 60 FPS.
    • Type-Safety: Provides a type-safe Luau API.
    • Optimized Storage: Uses an archetype/SoA (Structure of Arrays) approach optimized for column-major operations and cache friendliness.
    • Zero-dependency: The package has no external dependencies.
  2. How Relationships and Pairs work

    main

    Jecs supports relationships using Pair entities. A Pair represents a connection between a predicate entity and an object entity.

    • pair(pred, obj): Creates a composite key representing a relationship.
    • world.target(entity, relation, [index]): Gets the target of a relationship (e.g., finding the parent in a ChildOf relationship).
    • world.targets(entity, relation): Returns an iterator of all targets for a given relationship.
    • world.parent(entity): A convenience method to get the target of a ChildOf relationship.
    • world.children(parent): Returns an iterator of all child entities of a parent (using ChildOf internally).
    import { world, pair, ChildOf } from 'jecs';
    
    const world = world();
    const parent = world.entity();
    const child = world.entity();
    
    // Establish a relationship: child is a ChildOf parent
    const relationship = pair(ChildOf, parent);
    world.set(child, relationship, parent);
    
    // Query the relationship
    const p = world.parent(child);
    if (p === parent) {
      console.log("Found parent!");
    }
    
    // Get all children
    for (const c of world.children(parent)) {
      // ...
    }
  3. Initialize a World

    main

    To use Jecs, you must first create a World instance. This instance acts as the container for all entities, components, and relationships. You can optionally pass a DEBUG boolean to the constructor.

    import { world } from 'jecs';
    
    const world = world(); // or world(true) for debug mode
  4. Get started with jecs

    main

    To begin using jecs, it is recommended to explore the provided subfolders in the repository to understand the design and implementation patterns:

    • how_to/: Provides a step-by-step introduction to the ECS mechanics and the design reasoning.
    • modules/: Contains regularly used modules that can be used to accelerate development.
    • examples/: Contains larger programs demonstrating real-world use cases and how the different parts of the ECS integrate.

    If you are working within Roblox Studio, you can import a model file containing the documentation directly from the provided nightly link.

    https://nightly.link/Ukendio/jecs/workflows/build-studio-docs.yaml/main/studio_docs.zip
  5. How cached queries work and how to manage them

    main

    A cached query is an optimized version of a standard query. It works by:

    1. Archetype Tracking: It pre-calculates which archetypes match the query.
    2. Observer Pattern: It registers observers on the world.observable for EcsOnArchetypeCreate and EcsOnArchetypeDelete. When an archetype is created or destroyed, the cached query updates its internal list of compatible archetypes.
    3. Fast Iteration: The iterator uses the pre-calculated archetype list to jump directly to relevant data.

    Lifecycle Management: Because cached queries register observers on the world, they must be manually cleaned up using query:fini() to avoid memory leaks and stale observers.

  6. Use Entity Relationships (Pairs)

    main

    Jecs supports relationships using pairs. A pair is a single ID representing a connection between two entities (e.g., ChildOf).

    • jecs.pair(A, B): Creates a new relationship ID representing the connection from A to B.
    • world:target(entity, relation, index?): Finds the entity at the other end of a relationship. If index is provided, it allows iterating through multiple targets.
    • world:targets(entity, relation): Returns an iterator function to traverse all entities related to the given entity via the specified relation.
    • jecs.IS_PAIR(id): Checks if an ID is a relationship pair.
    • jecs.pair_first(id) and jecs.pair_second(id): Extracts the constituent components from a pair.
    local ChildOf = world:component()
    local parent = world:entity()
    local child = world:entity()
    
    -- Create the relationship
    local relationship = jecs.pair(ChildOf, parent)
    world:add(child, relationship)
    
    -- Query the relationship
    local found_parent = world:target(child, ChildOf)
  7. Create Entities and Components

    main

    Jecs distinguishes between standard entities and components.

    • Entities: Use world.entity() to create a basic entity (a Tag with no data).
    • Components: Use world.component<TData>() to create a component that holds data of type TData. Components are typically created in the first 256 IDs for fast access.
    • Tags: Use tag() or world.entity() to create a Tag, which is an entity used as a component with no associated data.
    import { world, tag } from 'jecs';
    
    const world = world();
    
    // Create a basic entity (Tag)
    const myEntity = world.entity();
    
    // Create a component with data type
    interface Position { x: number; y: number }
    const Position = world.component<Position>();
    
    // Create a tag specifically
    const MyTag = tag();
  8. Component Lifecycle Hooks

    main

    You can install hooks on components to react to changes in the world:

    • OnAdd: Triggered when a component is added to an entity.
    • OnRemove: Triggered when a component is removed.
    • OnChange: Triggered when a component's value is updated.
    • OnDelete: Triggered when an entity is deleted.

    Use world.set(component, hook, callback) to register these. Note that OnAdd and OnChange provide the new data, while OnRemove can optionally provide a deleted flag.

    const pos = world.component<{x: number}>();
    
    world.set(pos, OnAdd, (entity, id, data) => {
      console.log("Component added to", entity);
    });
    
    world.set(pos, OnChange, (entity, id, data) => {
      console.log("Position changed to", data.x);
    });
  9. Bulk Operations

    main

    For performance-critical updates, use bulk operations to modify multiple components on an entity at once:

    • bulk_insert(world, entity, ids, values): Inserts multiple components/relationships with provided values.
    • bulk_remove(world, entity, ids): Removes multiple components/relationships from an entity.
    import { bulk_insert, bulk_remove } from 'jecs';
    
    // bulk_insert(world, entity, [id1, id2], [val1, val2])
    bulk_insert(world, entity, [pos, vel], [{x: 0}, {v: 1}]);
    
    bulk_remove(world, entity, [pos, vel]);
  10. Query Entities with Components

    main

    Queries allow you to iterate over entities that match specific component requirements.

    • world.query(...components): Returns a Query object.
    • query.with(...components): Refines the query to include more components.
    • query.without(...components): Refines the query to exclude specific components.
    • query.cached(): Returns a CachedQuery for high-performance reuse in loops.
    • query.iter(): Returns an iterator yielding tuples of [Entity, ...queriedComponents].

    To optimize performance, call .with() or .without() before calling .cached().

    const pos = world.component<{x: number}>();
    const vel = world.component<{v: number}>();
    
    // Create a query for entities having both Position and Velocity
    const q = world.query(pos, vel);
    
    // Iterate over results
    for (const [entity, p, v] of q.iter()) {
      console.log(entity, p.x, v.v);
    }
    
    // High-performance cached query
    const cachedQ = world.query(pos).with(vel).cached();
    for (const [entity, p, v] of cachedQ.iter()) {
      // ...
    }
  11. Manage Component Data on Entities

    main

    Use the following methods to manipulate data on entities:

    • world.set(entity, component, value): Assigns a value to a component on an entity. The component can be a single Entity or a Pair (relationship).
    • world.get(entity, ...components): Retrieves values for up to 4 components. Returns a tuple of values (or a single value if only one component is requested), where missing components are undefined.
    • world.add(entity, component): Adds a component (or tag) to an entity without a value.
    • world.remove(entity, component): Removes a specific component from an entity.
    • world.clear(entity): Removes all components and relationships from an entity without deleting the entity itself.
    • world.delete(entity): Completely removes the entity and all its associated data from the world.
    const pos = world.component<{x: number, y: number}>();
    const entity = world.entity();
    
    // Set data
    world.set(entity, pos, { x: 10, y: 20 });
    
    // Get data
    const [p] = world.get(entity, pos);
    console.log(p?.x);
    
    // Remove component
    world.remove(entity, pos);
    
    // Delete entity
    world.delete(entity);