hecs

repository·master·Indexed 23 days ago

https://github.com/ralith/hecs

A fast, minimal, and ergonomic entity-component-system (ECS) library for Rust. It uses an archetypal memory layout for high performance and cache locality, allowing developers to query entities directly from their code without a rigid 'System' abstraction. Features include a World for entity management, ChangeTracker for detecting component modifications, and CommandBuffer for deferred world mutations.

Tokens
11.7K
Snippets
29
Records
70
Agent score
78%

What's inside hecs

  1. How hecs ECS architecture works

    master

    The hecs architecture is a minimalist Entity-Component-System (ECS) designed for high performance and loose coupling.

    • Entities: Represent distinct objects in your world.
    • Components: Data associated with entities. An entity can have at most one component of any specific type.
    • Systems: Instead of a formal 'System' abstraction, you implement behavior by querying the World for entities that match a specific set of component types. This allows you to organize your application logic however you prefer.

    Internally, hecs uses an archetypal memory layout. It tracks groups of entities that share the same component types and stores them in dense, contiguous arrays. This provides excellent cache locality and allows for fast linear traversals during queries, similar to a columnar database.

  2. When to use (or avoid) hecs

    master

    Use hecs when:

    • You want to compose loosely-coupled state and behavior.
    • You need high performance and cache locality for batch processing many entities.
    • You want a lightweight, unobtrusive library rather than a full-featured framework.

    Avoid hecs when:

    • Your application has very few types of entities (a simple Vec might be more efficient).
    • Your primary logic does not involve batch processing entities.
    • You need to search for entities based on criteria other than component types (in this case, maintain a specialized index alongside the World that stores Entity handles).
  3. Understand Ref and RefMut component borrows

    master

    When you use EntityRef::get::<&T>() or EntityRef::get::<&mut T>(), you receive a Ref<'a, T> or RefMut<'a, T> respectively. These act as smart pointers that manage the borrow state of the component within the archetype.

    • Ref<'a, T>: A shared borrow. It implements Deref to provide access to &T. It can be cloned and mapped to sub-fields.
    • RefMut<'a, T>: A unique borrow. It implements Deref (for &T) and DerefMut (for &mut T). It can be mapped to sub-fields.

    Both types ensure that the archetype's borrow state is correctly updated and released when the reference is dropped.

  4. Detect component changes with ChangeTracker

    master

    The ChangeTracker<T> provides a way to detect when components of type T have been added, modified, or removed from entities in a World.

    Usage Requirements

    • Component Constraints: The component T must implement Component, Clone, and PartialEq.
    • Single Tracker Rule: Always use exactly one ChangeTracker per World per component type. Using multiple trackers for the same type on the same world, or sharing a tracker across multiple worlds, will cause unpredictable results.
    • Performance Note: This mechanism works by inserting a private Previous<T> component to store the last known state. It is best suited for components that are fast to Clone and PartialEq. For expensive components, consider manual change tracking (e.g., via a flag in DerefMut).

    Workflow

    1. Create a tracker using ChangeTracker::new().
    2. Call tracker.track(&mut world) to obtain a Changes object.
    3. Iterate over the desired change sets (added(), changed(), or removed()) using the Changes object.
    4. The Changes object must be dropped (e.g., by letting it go out of scope) to finalize the tracking state and update the internal Previous<T> components.
  5. How `flush` works with reserved entities

    master

    When you reserve entities using reserve_entity or reserve_entities, the system tracks these IDs but does not yet expand the internal metadata storage. To finalize these allocations, you must call flush(init).

    The init closure is called for every reserved entity, providing its id and a mutable reference to its Location.

    flush performs two main tasks:

    1. It expands the internal metadata storage to accommodate new IDs.
    2. It initializes the Location for both newly created IDs and IDs that were previously in the freelist (reused IDs).
  6. Use `Satisfies<Q>` for high-performance existence checks

    master

    If you only need to know if an entity has certain components but don't need to access the data itself, use Satisfies<Q>.

    Unlike Option<Q>, which performs dynamic borrows on the components, Satisfies<Q> does not borrow any components. This makes it significantly faster and more concurrency-friendly, as it only checks for the presence of the component type in the archetype.

  7. Querying the World with `World::query`

    master

    To iterate over entities and components, use World::query::<Q>(). This returns a QueryBorrow which provides access to the data. You can use .iter() for standard iteration, .view() for random access, or .iter_batched(batch_size) to distribute work across threads.

    Common query types include:

    • (Entity, &T, &mut U): A tuple of components and the entity ID.
    • Option<T>: An optional component (returns None if the entity lacks T).
    • Or<L, R>: Matches either query L or query R.
    • With<Q, R>: Matches query Q only if the entity also satisfies R.
    • Without<Q, R>: Matches query Q only if the entity does not satisfy R.
    • Satisfies<Q>: Matches all entities, yielding true if they satisfy Q and false otherwise. This is faster than Option<Q> for checking existence because it doesn't borrow components.
    let mut world = World::new();
    let a = world.spawn((123, true, "abc"));
    let b = world.spawn((456, false));
    let c = world.spawn((42, "def"));
    
    // Example: Using With to filter
    let entities = world.query::<With<(Entity, &i32), &bool>>()
        .iter()
        .map(|(e, i)| (e, i))
        .collect::<Vec<_>>();
    
    // Example: Using Satisfies for fast existence checks
    let entities = world.query::<(Entity, Satisfies<&bool>)>()
        .iter()
        .collect::<Vec<_>>();
  8. Use EntityBuilder to incrementally construct entities

    master

    The EntityBuilder allows you to incrementally add components to a bundle before spawning an entity in the World. This is useful for constructing complex entities step-by-step.

    Key behaviors:

    • Replacement: If you add a component of a type T that is already in the builder, the old component is dropped and replaced by the new one.
    • Reuse: You can reuse the same builder instance by calling .build() to get a BuiltEntity, which can then be passed to world.spawn(). The builder is cleared implicitly when built, making it ready for the next entity.
    • Access: You can check for component existence with .has::<T>() or borrow components using .get::<T>() (shared) and .get_mut::<T>() (unique) during the building process.
    # use hecs::*;
    let mut world = World::new();
    let mut builder = EntityBuilder::new();
    builder.add(123).add("abc");
    let e = world.spawn(builder.build()); // builder can now be reused
    assert_eq!(*world.get::<&i32>(e).unwrap(), 123);
    assert_eq!(*world.get::<&&str>(e).unwrap(), "abc");
  9. Use CommandBuffer for deferred world mutations

    master

    A CommandBuffer allows you to record operations (like inserting components, removing components, despawning entities, or running arbitrary code) to be applied to a World at a later time. This is useful for avoiding borrow checker issues or managing operation ordering during system execution.

    To use it:

    1. Create a new buffer with CommandBuffer::new().
    2. Record operations using methods like insert, spawn, remove, or despawn.
    3. Apply all recorded operations to a World instance using run_on(&mut world).

    Note that run_on clears the command buffer, allowing it to be reused.

    ```rust
    # use hecs::*;
    let mut world = World::new();
    let entity = world.reserve_entity();
    let mut cmd = CommandBuffer::new();
    cmd.insert(entity, (true, 42));
    cmd.run_on(&mut world); // cmd can now be reused
    assert_eq!(*world.get::<&i32>(entity).unwrap(), 42);
    ```埋
  10. Create and manage a World

    master

    The World is the primary container for entities and components in hecs. It stores components in contiguous runs (archetypes) for cache-friendly iteration.

    To create a new, empty world, use World::new().

    Note on Entity Collisions: hecs uses a finite number of entity IDs. When entities are despawned, their IDs are eventually reused. In extremely long-lived applications (billions of spawns/despawns), a preserved handle to a despawned entity might eventually collide with a new entity. It is best practice to avoid retaining handles to despawned entities indefinitely.

    let mut world = World::new();
  11. Use EntityBuilderClone for repeated spawning

    master

    If you need to spawn multiple entities with the exact same set of components, use EntityBuilderClone. Unlike the standard EntityBuilder, which is consumed or cleared upon building, EntityBuilderClone produces a BuiltEntityClone that can be cloned and used to spawn many entities.

    Key behaviors:

    • Clonable Components: Only components that implement Clone can be added via .add().
    • Repeated Spawning: The output of .build() is a BuiltEntityClone. Because &BuiltEntityClone implements DynamicBundle, you can pass a reference to it to world.spawn() multiple times.

    Note: EntityBuilderClone does not perform dynamic borrow checking during the building phase.

    # use hecs::*;
    let mut world = World::new();
    let mut builder = EntityBuilderClone::new();
    builder.add(123).add("abc");
    let bundle = builder.build();
    let e = world.spawn(&bundle);
    let f = world.spawn(&bundle); // `&bundle` can be used many times
    assert_eq!(*world.get::<&i32>(e).unwrap(), 123);
    assert_eq!(*world.get::<&&str>(e).unwrap(), "abc");
    assert_eq!(*world.get::<&i32>(f).unwrap(), 123);
    assert_eq!(*world.get::<&&str>(f).unwrap(), "abc");
  12. Basic usage of hecs World

    master

    hecs is a minimalist Entity-Component-System (ECS). Instead of a rigid 'System' abstraction, you use a World to spawn entities with various components and query them using standard Rust loops. Any type can be used as a component with zero boilerplate.

    To use hecs:

    1. Create a World using World::new().
    2. Spawn entities with world.spawn((component1, component2, ...)).
    3. Query entities using world.query_mut::<(&mut Type1, &Type2)>() for mutable access or world.query_shared::<(&Type1, &Type2)>() for read-only access.
    4. Access specific components of an entity using world.get::<&Type>(entity).
    use hecs::*;
    let mut world = World::new();
    
    // Spawn entities with components
    let a = world.spawn((123, true, "abc"));
    let b = world.spawn((42, false));
    
    // Systems are simple for loops using queries
    for (number, &flag) in world.query_mut::<(&mut i32, &bool)>() {
      if flag { *number *= 2; }
    }
    
    // Random access is simple and safe
    assert_eq!(*world.get::<&i32>(a).unwrap(), 246);
    assert_eq!(*world.get::<&i32>(b).unwrap(), 42);