Skip to content
/ Apex Public

Step-by-step ECS experiments in C#, exploring different component storage strategies, system designs, and performance trade-offs

License

Notifications You must be signed in to change notification settings

unrays/Apex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Apex

A C# ECS framework for experimentation and performance testing

C# .NET License

Apex is a small ECS framework used to explore different design approaches, benchmark performance, and document trade-offs. It shows step-by-step iterations, memory access patterns, and system designs to understand how each choice affects efficiency and maintainability.

This is my first real project focused on performance optimization. Over five days (actually 7) of intense work, coding roughly ten hours per day, I developed around eleven different versions using various techniques. Not all of them worked, and some proved to be far less efficient than others, but I learned a lot in the process. This framework lays the foundation for my upcoming game engine project. It allowed me to deepen my understanding of ECS, a topic I am passionate about. I don’t fully understand every single detail yet (which would be impossible at my current level), but I am confident I can explain at least 90% of the code, even though I wrote 95% of it myself. I worked extremely hard on this, and I truly feel that it paid off.

Iteration 1 • Iteration 2 • Iteration 3 • Iteration 4 • Iteration 5 • Iteration 6 • Iteration 7 • Iteration 8 • Iteration 9 • Iteration 10 • Iteration 11

Copyright 2025 Félix-Olivier Dumas


First iteration – (not) Super performant and simple

Fast, lean, and easy to understand.

Setup 100000 entities with 3 components each: 29 ms
Accessed and modified Name components: 21 ms
Accessed and modified Position and Velocity components: 26 ms
CountComponents lookup for 1 entity: 1963 ticks
Random entity Name component: Test50000
// Copyright (c) October 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

class Entity {
    private static UInt32 nextId;
    private readonly UInt32 id;

    public Entity() => this.id = nextId++;
    public UInt32 getId() => id;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string? name { get; set; }
}

class EntityManager<E, C> where E : Entity where C : Component {
    private readonly Dictionary<E, List<C>> registry = new Dictionary<E, List<C>>();

    public EntityManager() { }

    public void AddComponent<CC>(E e) where CC : C, new() {
        if (!registry.TryGetValue(e, value: out var components)) {
            components = new List<C>(); registry[e] = components;
        }
        components.Add(new CC()); // aucune verif si un component du meme type est deja présent
    }

    public CC? getComponent<CC>(E e) where CC : C, new() {
        if (registry.TryGetValue(e, value: out var components)) {
            var corresponding = components.OfType<CC>().FirstOrDefault();
            return corresponding;
        }
        return null;
    }

    public UInt32 countComponents(E e) => (UInt32)registry[e].Count();

    public Boolean hasComponents(E e) => registry[e].Count() is not 0;

    public List<string> getComponentNames(E e) {
        var names = new List<string>();
        registry[e].ForEach(obj => names.Add(obj.GetType().Name));
        return names;
    }
}
class Position : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Velocity : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager<Entity, Component>();
        UInt32 entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>((int)entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.AddComponent<Name>(e);
            entityManager.AddComponent<Position>(e);
            entityManager.AddComponent<Velocity>(e);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.getComponent<Name>(e);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.getComponent<Position>(e);
            var vel = entityManager.getComponent<Velocity>(e);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.countComponents(testEntity);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(int)(entityCount / 2)];
        var nameRandom = entityManager.getComponent<Name>(randomEntity);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Second iteration – Over 10x slower than first iteration

Conceptually elegant, great design, but not optimized for speed.

Setup 100000 entities with 3 components each: 1814 ms
Accessed and modified Name components: 17 ms
Accessed and modified Position and Velocity components: 29 ms
CountComponents lookup for 1 entity: 4663 ticks
Random entity Name component:
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data.Common;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;

using Entity = Entity<System.UInt32>;
public interface IEntity<T> where T : unmanaged { T Id { get; } }
public readonly struct Entity<T> : IEntity<T> where T : unmanaged, IComparable<T> {
    public T Id { get; }
    public Entity(T id) => Id = id;
    public static explicit operator Entity<T>(T id) => new Entity<T>(id);
    public static implicit operator T(Entity<T> e) => e.Id;
}

public interface IComponent<T> where T : unmanaged { T Id { get; set; } }

public class Component<T> : IComponent<T> where T : unmanaged, IComparable<T> {
    public T Id { get; set; }
    public Component() => Id = default;
    public Component(T id) => Id = id;
}

public class Name : Component<UInt32> {
    public string NameValue { get; set; } = "";
    public Name() : base() { }
    public Name(string name, UInt32 id) : base(id) => NameValue = name;
}

public class Position : Component<UInt32> {
    public int X { get; set; }
    public int Y { get; set; }
    public Position() : base() { }
    public Position(int x, int y, UInt32 id) : base(id) { X = x; Y = y; }
}

public class Velocity : Component<UInt32> {
    public int X { get; set; }
    public int Y { get; set; }
    public Velocity() : base() { }
    public Velocity(int x, int y, UInt32 id) : base(id) { X = x; Y = y; }
}


//class ComponentPool : ComponentPool<Component, UInt32> { }
class ComponentPool<C, T> where C : IComponent<T> where T : unmanaged, INumber<T> {
    private readonly Dictionary<Type, int> _typeIds = new Dictionary<Type, int>();
    private readonly Dictionary<Type, HashSet<T>> _componentsByTypeId = new Dictionary<Type, HashSet<T>>();
    private C[] _components = new C[32];

    public ComponentPool() { }

    private void InternalRegister(C cmp) => InternalRegister(cmp.Id, cmp);
    private void InternalRegister(T idx, C cmp) {
        int uidx = int.CreateTruncating(idx);
        if (uidx >= _components.Length)
            Array.Resize(ref _components, _components.Length * 2);
        _components[uidx] = cmp;

        if (!_typeIds.ContainsKey(cmp.GetType()))
            _typeIds[cmp.GetType()] = _typeIds.Count;
        if (!_componentsByTypeId.TryGetValue(cmp.GetType(), out var set))
            _componentsByTypeId[cmp.GetType()] = set = new HashSet<T>();
        set.Add(idx);
    }

    private C? InternalFetch(T idx) => _components[int.CreateTruncating(idx)] ?? default;

    public void AddAt(T idx, C cmp) => InternalRegister(idx, cmp);

    public T Add<CC>() where CC : C, new() {
        var c = new CC(); InternalRegister(c);
        return c.Id;
    }

    public CC? GetIfPresent<CC>(T id) where CC : C {
        if (_componentsByTypeId.TryGetValue(typeof(CC), out var ids) && ids.Contains(id)) {
            var comp = _components[int.CreateTruncating(id)];
            if (comp is CC typed) return typed;
        } return default;
    }

