fennecs

repository·main·Indexed 19 days ago

https://github.com/outfox/fennecs

A high-performance Entity Component System (ECS) for C#. The library provides tools for managing entities, components, and queries, featuring support for entity-entity relations, object links, and bulk operations. This documentation includes a cookbook of 'appetizer' recipes for core ECS concepts and demos for integrating fennecs with Godot 4.6+ (.NET edition).

Tokens
57.9K
Snippets
163
Records
226
Agent score
63%

What's inside fennecs

  1. Planned Roadmap for fennecs

    main

    The fennecs roadmap outlines the evolution of the ECS from its current beta state toward a stable 1.0.0 release. Key upcoming milestones include:

    • Aspects (v0.7.0): Introduction of self-contained collections of Archetypes with contiguous memory layouts to group hot data and reduce fragmentation.
    • Unified Entity (v0.7.x): A refactor of the entity struct to improve memory bandwidth and support new key types like hash keys.
    • Easy SIMD (v0.8.0+): Methods for SIMD-accelerated arithmetic operations on Component data, inspired by TensorPrimitives.
    • Benchmark Suite (v0.9.0+): An internal benchmark suite (fennecs Arena) to ensure performance and memory usage standards.
    • Stable Release (v1.0.0): Marks the end of Beta, featuring complete documentation, web support (including Godot via Godot 4.4+), and expanded 3rd party engine demos (Stride, Flax, MonoGame, etc.).
  2. What is a Component in fennecs?

    main

    A Component is any piece of data attached to an Entity. Components are the primary way to provide entities with properties, behaviors, and relationships. In fennecs, components can be value types (structs), reference types (classes), tags (zero-size markers), or relationships to other entities and objects.

    Whenever a component is added to or removed from an entity, that entity moves to a new Archetype.

    // Simple data component
    entity.Add(new Position { X = 10, Y = 20 });
    
    // Tag component (zero-size marker)
    entity.Add<Enemy>();
    
    // Relation to another entity
    entity.Add<ChildOf>(parentEntity);
    
    // Link to a managed object
    entity.Add(Link.With(gameObject));
  3. What is an Entity in fennecs?

    main

    An Entity is a lightweight, immutable 64-bit handle (implemented as a readonly record struct) that represents an identity within a World. It is used to attach components to create composable game state. Entities can be stored in variables, collections, or as components on other entities.

    To use an entity, you typically spawn it from a World and then use methods to add or remove data.

    var player = world.Spawn();           // Create an entity
    player.Add(new Health { Value = 100 }); // Give it components
    player.Add<Player>();                   // Tag it as the player
  4. What is an Aspect in fennecs?

    main

    An Aspect is a self-contained collection of Archetypes within a World. It acts as a contiguous component storage universe. While all Aspects in a World share the same set of Entities, the actual component data is partitioned into these different storage neighborhoods.

    Key Characteristics:

    • The Main Aspect: Every World has a built-in Aspect named "main" (World.Main). It is the default storage for any component type that hasn't been explicitly assigned to another Aspect. Every living Entity is a member of the Main Aspect.
    • Fragmentation Fighting: Aspects are used to group "hot data" (components accessed frequently, like Position or Velocity) into their own storage. This prevents the combinatorial explosion of Archetypes that occurs when many different gameplay components are mixed with high-frequency movement components, thereby improving cache locality.
    • Entity Identity: An Entity exists across all Aspects it has components in. An Entity is a single identity, even if its data is spread across multiple Aspects.
  5. What is an Object Link and how does it work?

    main

    An Object Link is a special type of Relation in fennecs ECS that associates a non-entity Object (a reference type like a string, a game engine Node, or a class instance) as the secondary key in a Type Expression.

    Unlike standard Entity-Entity Relations, the Link's target is the backing data. This means:

    1. Grouping: You can group Entities by shared non-entity objects.
    2. Bidirectional Access: Because the object is the data, the Entity linked to the object has full access to the object itself during enumeration.
    3. Multiplicity: An entity can have multiple Object Links of the same type (e.g., an entity can be linked to multiple different Bank objects).
    // Example: bob has two Bank relations (each backed by a reference to the object)
    bob.Add(Link.With(chase)); // bob banks at chase (Type Bank->chase)
    bob.Add(Link.With(targo)); // bob also banks at targo (Type Bank->targo)
  6. What is an Aspect and how to use it to mitigate fragmentation

    main

    An Aspect is a self-contained collection of Archetypes within a World, acting as its own contiguous component storage universe.

    Key Characteristics:

    • Shared Entities: All Aspects in a single World share the same set of Entities.
    • Default Aspect: Every World starts with a default Aspect named Main.
    • Customization: You can create new Aspects using World.AddAspect to group 'hot' data together.
    • Ownership: Component types are assigned to a specific Aspect using Aspect.Owns<T>().
    • Query Constraints: A Query can only match types that are stored within a single Aspect.

    Use Case: Mitigating Fragmentation

    Archetype Fragmentation occurs when entities have many unique combinations of components, links, or relations, creating many small Archetypes. This reduces parallelization efficiency. Using Aspects allows you to group high-frequency (hot) components into their own storage, preventing them from being fragmented by the addition or removal of other, less frequent components.

    // Example of adding an Aspect (conceptual based on documentation)
    var aspect = myWorld.AddAspect();
    aspect.Owns<MyHotComponent>();
  7. Use type wrapping to avoid ambiguous data

    main

    Avoid using primitive types (like float or int) directly as components. Using primitives leads to ambiguity (e.g., which float is speed vs. health?) and prevents you from having multiple components of the same primitive type on a single entity. Instead, wrap primitives in unique record struct types to make components self-documenting and distinct.

    Avoid:

    entity.Add<float>(10.0f); // Ambiguous

    Use Type Wrapping:

    public record struct Speed(float Value);
    public record struct Health(float Value);
    
    entity.Add(new Speed(10.0f));
    entity.Add(new Health(100.0f));
  8. Understand Secondary Keys in fennecs

    main
    In fennecs, component types typically act as primary keys. Secondary Keys allow a component to reference an additional Entity or Object, transforming a standard component into a relationship model. This enables you to create connections between different parts of your ECS world rather than just storing isolated data.
  9. Add multiple components of the same type using match expressions

    main

    An entity can hold multiple components of the same type if they are distinguished by different match expressions. For example, you can have multiple int components if some are plain values and others are relations to different entities.

    entity.Add<int>(100);              // Plain int
    entity.Add<int>(50, target1);      // int relation to target1
    entity.Add<int>(25, target2);      // int relation to target2
    
    // The entity now contains three distinct int components
    // All of these are different components!
    entity.Add<int>(100);              // Plain int
    entity.Add<int>(50, target1);      // int relation to target1
    entity.Add<int>(25, target2);      // int relation to target2
    
    Console.WriteLine(entity.Get<int>(Match.Any).Length);  // 3
  10. Use Wildcards to match Secondary Keys

    main

    When performing Queries or using structural change methods like Remove<C>(Match), you can use Wildcard Match Expressions to target whole categories of secondary keys.

    Supported wildcard expressions include:

    • Match.Any
    • Match.Target
    • Entity.Any
    • Link.Any

    These allow you to, for example, strip all components of a certain type that match a specific relationship pattern in a single operation.

  11. Understand Archetypes and performance

    main

    In fennecs, entities with identical combinations of component types share the same Archetype.

    This grouping allows the engine to store entities with the same "shape" together in contiguous memory, enabling high-performance iteration. When you add or remove a component that changes an entity's composition, the entity moves to a different Archetype.

    Warning: When an entity is despawned, all its components are removed, and the handle becomes a stale reference to a recycled identity.

  12. Understand Entity identity, despawning, and recycling

    main

    This guide explores how entities behave within a World regarding their identity and lifecycle. Key concepts include:

    • Identity & Logging: The ToString() method on an Entity provides a human-readable representation of the entity's identity, useful for logs and DebuggerDisplay.
    • Despawning: When an entity is despawned, it is removed from the active world.
    • Recycling & Generations: To optimize memory, fennecs recycles entity slots. When an entity is destroyed and a new one is spawned in its place, the World increments the Generation count. This allows you to distinguish between a new entity and a recycled entity that previously occupied the same memory slot.

    This pattern is essential for debugging lifecycle issues and understanding how the World manages entity memory.

    // Conceptual usage based on the Star Trek recipe
    // Spawning entities, despawning them, and observing generation increments
    
    // 1. Spawn an entity
    var kirk = world.Spawn();
    
    // 2. Despawn the entity
    world.Despawn(kirk);
    
    // 3. Spawn a new entity (may recycle the same slot but with a higher generation)
    var picard = world.Spawn();
    
    // Observe identity via ToString()
    Console.WriteLine(kirk.ToString());
    Console.WriteLine(picard.ToString());