EliCS Entity Component System

repository·main·Indexed 42 days ago

https://github.com/elixr-games/elics

A lightweight, high-performance ECS framework optimized for complex 3D web applications. EliCS focuses on performance, ergonomics, and flexibility, utilizing TypedArrays for memory efficiency and cache locality. Version 3.4.2 features a centralized World orchestrator, bitmasking for high-performance queries, and a ComponentRegistry for serialized data reconstruction.

Tokens
22K
Snippets
91
Records
105
Agent score
53%

What's inside EliCS

  1. What is EliCS?

    main

    EliCS is a lightweight, flexible, and robust Entity-Component-System (ECS) framework for TypeScript and JavaScript. It is specifically designed for building dynamic and complex applications, such as 3D web applications and games.

    Inspired by the architecture of the archived ECSY project, EliCS prioritizes developer ergonomics and type-safety over extreme low-level performance optimizations (like those found in BitECS), making it suitable for complex state management in 3D environments where rendering is typically the primary bottleneck.

  2. Overview of EliCS

    main

    EliCS is a lightweight Entity Component System (ECS) framework designed for complex 3D web applications. It focuses on three core pillars:

    • Performance: Optimized for high-performance requirements in 3D environments.
    • Ergonomics: Designed to be developer-friendly and easy to use.
    • Flexibility: Supports various programming styles and architectural approaches.

    For detailed implementation details, refer to the official documentation site.

  3. Understand the EliCS Architecture Overview

    main

    EliCS is an implementation of the Entity Component System (ECS) design pattern. It is designed for flexibility, efficiency, and scalability in game development and complex applications. The architecture is built around five primary pillars that interact through a central orchestrator:

    • Entities: Pure containers for components; they hold no data or logic themselves.
    • Components: Pure data containers that define properties or behaviors. They are modular and can be dynamically attached/detached. Components are automatically registered in a global ComponentRegistry.
    • Systems: The logic layer. Systems operate on sets of entities that match specific component requirements.
    • Queries: The selection mechanism used by systems to find entities. EliCS uses bitmasking for high-performance query evaluation.
    • World: The central orchestrator that manages the lifecycle and interaction of all the above elements.
  4. What is an Entity in EliCS?

    main

    An Entity is a core component of the EliCS ECS architecture, acting as a container for multiple components. It represents individual objects or actors within your application.

    Key characteristics include:

    • Component Management: Adding, accessing, and removing components.
    • Lifecycle: Managing active states and destruction.
    • Query Integration: Filtering and processing via queries.
    • Vector Views: Optimized access to array-like component data.

    Note on Creation: You should never instantiate an Entity directly. Instead, use the World.createEntity() method. The system uses an internal pooling mechanism via the EntityManager to reuse inactive entities, which minimizes garbage collection and improves performance.

    import { World } from 'elics';
    
    const world = new World();
    const entity = world.createEntity();
  5. Configure System behavior with a Schema

    main

    Systems can use a schema to define configuration data that can be customized during registration. This data is accessed via the this.config property.

    EliCS uses @preact/signals-core for configuration, meaning this.config properties are Signals. You can read their .value, subscribe to changes, or update them at runtime to trigger reactive logic.

    // 1. Define schema
    const schema = {
    	healthRegen: { type: Types.Float32, default: 0.5 },
    };
    
    // 2. Register with overrides
    world.registerSystem(EnemySystem, { configData: { healthRegen: 1.0 } });
    
    // 3. Access in system
    class EnemySystem extends createSystem(queries, schema) {
    	update(delta: number): void {
    		const rate = this.config.healthRegen.value;
    	}
    }
  6. How components and entities work together

    main

    In EliCS, components are not instantiated per entity. Instead, a Component instance acts as a centralized manager for a specific data type. When a component is attached to an entity, the entity's unique index is used to look up or update its specific data within the component's internal optimized arrays.

    This architecture provides:

    • Memory Efficiency: Avoids creating thousands of small objects.
    • Cache Locality: Data is stored in contiguous memory (e.g., Float32Array), making iteration extremely fast.
    • Batch Operations: Allows for high-performance bulk data processing.
  7. How the System class works in EliCS

    main

    A System is a blueprint for application logic in EliCS. It processes entities that match specific component criteria defined via static queries. Systems follow a managed lifecycle (initialization, updates, pausing, and destruction) and can be prioritized to control their execution order within the game loop. When registered with a World, their queries are automatically managed by a QueryManager using efficient BitSet operations.

    import { createSystem, Entity } from 'elics';
    import { EnemyComponent } from './EnemyComponent';
    
    const queries = {
    	enemies: { required: [EnemyComponent] },
    };
    
    const schema = {
    	healthRegen: { type: Types.Float32, default: 0.5 },
    };
    
    class EnemySystem extends createSystem(queries, schema) {
    	init(): void {
    		this.queries.enemies.subscribe('qualify', (entity: Entity) => {
    			console.log('new enemy entity:', entity);
    		});
    	}
    
    	update(delta: number, time: number): void {
    		this.queries.enemies.entities.forEach((enemy: Entity) => {
    			EnemyComponent.data['health'][enemy.index] +=
    				this.config.healthRegen * delta;
    		});
    	}
    }
  8. How the World class works in EliCS

    main

    The World class is the central hub of the EliCS ECS architecture. It orchestrates the interaction between entities, components, queries, and systems.

    Internally, the World manages three core components:

    • ComponentManager: Handles component registration and optimized storage using typed arrays.
    • EntityManager: Manages entity creation and recycling via a pooling mechanism to minimize garbage collection.
    • QueryManager: Uses BitSet masks for fast entity filtering and supports deferred updates to batch processing.

    Systems are executed based on their registration priority (lower-priority systems run first). This architecture is designed for high-performance, cache-friendly access and efficient memory management.

    import { World } from 'elics';
    
    const world = new World();
    // The world is now ready to orchestrate components, entities, and systems.
  9. How EliCS ECS principles work

    main

    EliCS follows the Entity Component System (ECS) pattern, which uses Data-Oriented Programming to favor composition over class hierarchies. The core abstractions are:

    • Entities: Unique objects that act as containers for components.
    • Components: Data structures representing facets of an entity (e.g., position, health).
    • Systems: Logic that processes entities and modifies their components.
    • Queries: Mechanisms used by systems to select specific entities based on their component composition.
    • World: The central container that manages entities, components, systems, and queries.
  10. How Queries work in EliCS

    main

    A Query is a tool used to select entities based on their component composition. It uses bitmask operations for high-performance, constant-time evaluations.

    Core Mechanics

    • requiredMask: A bitwise OR of all required components. An entity must have all these components to match.
    • excludedMask: A bitwise OR of all excluded components. An entity must have none of these components to match.
    • Bitmask Matching: An entity matches if (entityMask & requiredMask) === requiredMask and (entityMask & excludedMask) === 0.

    Queries are highly efficient because structural bitmask checks are performed before any value-based predicates are evaluated.

    import { createSystem, Entity } from 'elics';
    
    // Example of a system using a query
    const queryConfig = {
    	AButNotC: { required: [ComponentA], excluded: [ComponentC] },
    };
    
    class GenericSystem extends createSystem(queryConfig) {
    	update(): void {
    		const entities = this.queries.AButNotC.entities;
    		entities.forEach((entity: Entity) => {
    			// Process matching entities
    		});
    	}
    }
  11. Use ComponentRegistry for component lookup

    main

    The ComponentRegistry is a global registry that automatically records every component created via createComponent. It provides lookup capabilities that are essential for runtime entity reconstruction (e.g., from serialized data) and build-time analysis tools.

    Key features include:

    • Automatic Recording: No manual registration is required after calling createComponent.
    • ID-based Lookup: Retrieve component definitions using their string identifier.
    • Integration: Enables external tools (like Unity or Blender) to discover component schemas via AST parsing or runtime registry access.
    import { createComponent, ComponentRegistry, Types } from 'elics';
    
    // Components auto-record upon creation
    const HealthComponent = createComponent('Health', {
    	current: { type: Types.Float32, default: 100 },
    	maximum: { type: Types.Float32, default: 100 },
    });
    
    // Lookup components by ID
    const healthComp = ComponentRegistry.getById('Health');
    
    // Check existence
    if (ComponentRegistry.has('Health')) {
    	console.log('Health component is registered');
    }
    
    // Get all registered components
    const allComponents = ComponentRegistry.getAllComponents();
  12. Access and mutate component data

    main

    To interact with component data on an entity:

    1. Standard Data: Use entity.getValue(Component, 'key') to read and entity.setValue(Component, 'key', value) to write.
    2. Vector/Color Data: For components using Types.Vec2, Types.Vec3, Types.Vec4, or Types.Color, you must use entity.getVectorView(Component, 'key').

    Warning: Attempting to use getValue or setValue on vector/color fields will throw a runtime error to enforce proper mutation of these types.

    // Correct way to mutate vector data
    const position = entity.getVectorView(Position, 'value');
    position[0] += velocity[0] * delta;
    
    // Correct way to mutate standard data
    entity.setValue(Unit, 'state', CombatState.Idle);