    public C? GetAt(T idx) => InternalFetch(idx);

    public T GetTypeId<CC>() where CC : C {
        Type type = typeof(CC);
        if (!_typeIds.TryGetValue(type, out var id)) {
            id = _typeIds.Count;
            _typeIds[type] = id;
        } return T.CreateTruncating(id);
    }

    public HashSet<T> GetTypeIds<CC>() where CC : C {
        Type type = typeof(CC);
        if (!_componentsByTypeId.TryGetValue(type, out var set)) {
            set = new HashSet<T>();
            _componentsByTypeId[type] = set;
        } return set;
    }
}

//class EntityManager : EntityManager<Entity, Component, UInt32> { }
class EntityManager32<E, C> : EntityManager<E, C, UInt32> // lol
where C : IComponent<UInt32>, new()
where E : IEntity<UInt32> { }
class EntityManager<E, C, T>
where C : IComponent<T>, new() where T : unmanaged, INumber<T> where E : IEntity<T> {
    private readonly Dictionary<T, HashSet<T>> _registry = new Dictionary<T, HashSet<T>>();
    private readonly Dictionary<T, HashSet<T>> _entityTypeIds = new Dictionary<T, HashSet<T>>();
    private readonly ComponentPool<C, T> _pool = new ComponentPool<C, T>();

    public EntityManager() { }

    public void AddComponent<C0>(E e) where C0 : C, new() {
        if (!_registry.TryGetValue(e.Id, out var components))
            _registry[e.Id] = components = new HashSet<T>();
        if (!_entityTypeIds.TryGetValue(e.Id, out var typeIds))
            _entityTypeIds[e.Id] = typeIds = new HashSet<T>();

        var typeId = _pool.GetTypeId<C0>();
        if (!_entityTypeIds[e.Id].Add(typeId)) {
            Trace.TraceWarning("Component of type has already been added.");
            return;
        }

        if (!_registry[e.Id].Add(_pool.Add<C0>())) {
            Trace.TraceWarning("Unable to add component.");
            return;
        }
    }

    public C0? getComponent<C0>(E e) where C0 : C, new() {
        if (!_entityTypeIds.TryGetValue(e.Id, out var typeIds))
            return default;

        var typeId = _pool.GetTypeId<C0>();
        if (!typeIds.Contains(typeId))
            return default;

        var compIds = _pool.GetTypeIds<C0>();
        foreach (var compId in compIds) {
            if (_registry[e.Id].Contains(compId)) {
                var comp = _pool.GetIfPresent<C0>(compId);
                if (comp != null) return comp;
            }
        } return default;
    }

    public T CountComponents(E e) => T.CreateChecked(_registry[e.Id].Count);

    public Boolean HasComponents(E e) => _registry[e.Id].Count() is not 0;

    public List<string> GetComponentNames(E e) {
        var names = new List<string>();
        if (_registry.TryGetValue(e.Id, out var components)) {
            foreach (var compId in components) {
                var comp = _pool.GetAt(compId);
                if (comp != null)
                    names.Add(comp.GetType().Name);
            }
        }
        Console.WriteLine(e);
        return names;
    }
}

class Program {
    static UInt32 entityCounter = 0;
    static void Main(string[] args) {
        // Tests generated by ChatGPT - OpenAI

        var entityManager = new EntityManager<Entity, Component<UInt32>, UInt32>();
        UInt32 entityCounter = 0;

        UInt32 entityCount = 100_000;
        int componentsPerEntity = 3;

        var sw = Stopwatch.StartNew();

        for (int i = 0; i < entityCount; i++) {
            var e = new Entity(entityCounter++);
            entityManager.AddComponent<Name>(e);
            entityManager.AddComponent<Position>(e);
            entityManager.AddComponent<Velocity>(e);
        }

        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity((UInt32)i);
            var name = entityManager.getComponent<Name>(e);
            if (name != null) name.NameValue = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity((UInt32)i);
            var pos = entityManager.getComponent<Position>(e);
            var vel = entityManager.getComponent<Velocity>(e);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = new Entity(0);
        var count = entityManager.CountComponents(testEntity);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = new Entity(entityCount / 2);
        var nameRandom = entityManager.getComponent<Name>(randomEntity);
        Console.WriteLine($"Random entity Name component: {nameRandom?.NameValue}");
    }
}

Third iteration – Significantly more performant (~10-15% faster than first iteration)

Lean, efficient, and battle-tested.

Setup 100000 entities with 3 components each: 29 ms
Accessed and modified Name components: 19 ms
Accessed and modified Position and Velocity components: 25 ms
CountComponents lookup for 1 entity: 1616 ticks
Random entity Name component: Test50000
// Copyright (c) October 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Schema;

class Entity {
    private static int nextId;
    private readonly int id;

    public Entity() => this.id = nextId++;
    public int getId() => id;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string? name { get; set; }
}

class EntityManager {
    private readonly Dictionary<int, List<Component>> registry = new Dictionary<int, List<Component>>();

    public EntityManager() { }

    public void AddComponent<C>(int e) where C : Component, new() {
        if (!registry.TryGetValue(e, value: out var components)) {
            components = new List<Component>(); registry[e] = components;
        } components.Add(new C());
    }

    public C? getComponent<C>(int e) where C : Component, new() {
        if (registry.TryGetValue(e, value: out var components)) {
            var corresponding = components.OfType<C>().FirstOrDefault();
            return corresponding;
        } return default;
    }

    public int countComponents(int e) => registry[e].Count();

    public bool hasComponents(int e) => registry[e].Count() is not 0;

    public List<string> getComponentNames(int e) {
        var names = new List<string>();
        registry[e].ForEach(obj => names.Add(obj.GetType().Name));
        return names;
    }
}
class Position : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Velocity : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.AddComponent<Name>(e.getId());
            entityManager.AddComponent<Position>(e.getId());
            entityManager.AddComponent<Velocity>(e.getId());
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.getComponent<Name>(e.getId());
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.getComponent<Position>(e.getId());
            var vel = entityManager.getComponent<Velocity>(e.getId());
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.countComponents(testEntity.getId());
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.getComponent<Name>(randomEntity.getId());
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Fourth iteration – Performance similar to first iteration (~30% faster than first iteration, ~20% faster than second iteration)

Highly optimized for runtime, but setup overhead is a bit higher.

Setup 100000 entities with 3 components each: 35 ms
Accessed and modified Name components: 12 ms
Accessed and modified Position and Velocity components: 12 ms
CountComponents lookup for 1 entity: 1297 ticks
Random entity Name component: Test50000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml.Schema;
using static System.Runtime.InteropServices.JavaScript.JSType;

