DragonECS

repository·main·Indexed 43 days ago

https://github.com/dcfapixels/dragonecs

A high-performance C# Entity Component System (ECS) framework designed for modularity and extensibility. It supports game engines like Unity and Godot, as well as NativeAOT environments, without requiring code generation or external dependencies. Version 1.1.15 features a fluent API for pipeline management, dependency injection, and optimized component storage via EcsPool and EcsTagPool.

Tokens
28.7K
Snippets
73
Records
82
Agent score
55%

What's inside DragonECS

  1. Define Components in DragonECS

    main

    Components are data structures attached to entities. DragonECS distinguishes between standard components and tag components for optimization:

    • Standard Components: Implement IEcsComponent and are stored in regular storage.
    • Tag Components: Implement IEcsTagComponent and are stored in optimized tag storage (used for marking entities without storing extra data).
    // Standard component
    struct Health : IEcsComponent
    {
        public float health;
        public int armor;
    }
    
    // Tag component
    struct PlayerTag : IEcsTagComponent {}
  2. Use World Components to attach data to the world

    main

    World components allow you to attach extra data to the EcsWorld. They can be struct or class types.

    • Reference types (classes): Initialized to null. Lifecycle callbacks (IEcsWorldComponent<T>) are not supported for reference types; a warning will be issued.
    • Value types (structs): Support the IEcsWorldComponent<T> lifecycle interface for automatic initialization and destruction.
    ref WorldComponent component = ref _world.Get<WorldComponent>();
    
    // Implementing a struct world component with lifecycle callbacks
    public struct WorldComponent : IEcsWorldComponent<WorldComponent>
    {
        private SomeClass _object; // Resource to be released
        
        void IEcsWorldComponent<WorldComponent>.Init(ref WorldComponent component, EcsWorld world)
        {
            // Called before Get<WorldComponent>() returns.
            component._object = new SomeClass();
        }
    
        void IEcsWorldComponent<WorldComponent>.OnDestroy(ref WorldComponent component, EcsWorld world)
        {
            // Called when EcsWorld.Destroy is called.
            component._object.Dispose();
            component._object = null;
            component = default;
        }
    }
  3. How multithreading works in DragonECS

    main

    DragonECS supports multithreading with specific safety rules:

    • Read Operations: Getting components and iterating can be performed in parallel, provided no structural changes occur.
    • Structural Changes: Creating/destroying entities or adding/removing components is not thread-safe and must be synchronized.
    • Isolated Worlds: Separate EcsWorld instances can be processed in different threads (including structural changes) because each world maintains its own state.
    • Singleton Components: Retrieving world-scoped singleton components is thread-safe.

    Parallel Iteration Approaches:

    1. Classic Threads: Uses a separate extension (DragonECS-ClassicThreads).
    2. Unity Jobs: Built-in support via Native views of EcsValuePool<T> pools.
  4. How Entities, Components, and Systems work together

    main

    DragonECS follows the standard ECS pattern:

    • Entities: Basic identifiers for data. Use int for short-term identifiers (valid for one tick) or entlong for long-term unique identifiers that include a generation tag.
    • Components: Data attached to entities. Implement IEcsComponent for standard data or IEcsTagComponent for optimized tag-only storage.
    • Systems: Logic that processes entities. Systems implement lifecycle interfaces like IEcsRun for per-tick logic, IEcsInit for initialization, and IEcsDestroy for cleanup.
  5. Customize metadata with MetaProxy

    main

    The [MetaProxy(typeof(MetaProxyType))] attribute allows for flexible, programmatic metadata configuration. This is useful for bypassing attribute limitations (like non-constant data) and allows metadata to be inherited.

    To use it, your proxy class must inherit from MetaProxyBase and accept a Type in its constructor. This pattern is similar to C#'s [DebuggerTypeProxy].

    // Apply the attribute to a type
    [MetaProxy(typeof(UnityComponent<>.MetaProxy))]
    public struct UnityComponent<T> where T : Component
    {
        private class MetaProxy : MetaProxyBase
        {
            protected TypeMeta Meta = typeof(T).GetMeta();
            public override string Name { get { return Meta?.Name; } }
            public override MetaColor? Color { get { return Meta != null && Meta.IsCustomColor ? Meta.Color : null; } }
            public override MetaGroup Group { get { return Meta?.Group; } }
            public override MetaDescription Description { get { return Meta?.Description; } }
            public override IEnumerable<string> Tags { get { return Meta?.Tags; } }
            public MetaProxy(Type type) : base(type) { }
        }
    }
  6. What an Aspect is and how to use it

    main

    An EcsAspect is a user-defined class used to describe a group of components that a system needs to process. It serves two primary purposes:

    1. Masking: It initializes and stores an EcsMask used to filter entities during queries.
    2. Pool Caching: It provides fast access to component pools.

    Essentially, an Aspect defines "which entities I process and how I access their components."

    Constraint Modes

    When defining pools within an Aspect, you can specify how the component affects the entity filter:

    • Inc (Include): The entity must have this component.
    • Exc (Exclude): The entity must not have this component.
    • Opt (Optional): The component pool is cached for access, but its presence or absence does not affect the entity filter mask.

    Combining Aspects

    You can combine multiple aspects into one. If there are conflicting constraints (e.g., one aspect requires a component and another excludes it), the constraint in the "root" aspect (the one being defined) takes precedence and replaces the constraints from the combined aspects.

    using DCFApixels.DragonECS;
    
    class Aspect : EcsAspect
    {
        // Caches pool and adds Pose to the 'Include' constraint.
        public EcsPool<Pose> poses = Inc;
        // Caches pool and adds Velocity to the 'Include' constraint.
        public EcsPool<Velocity> velocities = Inc;
        // Caches pool and adds FreezedTag to the 'Exclude' constraint.
        public EcsTagPool<FreezedTag> freezedTags = Exc;
        // Caches pool but does not affect the mask.
        public EcsTagPool<IsDiedSignal> isDiedSignals = Opt;
    }
  7. Define Components and Tags

    main

    Components hold the data for entities. DragonECS distinguishes between regular components and tags for optimized storage:

    • Regular Components: Implement IEcsComponent. These are stored in standard storage.
    • Tags: Implement IEcsTagComponent. These are optimized for tag-based filtering and storage.
    struct Health : IEcsComponent
    {
        public float health;
        public int armor;
    }
    
    // Tag component
    struct PlayerTag : IEcsTagComponent {}
    {
        public float health;
        public int armor;
    }
    
    struct PlayerTag : IEcsTagComponent {}
  8. Use Dependency Injection in Systems

    main

    DragonECS supports dependency injection via the IEcsInject<T> interface. During pipeline initialization, the framework injects provided instances into systems.

    To support polymorphic injection (e.g., injecting a derived class into a system requesting a base class), use .Injector.AddNode<BaseType>() in the builder.

    Note: For simplified syntax, you can use the DragonECS-AutoInjections extension.

    class SomeDataB : SomeDataA { /* ... */ }
    
    SomeDataB _someDataB = new SomeDataB();
    EcsPipeline pipeline = EcsPipeline.New()
        .Inject(_someDataB) 
        // Allows systems requesting SomeDataA to receive _someDataB
        .Injector.AddNode<SomeDataA>()
        .Add(new SomeSystem())
        .BuildAndInit();
    
    // System receiving the injected data
    class SomeSystem : IEcsInject<SomeDataA>, IEcsRun
    {
        SomeDataA _someDataA;
        public void Inject(SomeDataA obj) => _someDataA = obj;
    
        public void Run () 
        {
            _someDataA.DoSomething();
        }
    }
  9. How EcsAspect works to manage components and filtering

    main

    An EcsAspect is a user-defined class (inheriting from EcsAspect) that describes the set of components a system works with. It serves two primary purposes:

    1. Mask: It initializes and stores an EcsMask used for query filtering.
    2. Pool Cache: It provides fast access to component pools.

    Essentially, an aspect defines "which entities I work with and how I access their components."

    Defining Component Requirements in an Aspect

    When defining properties in an aspect, use these static assignment keywords:

    • Inc: Component must be present (adds to inclusion mask) + caches the pool.
    • Exc: Component must be absent (adds to exclusion mask) + caches the pool.
    • Opt: Component may or may not be present (does not affect filtering) + caches the pool for access.
    • Any: At least one of the components specified with Any must be present + caches the pools.
    class Aspect : EcsAspect
    {
        // Caches pool and adds Pose to the inclusion condition.
        public EcsPool<Pose> poses = Inc;
        // Caches pool and adds Velocity to the inclusion condition.
        public EcsPool<Velocity> velocities = Inc;
        // Caches pool and adds FreezedTag to the exclusion condition.
        public EcsTagPool<FreezedTag> freezedTags = Exc;
    }
  10. Use Dependency Injection in DragonECS

    main

    DragonECS supports optional dependency injection during pipeline initialization.

    1. Register Data: Use .Inject(object) on the builder to inject a specific instance.
    2. Configure Injection Tree: Use .Injector.AddNode<T>() to allow systems requesting type T (or its derivatives) to receive the injected object.
    3. Implement Injection: Systems must implement the IEcsInject<T> interface and its Inject(T obj) method.

    Example: If you inject SomeDataB and add a node for SomeDataA, any system implementing IEcsInject<SomeDataA> will receive the SomeDataB instance.

    SomeDataB _someDataB = new SomeDataB();
    
    EcsPipeline pipeline = EcsPipeline.New()
        .Inject(_someDataB) 
        .Injector.AddNode<SomeDataA>() // Allows injection of subtypes of SomeDataA
        .Add(new SomeSystem())
        .BuildAndInit();
    
    // System implementation
    class SomeSystem : IEcsInject<SomeDataA>, IEcsRun
    {
        SomeDataA _someDataA;
        public void Inject(SomeDataA obj) => _someDataA = obj;
    
        public void Run () 
        {
            _someDataA.DoSomething();
        }
    }
  11. Manage system execution order with Layers and Sorting

    main

    To control the order of system execution independently of the order they were added, use Layers and Sorting Order.

    Layers

    Layers segment the system queue. You can define custom layers and position them relative to built-in layers using .Before() or .After() on the EcsPipeline.Builder.Layers object.

    Built-in Layers (in order):

    1. EcsConsts.PRE_BEGIN_LAYER
    2. EcsConsts.BEGIN_LAYER
    3. EcsConsts.BASIC_LAYER (Default layer if none specified)
    4. EcsConsts.END_LAYER
    5. EcsConsts.POST_END_LAYER

    Sorting Order

    Within a layer, systems are sorted by an integer sortOrder. The default is 0. Higher values are placed later in the layer.

    Example: Adding a system to BEGIN_LAYER with a sort order of 10 will place it after all systems in that layer with an order < 10.

    
    EcsPipeline pipeline = EcsPipeline.New()
        // Define layer position
        .Layers.Add(SOME_LAYER).Before(EcsConsts.END_LAYER)
        .Layers.Add(SOME_LAYER_2).After(EcsConsts.BASIC_LAYER)
        
        // Add system to specific layer and sort order
        .Add(new SomeSystem(), SOME_LAYER)
        .Add(new AnotherSystem(), EcsConsts.BEGIN_LAYER, 10)
        .BuildAndInit();
  12. Work with entity collections: EcsSpan and EcsGroup

    main

    DragonECS provides two main types of entity collections for iterating and managing entities:

    EcsSpan

    A read-only, stack-allocated collection of entities. It is similar to ReadOnlySpan<int> and is returned by standard Where queries. It supports foreach and for iteration. It does not allow duplicate entities.

    EcsGroup

    A collection based on a Sparse Set, optimized for $O(1)$ additions, removals, and checks. Groups are managed by a pool within EcsWorld.

    Key Operations for EcsGroup:

    • group.Add(entityID): Adds an entity.
    • group.Has(entityID): Checks if an entity is in the group.
    • group.Remove(entityID): Removes an entity.
    • group.Dispose(): Returns the group to the pool.

    Set Operations (implements ISet<int>): Groups support set algebra (Union, Intersect, Except, SymmetricExcept, and Inverse) both in-place (e.g., groupA.UnionWith(groupB)) and by returning a new group (e.g., EcsGroup.Union(groupA, groupB)).

    Querying into a Group: Use _world.WhereToGroup(out TAspect aspect) to return an EcsReadonlyGroup.

    // EcsSpan usage
    EcsSpan es = _world.Where(out Aspect a);
    foreach (var e in es) { /* ... */ }
    
    // EcsGroup usage
    EcsGroup group = EcsGroup.New(_world);
    group.Add(entityID);
    if (group.Has(entityID)) { /* ... */ }
    group.Dispose();
    
    // Set operations
    groupA.UnionWith(groupB);
    EcsGroup newGroup = EcsGroup.Intersect(groupA, groupB);