GECS Entity Component System

repository·main·Indexed 20 days ago

https://github.com/csprance/gecs

An Entity Component System (ECS) for Godot 4.6+ designed to separate game data from logic. It features a dedicated Network Addon for declarative multiplayer synchronization with priority groups, native transform sync via MultiplayerSynchronizer, and authority markers. The framework includes tools for deferred execution via CommandBuffer, system throttling with SystemTimer, entity relationships, and game state serialization.

Tokens
65K
Snippets
192
Records
265
Agent score
69%

What's inside GECS

  1. Overview of the Sheep Herding Demo

    main

    The Sheep Herding demo is a small ECS (Entity Component System) demonstration built on top of GECS. It showcases several advanced reactive and relationship-driven patterns:

    • Reactive Observers: Responding to specific events rather than running every frame.
    • Relationship-driven Flocking: Using relationships between entities to avoid expensive $O(N^2)$ distance scans.
    • CommandBuffer-safe State Machines: Managing state transitions (like fleeing or wandering) safely.
    • Movement Integration: Combining CharacterBody3D and NavigationAgent3D with ECS-driven velocity.

    Goal: Drive the shepherd (WASD + Shift to sprint) to herd sheep into the pen.

  2. GECS Network Features and Capabilities

    main

    The GECS Network Addon provides several high-level features for multiplayer development:

    • Declarative sync priorities: Use @export_group("HIGH"), "MEDIUM", "LOW", "SPAWN_ONLY", or "LOCAL" on component properties to manage bandwidth without external config.
    • Native transform sync: Adding CN_NativeSync to an entity triggers NativeSyncHandler to manage a MultiplayerSynchronizer with interpolation support.
    • Authority markers: The framework automatically injects CN_LocalAuthority, CN_RemoteEntity, and CN_ServerAuthority at spawn. Use these in ECS queries to gate systems based on ownership.
    • Relationship sync: Entity relationships are synchronized across peers with deferred resolution to handle non-deterministic spawn ordering.
    • Periodic reconciliation: Configurable full-state broadcasts correct state drift automatically.
    • Custom sync handlers: You can register per-component-type send/receive handlers at the system level for custom serialization or server correction blending.
    • Zero overhead in single-player: NetworkSync detects offline mode and skips all RPC and synchronization logic.
  3. Apply the Single Responsibility Principle to systems

    main

    Each System should handle exactly one specific concern. A system should define a narrow query() that selects only the entities possessing the specific components it needs to process.

    # Good - Focused systems
    class_name MovementSystem extends System
    func query(): return q.with_all([C_Position, C_Velocity])
    
    class_name RenderSystem extends System
    func query(): return q.with_all([C_Position, C_Sprite])
    
    class_name HealthSystem extends System
    func query(): return q.with_all([C_Health])
  4. How Entities, Components, and Systems work together

    main

    GECS follows the Entity Component System pattern:

    • Entities: Objects that exist in the world (often wrapping a Godot Node).
    • Components: Pure data containers (Resources) attached to entities.
    • Systems: Logic providers that query() for entities possessing specific components and process() them.

    This decoupling allows you to scale behavior easily. For example, adding a new entity with an existing component (like C_Velocity) automatically makes it subject to any existing systems (like MovementSystem) without modifying the system's code.

  5. Understand the cost and constraints of GECSTracker

    main

    When using dependency tracking, be aware of the following performance characteristics and limitations:

    • Performance (Inactive vs Active):
      • Inactive (Default): When not tracking, there is minimal overhead (a single static boolean branch in QueryBuilder.execute(), Entity.get_component(), and has_component()). No allocations or callables are invoked.
      • Active: When track() is running, there is a cost of one static callback per query execution or component read, but only for the duration of the tracked call.
    • No Re-entrancy: You must not nest track() calls. The system uses a single static accumulator, so nested tracking is not supported.
    • Custom Instrumentation: While GECSTracker.track() is the intended entry point, the low-level hooks QueryBuilder.set_execute_tracker and Entity.set_read_tracker are public if you need to implement custom instrumentation.
  6. How GECS core concepts work together

    main

    GECS follows an Entity-Component-System (ECS) architecture:

    • Entities: Nodes that hold data via Components. They can be spatial (attached to Node3D/Node2D scenes) or pure data (code-based).
    • Components: Data-only resources that define properties. They must have default values for all properties to avoid Godot export errors.
    • Systems: Logic containers that process entities. They use a query() to select entities and a process() method to operate on them.
    • World: The central tracker that manages all entities and drives system processing.

    Data flows through components, and logic lives exclusively in systems. Nothing communicates directly.

  7. Understand GECS Architecture and Query Performance

    main

    GECS uses a hybrid archetype ECS architecture to optimize performance. Entities with identical component signatures are grouped into an Archetype.

    Key architectural features include:

    • SoA (Structure of Arrays) Columns: Each component type has a column in the archetype that is index-aligned with the entities array. This allows iterate() to provide systems with raw column arrays for zero-copy iteration.
    • Cached Queries: Queries resolve to a query → matching-archetypes cache. Execution involves a single dictionary lookup and a flatten operation, making it highly efficient.
    • Archetype Transitions: Uses add_edges/remove_edges for $O(1)$ transitions after an initial warmup period.
    • Enabled Bitsets: An enabled_bitset allows enabling or disabling entities without splitting them into different archetypes.
  8. Match relationships using Type Matching and Component Queries

    main

    GECS provides two modes for matching relationships in queries:

    1. Type Matching (Default)

    Matches relationships by the component type, ignoring specific property values. Use this to find entities with "any fire damage" or "any buff of this type".

    # Matches any fire damage effect by type
    ECS.world.query.with_relationship([Relationship.new(C_Damaged.new(), C_FireDamage)])

    2. Component Query Matching

    Matches relationships by specific property criteria using dictionaries. This allows you to filter by values like amount, duration, or level.

    Supported Operators:

    • _eq (equal)
    • _ne (not equal)
    • _gt (greater than)
    • _lt (less than)
    • _gte (greater than or equal)
    • _lte (less than or equal)
    • _in (in list)
    • _nin (not in list)
    • func (custom function)

    Example of matching a relation property and a target property:

    # Match C_Damaged relationships where amount >= 50
    ECS.world.query.with_relationship([
        Relationship.new(C_Damaged.new(), {C_FireDamage: {'amount': {'_gte': 50}}})
    ])
    # Match C_Damaged relationships where amount >= 50
    var high_fire_damaged = ECS.world.query.with_relationship([
        Relationship.new(C_Damaged.new(), {C_FireDamage: {'amount': {'_gte': 50}}})
    ]).execute()
    
    # Match both relation AND target with queries
    var strong_buffs = ECS.world.query.with_relationship([
        Relationship.new(
            {C_Buff: {'duration': {'_gt': 10}}},
            {C_Player: {'level': {'_gte': 5}}}
        )
    ]).execute()
  9. Optimize Relationship Queries for Performance

    main

    Relationship queries have different performance characteristics based on how they are structured:

    1. Prefer Specific Targets (O(1))

    Queries that specify an exact target entity resolve in O(1) time because the archetype cache returns the result instantly. Relationship.new(C_X, target_entity) is highly efficient.

    2. Avoid Wildcard Scans (O(N))

    Queries using a null target (wildcards) must scan all matching archetypes, which scales linearly with the number of entities (O(N)).

    3. Use Component Filtering to Narrow Wildcards

    To optimize wildcard queries, always combine them with a with_all() component filter. This narrows the search space to a small set of archetypes before the wildcard scan begins.

    Slow (Wildcard scan on all archetypes):

    var all_poisoned = ECS.world.query.with_relationship([Relationship.new(C_Poison.new(), null)]).execute()

    Fast (Narrowed by component first):

    var all_poisoned = ECS.world.query.with_all([C_Alive]).with_relationship([Relationship.new(C_Poison.new(), null)]).execute()
    # Fast - component query narrows to small set first, wildcard only runs on those
    var all_poisoned = ECS.world.query
        .with_all([C_Alive])  # structural O(1) archetype lookup first
        .with_relationship([Relationship.new(C_Poison.new(), null)])
        .execute()
  10. What are Relationships in GECS?

    main

    Relationships in GECS are links that connect entities to other entities, components, or types, allowing for complex game interactions beyond simple component data. A relationship is composed of three parts:

    1. Source: The entity that possesses the relationship.
    2. Relation: A component that defines the type of relationship (e.g., C_Likes).
    3. Target: The object being related to, which can be an Entity, a Component instance, or an Archetype (class/type).

    Relationships can be simple links or can carry data about the connection itself via the Relation component.

  11. Manage archetype lifecycle and memory

    main

    In GECS v9, empty archetypes are retained to ensure transition edges survive spawn/despawn churn. While they are invisible to queries, they occupy memory. To reclaim memory from empty archetypes, call world.compact() during quiet periods, such as level transitions.

    Additionally, note that world.entities order is not stable across removals because GECS uses an O(1) swap-remove strategy. If your logic depends on insertion order, you must sort the entities explicitly.

  12. Limitations of Monitors with Godot groups and Relation components

    main

    Be aware of these two specific limitations regarding monitors (on_match / on_unmatch):

    1. Godot Groups: Monitors do not react to Godot's built-in group changes. A query like q.with_group("x").on_match().on_unmatch() only re-evaluates on component or relationship mutations. Calling node.add_to_group("x") will not trigger the monitor. To fix this, pair group filters with a component marker (e.g., C_Target).
    2. Relation Component Properties: Monitors using property-query relationships (e.g., checking a property on a relationship component) do not transition when that property changes. They only re-evaluate on structural mutations (adding or removing the relationship).
      • Example of what fails: q.with_relationship([Relationship.new({C_Buff: {"duration": {"_gt": 0}}}, null)]).on_match().on_unmatch() will not trigger when duration changes.
      • Workaround: Mirror the relevant property onto a direct component on the entity and monitor that component instead.