class Entity {
    private static int nextId;
    private readonly int id;

    public Entity() => this.id = nextId++;
    public int getId() => id;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string? name { get; set; }
}

class EntityManager {
    private readonly Dictionary<int, Memory<Component>> _reg = new Dictionary<int, Memory<Component>>();
    private Dictionary<int, int> _count = new Dictionary<int, int>();

    public EntityManager() { }

    public void AddComponent<C>(int eidx) where C : Component, new() {
        if (!_reg.TryGetValue(eidx, out var memc)) {
            Component[] initArr = new Component[10];
            memc = initArr;
            _reg[eidx] = memc;
            _count[eidx] = 0;
        }

        int len = memc.Span.Length;
        int count = _count[eidx];
        if (count == len) {
            Component[] newArray = new Component[len * 2];
            memc.Span.CopyTo(newArray);
            memc = newArray;
            _reg[eidx] = memc;
        }

        Span<Component> span = memc.Span;
        span[count] = new C();
        _count[eidx] = count + 1;
    }

    public C? getComponent<C>(int eidx) where C : Component, new() {
        Span<Component> span = _reg[eidx].Span;
        for (int i = 0; i < _count[eidx]; i++) {
            ref C c = ref Unsafe.As<Component, C>(ref span[i]);
            return c;
        }
        return null;
    }

    public int countComponents(int eidx) => _count[eidx];

    public bool hasComponents(int eidx) => _count[eidx] > 0;

    public List<string> getComponentNames(int eidx) {
        var names = new List<string>();
        if (_reg.TryGetValue(eidx, out var memc)) {
            Span<Component> span = memc.Span;
            for (int i = 0; i < span.Length; i++) {
                ref var c = ref span[i];
                names.Add(item: c.ToString());
            }
        }
        return names;
    }
}
class Position : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Velocity : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.AddComponent<Name>(e.getId());
            entityManager.AddComponent<Position>(e.getId());
            entityManager.AddComponent<Velocity>(e.getId());
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.getComponent<Name>(e.getId());
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.getComponent<Position>(e.getId());
            var vel = entityManager.getComponent<Velocity>(e.getId());
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.countComponents(testEntity.getId());
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.getComponent<Name>(randomEntity.getId());
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Fifth iteration – Conceptually clean, uses structs, but roughly 3× slower setup than first iteration due to dictionary and array overhead

Conceptually solid, fully struct-based, but runtime is heavier due to memory indirection. (That's what ChatGPT told me)

Setup 100000 entities with 3 components each: 94 ms
Accessed and modified Name components: 18 ms
Accessed and modified Position and Velocity components: 23 ms
CountComponents lookup for 1 entity: 1150 ticks
Random entity Name component: Test50000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public readonly struct Entity {
    private static int nextId;
    public readonly int id;

    public Entity() => id = nextId++;
}

public interface IComponent { }

public struct Position : IComponent {
    public float x, y;
}

public struct Velocity : IComponent {
    public float x, y;
}

public struct Name : IComponent {
    public string name;
}

class EntityManager {
    private readonly Dictionary<int, Memory<Type>> _entityCompTypes = new Dictionary<int, Memory<Type>>();
    private readonly Dictionary<int, Memory<int>> _registry = new Dictionary<int, Memory<int>>();
    private readonly Dictionary<Type, object> _dynamicStorage = new Dictionary<Type, object>();
    private readonly Dictionary<int, int> _entityCompCount = new Dictionary<int, int>();
    private readonly Dictionary<Type, int> _typeCount = new Dictionary<Type, int>();
    private readonly Dictionary<int, int> _count = new Dictionary<int, int>();

    public EntityManager() { }

    public void Add<T>(int eidx) where T : struct {
        Type t = typeof(T);
        if (!_dynamicStorage.ContainsKey(t)) {
            var arrType = t.MakeArrayType();
            object arrayInstance = Activator.CreateInstance(arrType, 10);
            _dynamicStorage[t] = arrayInstance;
        }

        if (!_registry.TryGetValue(eidx, out var compIds)) {
            int[] initArr = new int[10];
            compIds = initArr;
            _registry[eidx] = compIds;
            _count[eidx] = 0;
        }

        int len = compIds.Span.Length;
        int count = _count[eidx];
        if (count == len) {
            int[] newArray = new int[len * 2];
            compIds.Span.CopyTo(newArray);
            compIds = newArray;
            _registry[eidx] = compIds;
        }

        T[] typedArr = (T[])_dynamicStorage[t];
        if (!_typeCount.TryGetValue(t, out var tcount)) {
            tcount = 0;
            _typeCount[t] = tcount;
        }

        if (tcount == typedArr.Length) {
            T[] newArr = new T[typedArr.Length * 2];
            Array.Copy(typedArr, newArr, typedArr.Length);
            _dynamicStorage[t] = newArr;
            typedArr = newArr;
        }

        if (!_entityCompTypes.TryGetValue(eidx, out var typeMem)) {
            Type[] initArr = new Type[10];
            typeMem = initArr;
            _entityCompTypes[eidx] = typeMem;
        }

        if (!_entityCompCount.TryGetValue(eidx, out var typeCount)) {
            typeCount = 0;
            _entityCompCount[eidx] = typeCount;
        }

        Span<Type> typeSpan = typeMem.Span;

        if (typeCount >= typeSpan.Length) {
            Type[] newArr = new Type[typeSpan.Length * 2];
            typeSpan.CopyTo(newArr);
            _entityCompTypes[eidx] = newArr.AsMemory();
            typeSpan = _entityCompTypes[eidx].Span;
        }

        T element = default;
        typedArr[tcount] = element;

        _typeCount[t] = tcount + 1;
        _registry[eidx].Span[_count[eidx]] = _typeCount[t];

        typeSpan[typeCount] = t;
        _entityCompCount[eidx] = typeCount + 1;

        _count[eidx] = count + 1;
    }

    public ref T Get<T>(int eidx) where T : struct {
        if (!_entityCompTypes.TryGetValue(eidx, out var typesMem))
            return ref Unsafe.NullRef<T>();

        if (!_registry.TryGetValue(eidx, out var memIdx))
            return ref Unsafe.NullRef<T>();

        int count = _entityCompCount[eidx];
        Type t = typeof(T);

        for (int i = 0; i < count; i++) {
            if (typesMem.Span[i] == t) {
                int idxInDynamic = memIdx.Span[i];
                var arr = (T[])_dynamicStorage[t];
                return ref arr[idxInDynamic];
            }
        }
        return ref Unsafe.NullRef<T>();
    }

    public int countComponents(int eidx) => _count[eidx];

    public bool hasComponents(int eidx) => _count[eidx] > 0;

    public List<string> getComponentNames(int eidx) {
        var names = new List<string>();
        if (_registry.TryGetValue(eidx, out var memc)) {
            var span = memc.Span;
            for (int i = 0; i < span.Length; i++) {
                ref var c = ref span[i];
                names.Add(item: c.ToString());
            }
        } return names;
    }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.id);
            entityManager.Add<Position>(e.id);
            entityManager.Add<Velocity>(e.id);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];

            ref var name = ref entityManager.Get<Name>(e.id);
            if (!Unsafe.IsNullRef(ref name))
                name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            ref var pos = ref entityManager.Get<Position>(e.id);
            ref var vel = ref entityManager.Get<Velocity>(e.id);
            if (!Unsafe.IsNullRef(ref pos)) { pos.x = i; pos.y = i * 2; }
            if (!Unsafe.IsNullRef(ref vel)) { vel.x = i; vel.y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.countComponents(testEntity.id);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.id);
        Console.WriteLine($"Random entity Name component: {nameRandom.name}");
    }
}

Sixth iteration – Conceptually clean, uses raw T[] instead of Memory<T>; significantly faster setup and access, keeping struct-based type safety while cutting all wrapper overhead.

(~55% faster than first iteration, ~35% faster than fourth iteration)

Setup 100000 entities with 3 components each: 20 ms
Accessed and modified Name components: 8 ms
Accessed and modified Position and Velocity components: 6 ms
CountComponents lookup for 1 entity: 1277 ticks
Random entity Name component: Test50000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;

public readonly struct Entity {
    private static int nextId;
    public readonly int id;

    public Entity() => id = nextId++;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string? name { get; set; }
}

class EntityManager {
    private readonly Dictionary<int, Component[]> _reg = new Dictionary<int, Component[]>();
    private Dictionary<int, int> _count = new Dictionary<int, int>(); // faire int[][] ultimement pour les deux

    public EntityManager() { }

    public void Add<T>(int eidx) where T : Component, new() {
        if (!_reg.TryGetValue(eidx, out var memc)) {
            Component[] initArr = new Component[10];
            memc = initArr;
            _reg[eidx] = memc;
            _count[eidx] = 0;
        }

        Span<Component> span = memc.AsSpan();
        int len = span.Length;
        int count = _count[eidx];
        if (count == len) {
            Component[] newArray = new Component[len * 2];
            span.CopyTo(newArray);
            memc = newArray;
            _reg[eidx] = memc;
        }

        span[count] = new T();
        _count[eidx] = count + 1;
    }

    public T? Get<T>(int eidx) where T : Component, new() {
        Span<Component> span = _reg[eidx].AsSpan();
        for (int i = 0; i < _count[eidx]; i++) {
            if (span[i] is T c)
                return c;
        } return null;
    }

    public int CountComponents(int eidx) => _count[eidx];

    public bool HasComponents(int eidx) => _count[eidx] > 0;

    public List<string> GetComponentNames(int eidx) {
        var names = new List<string>();
        if (_reg.TryGetValue(eidx, out var memc)) {
            Span<Component> span = memc.AsSpan();
            for (int i = 0; i < span.Length; i++) {
                ref var c = ref span[i];
                names.Add(item: c.ToString());
            }
        }
        return names;
    }
}
class Position : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Velocity : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.id);
            entityManager.Add<Position>(e.id);
            entityManager.Add<Velocity>(e.id);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.Get<Name>(e.id);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.id);
            var vel = entityManager.Get<Velocity>(e.id);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.CountComponents(testEntity.id);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.id);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Seventh iteration – Flatter and faster.

Switched to plain arrays + indices. Setup and access are much quicker, even with lots of entities.

(~70% faster than first, ~40% faster than sixth)

Setup 100000 entities with 3 components each: 15 ms
Accessed and modified Name components: 4 ms
Accessed and modified Position and Velocity components: 4 ms
CountComponents lookup for 1 entity: 629 ticks
Random entity Name component: Test50000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;

public readonly struct Entity {
    private static int nextId;
    public readonly int id;

    public Entity() => id = nextId++;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string? name { get; set; }
}

class EntityManager {
    private int[][] _jreg = new int[10][];
    private Component[] _pool = new Component[10];

    private int[] _cCount = new int[10];  // correspond Ă  nb element _jreg[1]
    private int _eCount = 0; // correspond Ă  nb element _jreg[0]
    private int _pCount = 0; // correspond Ă  nb element de _pool 

    public EntityManager() { }

    public void Add<T>(int eidx) where T : Component, new() {
        while (eidx >= _jreg.Length) {
            Array.Resize(ref _jreg, _jreg.Length * 2);
            Array.Resize(ref _cCount, _cCount.Length * 2);
        }

        int[] cArr = _jreg[eidx];
        if (cArr == null)
            _jreg[eidx] = cArr = new int[10];

        if (_cCount[eidx] >= cArr.Length)
            Array.Resize(ref cArr, cArr.Length * 2);

        if (_pCount >= _pool.Length)
            Array.Resize(ref _pool, _pool.Length * 2);

        _pool[_pCount] = new T();
        cArr[_cCount[eidx]] = _pCount;

        _cCount[eidx]++;
        _pCount++;
    }

    public T? Get<T>(int eidx) where T : Component, new() {
        int[] cArr = _jreg[eidx];

        if (cArr != null) {
            Span<int> span = cArr.AsSpan();
            for (int i = 0; i < _cCount[eidx]; i++) {
                if (_pool[span[i]] is T c)
                    return c;
            } 
        } return null;
    }


    public int CountComponents(int eidx) => _cCount[eidx];

    public bool HasComponents(int eidx) => _cCount[eidx] > 0;

    public List<string> GetComponentNames(int eidx) {
        var names = new List<string>();

        int[] cArr = _jreg[eidx];
        if (cArr != null) {
            Span<int> span = cArr.AsSpan();
            for (int i = 0; i < _cCount[eidx]; i++) {
                names.Add(_pool[span[i]].ToString());
            }
        } return names;
    }
}
class Position : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Velocity : Component {
    public int X { get; set; }
    public int Y { get; set; }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.id);
            entityManager.Add<Position>(e.id);
            entityManager.Add<Velocity>(e.id);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.Get<Name>(e.id);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.id);
            var vel = entityManager.Get<Velocity>(e.id);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.CountComponents(testEntity.id);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.id);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Eighth iteration – Simply better :)

Fully optimized with direct indices and minimal overhead. Setup and component access are blazing fast, even at massive scale.

(~75-90% faster than first, ~2x faster in 3rd metric than seventh)

Setup 100000 entities with 3 components each: 12 ms
Accessed and modified Name components: 8 ms
Accessed and modified Position and Velocity components: 2 ms
CountComponents lookup for 1 entity: 991 ticks
Random entity Name component: Test50000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public readonly struct Entity {
    private static int nextId;
    public readonly int id;

    public Entity() => id = nextId++;
}

class Component {
    public Component() { }

    public void print() => Console.WriteLine(this.GetType().Name);
}

class Movement : Component {
    public float SpeedX { get; set; }
    public float SpeedY { get; set; }
    public (float X, float Y) Direction { get; set; } = (0, 0);

    public void SetDirection(float x, float y) {
        var length = MathF.Sqrt(x * x + y * y);
        Direction = length == 0 ? (0, 0) : (x / length, y / length);
    }
}

class Name : Component {
    public string name;
}

class EntityManager {
    private const int InitialTypeFlagsCapacity = 8;
    private const int InitialEntityCapacity = 100_000;
    private const int InitialPoolCapacity = 300_000;

    private int[][] _jreg = new int[InitialEntityCapacity][];
    private Component[] _pool = new Component[InitialPoolCapacity];
    private Type[] _typeFlags = new Type[InitialTypeFlagsCapacity];
    private int[] _typeIndex = new int[InitialEntityCapacity]; // map component id vers type id

    //possiblement faire genre un model qui stocke les types enrgistrés dans le system
    //et les mapper a un index, genre comme un dictionnaire de tous les types découverts
    //depuis le début de l'exécution et changer la pool ou alternative pour qu'il stocke
    //aussi un lien (index) vers le type de la collection générique. Par exemple, si on a
    //découvert 3 Position et 2 Name depuis le début, bien la collection aura
    //{ 1: Position, 2: Name } et les components pointerons vers l'index approprié
    //de cette collection. Petit tip, utilie typeof pour extraire le type de T
    //lors de l'ajout et ne fait que demander/where le type correspondant au type
    //T demandé dans le Get, afin d'avoir une recherche qui est essentiellement O(1)
    //contrairement à O(n) avec les boucles itératives sur des collections d'items.


    //faire un typePool pour les types

    private int[] _cCount = new int[InitialEntityCapacity];  // correspond Ă  nb element _jreg[1]
    private int _eCount = 0; // correspond Ă  nb element _jreg[0]
    private int _pCount = 0; // correspond Ă  nb element de _pool 
    private int _tfCount = 0;
    private int _tiCount = 0;

    public EntityManager() { }

    public void Add<T>(int eidx) where T : Component, new() {
        int tfCount = _tfCount;
        int tfLen = _typeFlags.Length;
        while (_tiCount >= _typeIndex.Length)
            Array.Resize(ref _typeIndex, _typeIndex.Length * 2);

        Type t = typeof(T);
        int foundIndex = -1;
        for (int i = 0; i < tfCount; i++)
            if (_typeFlags[i] == t)
                foundIndex = i;

        if (foundIndex == -1) {
            _typeFlags[tfCount] = t;
            foundIndex = tfCount;
            tfCount++;
            _tfCount = tfCount;
        }

        while (eidx >= _jreg.Length) {
            Array.Resize(ref _jreg, _jreg.Length * 2);
            Array.Resize(ref _cCount, _cCount.Length * 2);
        }

        int[] cArr = _jreg[eidx];
        if (cArr == null)
            _jreg[eidx] = cArr = new int[8];

        if (_cCount[eidx] >= cArr.Length) {
            Array.Resize(ref cArr, cArr.Length * 2);
            _jreg[eidx] = cArr;
        }

        if (_pCount >= _pool.Length)
            Array.Resize(ref _pool, _pool.Length * 2);

        _typeIndex[_pCount] = foundIndex;
        _tiCount++;

        _pool[_pCount] = new T();
        cArr[_cCount[eidx]] = _pCount;

        _cCount[eidx]++;
        _pCount++;
    }

    public T? Get<T>(int eidx) where T : Component, new() {
        int[] cArr = _jreg[eidx];

        Type t = typeof(T);
        if (cArr != null) {
            var span = cArr.AsSpan(0, _cCount[eidx]);
            for (int i = 0; i < _cCount[eidx]; i++) {
                ref int compID = ref cArr[i];
                int typeIndex = _typeIndex[compID];
                if (_typeFlags[typeIndex] == t)
                    return Unsafe.As<Component, T>(ref _pool[compID]);
            }
        } return null;
    }


    public int Count(int eidx) => _cCount[eidx];

    public bool HasComponents(int eidx) => _cCount[eidx] != 0;

    public List<string> GetComponentNames(int eidx) {
        var names = new List<string>();

        int[] cArr = _jreg[eidx];
        if (cArr != null) {
            Span<int> span = cArr.AsSpan();
            for (int i = 0; i < _cCount[eidx]; i++) {
                names.Add(_pool[span[i]].ToString());
            }
        } return names;
    }
}
class Position : Component {
    public int X;
    public int Y;
}

class Velocity : Component {
    public int X;
    public int Y;
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.id);
            entityManager.Add<Position>(e.id);
            entityManager.Add<Velocity>(e.id);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.Get<Name>(e.id);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.id);
            var vel = entityManager.Get<Velocity>(e.id);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.Count(testEntity.id);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.id);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Ninth iteration – Probably the last one.

Optimized release configuration with several JIT and compiler-level tweaks. Performance pushed even further with tighter inlining and instruction scheduling.

(Just take a look at the first one, no need to tell you how much more performant this one is over the first iteration)

Setup 100000 entities with 3 components each: 15 ms
Accessed and modified Name components: 2 ms
Accessed and modified Position and Velocity components: 0 ms (insane)
CountComponents lookup for 1 entity: 901 ticks
Random entity Name component: Test50000
Setup 10000000 entities with 3 components each: 2121 ms
Accessed and modified Name components: 1088 ms
Accessed and modified Position and Velocity components: 81 ms
CountComponents lookup for 1 entity: 2 ticks
Random entity Name component: Test5000000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Diagnostics;
using System.Runtime.CompilerServices;

public static class EntityId {
    private static int _nextId = 0;

    public static int Next() => _nextId++;
}

public readonly struct Entity {
    public readonly int Value;

    public Entity() => Value = EntityId.Next();
}

class Component {
    public Component() { }
}

class Name : Component {
    public string name;
}

class Position : Component {
    public int X;
    public int Y;
}

class Velocity : Component {
    public int X;
    public int Y;
}

class EntityManager {
    private const int InitialTypeFlagsCapacity = 8;
    private const int InitialEntityCapacity = 131_072;
    private const int InitialPoolCapacity = 524_288;

    private Component[] _pool = new Component[InitialPoolCapacity];
    private int[][] _jreg = new int[InitialEntityCapacity][];
    private Type[] _typeFlags = new Type[InitialTypeFlagsCapacity];
    private int[] _typeIndex = new int[InitialPoolCapacity];

    private int[] _cCount = new int[InitialEntityCapacity];
    private int _tfCount = 0;
    private int _tiCount = 0;
    private int _eCount = 0;
    private int _pCount = 0;


    public EntityManager() { }


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public void Add<T>(int eidx) where T : Component, new() {
        int tfCount = _tfCount;
        int tiCount = _tiCount;
        int eCount = _eCount;
        int pCount = _pCount;

        int tfLen = _typeFlags.Length;
        while (_tiCount >= _typeIndex.Length)
            Array.Resize(ref _typeIndex, _typeIndex.Length * 2);

        Type t = typeof(T);
        int foundIndex = -1;
        for (int i = 0; i < tfCount; i++)
            if (_typeFlags[i] == t)
                foundIndex = i;

        if (foundIndex == -1) {
            _typeFlags[tfCount] = t;
            foundIndex = tfCount;
            tfCount++;
        }

        while (eidx >= _jreg.Length) {
            Array.Resize(ref _jreg, _jreg.Length * 2);
            Array.Resize(ref _cCount, _cCount.Length * 2);
        }

        int[] cArr = _jreg[eidx];
        if (cArr == null)
            _jreg[eidx] = cArr = new int[8];

        if (_cCount[eidx] >= cArr.Length) {
            Array.Resize(ref cArr, cArr.Length * 2);
            _jreg[eidx] = cArr;
        }

        if (pCount >= _pool.Length)
            Array.Resize(ref _pool, _pool.Length * 2);

        _typeIndex[pCount] = foundIndex;
        _pool[pCount] = new T();
        cArr[_cCount[eidx]] = pCount;

        _tfCount = tfCount;
        _tiCount = tiCount + 1;
        _pCount = pCount + 1;
        _cCount[eidx]++;
    }


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public T? Get<T>(int eidx) where T : Component, new() {
        int[] cArr = _jreg[eidx];

        Type t = typeof(T);
        if (cArr != null) {
            var span = cArr.AsSpan(0, _cCount[eidx]);
            for (int i = 0; i < _cCount[eidx]; i++) {
                ref int compID = ref cArr[i];
                int typeIndex = _typeIndex[compID];
                if (_typeFlags[typeIndex] == t)
                    return Unsafe.As<Component, T>(ref _pool[compID]);
            }
        } return null;
    }


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public int Count(int eidx) => _cCount[eidx];


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public bool HasComponents(int eidx) => _cCount[eidx] != 0;


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public List<string> GetComponentNames(int eidx) {
        var names = new List<string>();

        int[] cArr = _jreg[eidx];
        if (cArr != null) {
            Span<int> span = cArr.AsSpan(0, _cCount[eidx]);
            for (int i = 0; i < _cCount[eidx]; i++) {
                names.Add(_pool[span[i]].ToString());
            }
        } return names;
    }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.Value);
            entityManager.Add<Position>(e.Value);
            entityManager.Add<Velocity>(e.Value);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.Get<Name>(e.Value);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.Value);
            var vel = entityManager.Get<Velocity>(e.Value);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.Count(testEntity.Value);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.Value);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Tenth iteration — I swear, this is the last one.

Mask-based mapping, 100% cache friendly, runtime lightning-fast.

(Short and efficient, nothing more)

Setup 100000 entities with 3 components each: 13 ms
Accessed and modified Name components: 2 ms
Accessed and modified Position and Velocity components: 0 ms
CountComponents lookup for 1 entity: 840 ticks
Random entity Name component: Test50000
Setup 10000000 entities with 3 components each: 768 ms
Accessed and modified Name components: 550 ms
Accessed and modified Position and Velocity components: 140 ms
CountComponents lookup for 1 entity: 1154 ticks
Random entity Name component: Test5000000
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public static class EntityId {
    private static int _nextId = 0;

    public static int Next() => _nextId++;
}

public readonly struct Entity {
    public readonly int Value;

    public Entity() => Value = EntityId.Next();
}

[Flags]
public enum ComponentMask : int { // autant de memoire qu'un int, perf++ (4 bit)
    None = 0,
    Position = 1 << 0,
    Velocity = 1 << 1,
    Name = 1 << 2
}

public static class ComponentMaskMapper {
    public static ComponentMask FromTypeOf(Type type) => type switch {
        Type t when t == typeof(Name) => ComponentMask.Name,
        Type t when t == typeof(Position) => ComponentMask.Position,
        Type t when t == typeof(Velocity) => ComponentMask.Velocity,
        _ => throw new Exception("Unknown type")
    };
}

// en gros, un entité a une liste de component.
// chaque component a une valeur bitmask ComponentTypes
// on peux combiner les types en une seule valeur, optimisé.

// userPerms |= Permissions.Read | Permissions.Write; (ajouter)
// if ((userPerms & Permissions.Write) != 0) (tester s'il peut)
// userPerms &= ~Permissions.Read; (retirer)
// userPerms ^= Permissions.Execute; (inverser)
// bool canReadAndWrite = (userPerms & (Permissions.Read | Permissions.Write)) == (Permissions.Read | Permissions.Write);

//faire bitmask des types du système
//ulong mask0; // types 0-63
//ulong mask1; // types 64-127
//ulong mask2; // types 128-191

// NOTE: STOCKER PLUSIEURS TYPES PAR ENTITÉ, JE VIENS DE CATCHER, C'EST CA QU'IL FAUT FAIRE

class Component {
    public Component() { }
}

class Position : Component {
    public int X;
    public int Y;
}

class Velocity : Component {
    public int X;
    public int Y;
}

class Name : Component {
    public string name;
}

class EntityManager {
    private const int InitialEntityCapacity = 131_072;
    private const int InitialPoolCapacity = 524_288;

    private Component[] _pool = new Component[InitialPoolCapacity];
    private int[][] _jreg = new int[InitialEntityCapacity][];
    private ComponentMask[] _cmask = new ComponentMask[InitialEntityCapacity];
    private ComponentMask[] _ctype = new ComponentMask[InitialPoolCapacity];

    private int[] _cCount = new int[InitialEntityCapacity];
    private int _ctCount = 0;
    private int _cmCount = 0;
    private int _eCount = 0;
    private int _pCount = 0;


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public void Add<T>(int eidx) where T : Component, new() {
        int pCount = _pCount;

        ComponentMask mask = _cmask[eidx];
        ComponentMask mapped = ComponentMaskMapper.FromTypeOf(typeof(T));
        if ((mask & mapped) != 0) {
            Console.Error.WriteLine("Component already present.");
            return;
        }

        int cmLen = _cmask.Length;
        while (_cmCount >= _cmask.Length)
            Array.Resize(ref _cmask, _cmask.Length * 2);

        while (_ctCount >= _ctype.Length)
            Array.Resize(ref _ctype, _ctype.Length * 2);

        while (eidx >= _jreg.Length) {
            Array.Resize(ref _jreg, _jreg.Length * 2);
            Array.Resize(ref _cCount, _cCount.Length * 2);
        }

        int[] cArr = _jreg[eidx];
        if (cArr == null)
            _jreg[eidx] = cArr = new int[8];

        if (_cCount[eidx] >= cArr.Length) {
            Array.Resize(ref cArr, cArr.Length * 2);
            _jreg[eidx] = cArr;
        }

        if (pCount >= _pool.Length)
            Array.Resize(ref _pool, _pool.Length * 2);

        mask |= mapped;

        _cmask[eidx] = mask;
        _pool[pCount] = new T();
        _ctype[pCount] = mapped;
        cArr[_cCount[eidx]] = pCount;

        _cmCount++;
        _ctCount++;
        _pCount++;
        _cCount[eidx]++;
    }


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public T? Get<T>(int eidx) where T : Component, new() {
        int[] cArr = _jreg[eidx];
        if (cArr == null)
            return null;

        ComponentMask mask = ComponentMaskMapper.FromTypeOf(typeof(T));
        if ((_cmask[eidx] & mask) == 0)
            return null;

        var span = cArr.AsSpan(0, _cCount[eidx]);
        for (int i = 0; i < _cCount[eidx]; i++) {
            ref int compID = ref cArr[i];
            if (mask == _ctype[compID])
                return Unsafe.As<Component, T>(ref _pool[compID]);
        } return null;
    }


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public int Count(int eidx) => _cCount[eidx];


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public bool HasComponents(int eidx) => _cCount[eidx] != 0;


    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public List<string> GetComponentNames(int eidx) {
        var names = new List<string>();

        int[] cArr = _jreg[eidx];
        if (cArr != null) {
            Span<int> span = cArr.AsSpan(0, _cCount[eidx]);
            for (int i = 0; i < _cCount[eidx]; i++) {
                names.Add(_pool[span[i]].ToString());
            }
        } return names;
    }
}

class Program {
    static void Main(string[] args) {
        var entityManager = new EntityManager();
        int entityCount = 100_000;
        int componentsPerEntity = 3;

        var entities = new List<Entity>(entityCount);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < entityCount; i++) {
            var e = new Entity();
            entities.Add(e);
            entityManager.Add<Name>(e.Value);
            entityManager.Add<Position>(e.Value);
            entityManager.Add<Velocity>(e.Value);
        }
        sw.Stop();
        Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var name = entityManager.Get<Name>(e.Value);
            if (name != null) name.name = "Test" + i;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Name components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.Value);
            var vel = entityManager.Get<Velocity>(e.Value);
            if (pos != null) { pos.X = i; pos.Y = i * 2; }
            if (vel != null) { vel.X = i; vel.Y = i * 2; }
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        var testEntity = entities[0];
        var count = entityManager.Count(testEntity.Value);
        sw.Stop();
        Console.WriteLine($"CountComponents lookup for 1 entity: {sw.ElapsedTicks} ticks");

        var randomEntity = entities[(entityCount / 2)];
        var nameRandom = entityManager.Get<Name>(randomEntity.Value);
        Console.WriteLine($"Random entity Name component: {nameRandom?.name}");
    }
}

Eleventh iteration — I'm back.

Here it is, the final, streamlined iteration. Just take a look, no need for explanations :)

Setup 10000 entities with 3 components each: 7 ms
Accessed and modified Position and Velocity components: 0,090 ms

Setup 100000 entities with 3 components each: 8 ms
Accessed and modified Position and Velocity components: 0,203 ms

Setup 1000000 entities with 3 components each: 19 ms
Accessed and modified Position and Velocity components: 2,086 ms

Setup 10000000 entities with 3 components each: 108 ms
Accessed and modified Position and Velocity components: 20,323 ms

Setup 100000000 entities with 3 components each: 958 ms
Accessed and modified Position and Velocity components: 211,961 ms
// Copyright (c) November 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

using System.Diagnostics;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public static class EntityId {
    private static int _nextId = 0;

    public static int Next() => _nextId++;
}

public readonly struct Entity {
    public readonly int Value;

    public Entity() => Value = EntityId.Next();
}

[Flags]
public enum ComponentMask : short {
    None = 0,
    Position = 1 << 0,
    Velocity = 1 << 1,
    Rotation = 1 << 2,
    Scale    = 1 << 3,
    Color    = 1 << 4,

    Name     = 1 << 5
}

class Component {
    public Component() { }
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct Name {
    public fixed char name[32];
}


[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Position {
    public int X;
    public int Y;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Velocity {
    public int X;
    public int Y;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Rotation {
    public int Angle;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Scale {
    public int X;
    public int Y;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Color {
    public int R;
    public int G;
    public int B;
    public int A;
}

class Registry {
    private const int InitialEntityCapacity = 131_072;

    private const int InitialPositionPool = 200_000;
    private const int InitialVelocityPool = 200_000;
    private const int InitialRotationPool = 200_000;
    private const int InitialScalePool = 200_000;
    private const int InitialColorPool = 200_000;
    private const int InitialNamePool = 200_000;

    private ComponentMask[] _cmask = new ComponentMask[InitialEntityCapacity];
    private ComponentMask[] _ctype = new ComponentMask[InitialPositionPool];
    private int _ctCount = 0; private int _cmCount = 0;

    private Position[] _positions = new Position[InitialPositionPool];
    private int[] _entityToPosIndex = new int[InitialEntityCapacity];
    private int _positionCount = 0;

    private Velocity[] _velocities = new Velocity[InitialVelocityPool];
    private int[] _entityToVelIndex = new int[InitialEntityCapacity];
    private int _velocityCount = 0;

    private Rotation[] _rotations = new Rotation[InitialRotationPool];
    private int[] _entityToRotIndex = new int[InitialEntityCapacity];
    private int _rotationCount = 0;

    private Scale[] _scales = new Scale[InitialScalePool];
    private int[] _entityToScaleIndex = new int[InitialEntityCapacity];
    private int _scaleCount = 0;

    private Color[] _colors = new Color[InitialColorPool];
    private int[] _entityToColorIndex = new int[InitialEntityCapacity];
    private int _colorCount = 0;

    private Name[] _names = new Name[InitialNamePool];
    private int[] _entityToNameIndex = new int[InitialEntityCapacity];
    private int _nameCount = 0;


    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static ComponentMask MaskOf<T>() where T : unmanaged {
        if (typeof(T) == typeof(Position)) return ComponentMask.Position;
        if (typeof(T) == typeof(Velocity)) return ComponentMask.Velocity;
        if (typeof(T) == typeof(Rotation)) return ComponentMask.Rotation;
        if (typeof(T) == typeof(Scale)) return ComponentMask.Scale;
        if (typeof(T) == typeof(Color)) return ComponentMask.Color;
        if (typeof(T) == typeof(Name)) return ComponentMask.Name;
        throw new Exception("Component type not supported");
    }


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public void Add<T>(int eidx) where T : unmanaged {
        ComponentMask mask = _cmask[eidx];
        ComponentMask mapped = MaskOf<T>();
        if ((mask & mapped) != 0) {
            Console.Error.WriteLine("Component already present.");
            return;
        }

        int cmLen = _cmask.Length;
        while (_cmCount >= _cmask.Length)
            Array.Resize(ref _cmask, _cmask.Length * 2);
        while (_ctCount >= _ctype.Length)
            Array.Resize(ref _ctype, _ctype.Length * 2);

        switch (mapped) {
            case ComponentMask.Position:
                if (eidx >= _entityToPosIndex.Length)
                    Array.Resize(ref _entityToPosIndex, Math.Max(_entityToPosIndex.Length * 2, eidx + 1));
                while (_positionCount >= _positions.Length)
                    Array.Resize(ref _positions, _positions.Length * 2);

                _entityToPosIndex[eidx] = _positionCount;
                _positions[_positionCount] = default;
                _positionCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            case ComponentMask.Velocity:
                if (eidx >= _entityToVelIndex.Length)
                    Array.Resize(ref _entityToVelIndex, Math.Max(_entityToVelIndex.Length * 2, eidx + 1));
                if (_velocityCount >= _velocities.Length)
                    Array.Resize(ref _velocities, _velocities.Length * 2);

                _entityToVelIndex[eidx] = _velocityCount;
                _velocities[_velocityCount] = default;
                _velocityCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            case ComponentMask.Rotation:
                while (eidx >= _entityToRotIndex.Length)
                    Array.Resize(ref _entityToRotIndex, Math.Max(_entityToRotIndex.Length * 2, eidx + 1));
                while (_rotationCount >= _rotations.Length)
                    Array.Resize(ref _rotations, _rotations.Length * 2);

                _entityToRotIndex[eidx] = _rotationCount;
                _rotations[_rotationCount] = default;
                _rotationCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            case ComponentMask.Scale:
                if (eidx >= _entityToScaleIndex.Length)
                    Array.Resize(ref _entityToScaleIndex, Math.Max(_entityToScaleIndex.Length * 2, eidx + 1));
                while (_scaleCount >= _scales.Length)
                    Array.Resize(ref _scales, _scales.Length * 2);

                _entityToScaleIndex[eidx] = _scaleCount;
                _scales[_scaleCount] = default;
                _scaleCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            case ComponentMask.Color:
                if (eidx >= _entityToColorIndex.Length)
                    Array.Resize(ref _entityToColorIndex, Math.Max(_entityToColorIndex.Length * 2, eidx + 1));
                while (_colorCount >= _colors.Length)
                    Array.Resize(ref _colors, _colors.Length * 2);

                _entityToColorIndex[eidx] = _colorCount;
                _colors[_colorCount] = default;
                _colorCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            case ComponentMask.Name:
                if (eidx >= _entityToNameIndex.Length)
                    Array.Resize(ref _entityToNameIndex, Math.Max(_entityToNameIndex.Length * 2, eidx + 1));
                while (_nameCount >= _names.Length)
                    Array.Resize(ref _names, _names.Length * 2);

                _entityToNameIndex[eidx] = _nameCount;
                _names[_nameCount] = default;
                _nameCount++;

                mask |= mapped;

                _cmask[eidx] = mask;
                _cmCount++;
                _ctCount++;
                return;
            default:
                throw new Exception("Component type not supported.");
        }
    }


    [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
    public ref T Get<T>(int eidx) where T : unmanaged {
        ComponentMask mask = MaskOf<T>();
        if ((_cmask[eidx] & mask) == 0)
            throw new Exception("Component not present");

        if (typeof(T) == typeof(Position))
            return ref Unsafe.As<Position, T>(ref _positions[_entityToPosIndex[eidx]]);
        else if (typeof(T) == typeof(Velocity))
            return ref Unsafe.As<Velocity, T>(ref _velocities[_entityToVelIndex[eidx]]);
        else if (typeof(T) == typeof(Rotation))
            return ref Unsafe.As<Rotation, T>(ref _rotations[_entityToRotIndex[eidx]]);
        else if (typeof(T) == typeof(Scale))
            return ref Unsafe.As<Scale, T>(ref _scales[_entityToScaleIndex[eidx]]);
        else if (typeof(T) == typeof(Color))
            return ref Unsafe.As<Color, T>(ref _colors[_entityToColorIndex[eidx]]);
        else if (typeof(T) == typeof(Name))
            return ref Unsafe.As<Name, T>(ref _names[_entityToNameIndex[eidx]]);
        else
            throw new Exception("Component type not supported");
    }
}

class Program {
        static void Main(string[] args) {
            var entityManager = new Registry();
            int entityCount = 100_000;
            int componentsPerEntity = 3;

            var entities = new List<Entity>(entityCount);

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < entityCount; i++) {
                var e = new Entity();
                entities.Add(e);
                entityManager.Add<Position>(e.Value);
                entityManager.Add<Velocity>(e.Value);
                
            }
            sw.Stop();
            Console.WriteLine($"Setup {entityCount} entities with {componentsPerEntity} components each: {sw.ElapsedMilliseconds} ms");

        sw.Restart();
        for (int i = 0; i < entityCount; i++) {
            var e = entities[i];
            var pos = entityManager.Get<Position>(e.Value);
            var vel = entityManager.Get<Velocity>(e.Value);
            pos.X = i; pos.Y = i * 2;
            vel.X = i; vel.Y = i * 2;
        }
        sw.Stop();
        Console.WriteLine($"Accessed and modified Position and Velocity components: {sw.Elapsed.TotalMilliseconds:F3} ms");
    }
}

Thank you so much for reading my work to the end, I wish you a very nice day and I look forward to meeting you one day!

About

Step-by-step ECS experiments in C#, exploring different component storage strategies, system designs, and performance trade-offs

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages