├── .gitignore ├── ecs.csproj ├── README.md ├── ecs ├── Entity.cs ├── Group.cs ├── ComponentStore.cs ├── SparseSet.cs ├── View.cs └── Registry.cs ├── Simple-ECS-Sharp.sln ├── .vscode ├── launch.json └── tasks.json └── Program.cs /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | -------------------------------------------------------------------------------- /ecs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple ECS 2 | A minimal ECS in C#. Sparse set based with EnTT-like (https://github.com/skypjack/entt) `View`s and `Group`s. Purposely simple so those new to ECS can read and understand all the code and extend it as needed. 3 | -------------------------------------------------------------------------------- /ecs/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleECS 2 | { 3 | public readonly struct Entity 4 | { 5 | public readonly uint Id; 6 | 7 | public Entity(uint id) => Id = id; 8 | 9 | public static implicit operator Entity(uint id) => new Entity(id); 10 | 11 | public override int GetHashCode() => Id.GetHashCode(); 12 | } 13 | } -------------------------------------------------------------------------------- /ecs/Group.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace SimpleECS 5 | { 6 | public struct Group : IEnumerable 7 | { 8 | Registry registry; 9 | Registry.GroupData groupData; 10 | 11 | internal Group(Registry registry, Registry.GroupData groupData) 12 | { 13 | this.registry = registry; 14 | this.groupData = groupData; 15 | } 16 | 17 | public IEnumerator GetEnumerator() => groupData.Entities.GetEnumerator(); 18 | 19 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 20 | } 21 | } -------------------------------------------------------------------------------- /Simple-ECS-Sharp.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.5.2.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ecs", "ecs.csproj", "{3FB33279-C01C-16F7-0458-62143BA33271}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {3FB33279-C01C-16F7-0458-62143BA33271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {3FB33279-C01C-16F7-0458-62143BA33271}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {3FB33279-C01C-16F7-0458-62143BA33271}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {3FB33279-C01C-16F7-0458-62143BA33271}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(SolutionProperties) = preSolution 19 | HideSolutionNode = FALSE 20 | EndGlobalSection 21 | GlobalSection(ExtensibilityGlobals) = postSolution 22 | SolutionGuid = {4434D3DB-87CF-497F-AF49-EFFA6BDDC0D2} 23 | EndGlobalSection 24 | EndGlobal 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net5.0/ecs.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/ecs.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/ecs.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/ecs.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /ecs/ComponentStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleECS 4 | { 5 | public interface IComponentStore 6 | { 7 | public bool Contains(uint entityId); 8 | 9 | void RemoveIfContains(Entity entity) => RemoveIfContains(entity.Id); 10 | 11 | void RemoveIfContains(uint entityId); 12 | 13 | public SparseSet Entities { get; } 14 | } 15 | 16 | public class ComponentStore : IComponentStore 17 | { 18 | public event Action OnAdd; 19 | public event Action OnRemove; 20 | 21 | public SparseSet Set; 22 | public SparseSet Entities => Set; 23 | T[] instances; 24 | 25 | public uint Count => Set.Count; 26 | 27 | public ComponentStore(uint maxComponents) 28 | { 29 | Set = new SparseSet(maxComponents); 30 | instances = new T[maxComponents]; 31 | } 32 | 33 | public void Add(Entity entity, T value) 34 | { 35 | Set.Add(entity.Id); 36 | instances[Set.Index(entity.Id)] = value; 37 | OnAdd?.Invoke(entity.Id); 38 | } 39 | 40 | public ref T Get(uint entityId) => ref instances[Set.Index(entityId)]; 41 | 42 | public bool Contains(uint entityId) => Set.Contains(entityId); 43 | 44 | public void RemoveIfContains(uint entityId) 45 | { 46 | if (Set.Contains(entityId)) Remove(entityId); 47 | } 48 | 49 | void Remove(uint entityId) 50 | { 51 | Set.Remove(entityId); 52 | OnRemove?.Invoke(entityId); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /ecs/SparseSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace SimpleECS 6 | { 7 | public class SparseSet : IEnumerable 8 | { 9 | struct Enumerator : IEnumerator 10 | { 11 | uint[] dense; 12 | uint size; 13 | uint current; 14 | uint next; 15 | 16 | public Enumerator(uint[] dense, uint size) 17 | { 18 | this.dense = dense; 19 | this.size = size; 20 | current = 0; 21 | next = 0; 22 | } 23 | 24 | public uint Current => current; 25 | 26 | object IEnumerator.Current => current; 27 | 28 | public void Dispose() 29 | { } 30 | 31 | public bool MoveNext() 32 | { 33 | if (next < size) 34 | { 35 | current = dense[next]; 36 | next++; 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public void Reset() => next = 0; 44 | } 45 | 46 | 47 | readonly uint max; 48 | uint size; 49 | uint[] dense; 50 | uint[] sparse; 51 | 52 | public uint Count => size; 53 | 54 | public SparseSet(uint maxValue) 55 | { 56 | max = maxValue + 1; 57 | size = 0; 58 | dense = new uint[max]; 59 | sparse = new uint[max]; 60 | } 61 | 62 | public void Add(uint value) 63 | { 64 | if (value >= 0 && value < max && !Contains(value)) 65 | { 66 | dense[size] = value; 67 | sparse[value] = size; 68 | size++; 69 | } 70 | } 71 | 72 | public void Remove(uint value) 73 | { 74 | if (Contains(value)) 75 | { 76 | dense[sparse[value]] = dense[size - 1]; 77 | sparse[dense[size - 1]] = sparse[value]; 78 | size--; 79 | } 80 | } 81 | 82 | public uint Index(uint value) => sparse[value]; 83 | 84 | public bool Contains(uint value) 85 | { 86 | if (value >= max || value < 0) 87 | return false; 88 | else 89 | return sparse[value] < size && dense[sparse[value]] == value; 90 | } 91 | 92 | public void Clear() => size = 0; 93 | 94 | public IEnumerator GetEnumerator() => new Enumerator(this.dense, size); 95 | 96 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 97 | 98 | public override bool Equals(object obj) => throw new Exception("Why are you comparing SparseSets?"); 99 | 100 | public override int GetHashCode() => System.HashCode.Combine(max, size, dense, sparse, Count); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleECS 4 | { 5 | struct Position 6 | { 7 | public float X, Y; 8 | } 9 | 10 | struct Velocity 11 | { 12 | public float X, Y; 13 | } 14 | 15 | struct Fart 16 | { 17 | public int Power; 18 | } 19 | 20 | struct SystemTimer : IDisposable 21 | { 22 | string name; 23 | System.Diagnostics.Stopwatch watch; 24 | 25 | public SystemTimer(string name) 26 | { 27 | this.name = name; 28 | watch = new System.Diagnostics.Stopwatch(); 29 | watch.Start(); 30 | } 31 | 32 | void IDisposable.Dispose() => Console.WriteLine($"{name}:\t\t{watch.ElapsedTicks}"); 33 | } 34 | 35 | class Program 36 | { 37 | static void Main(string[] args) 38 | { 39 | var registry = new Registry(100); 40 | 41 | for (var i = 0; i < 100; i++) 42 | { 43 | var entity = registry.Create(); 44 | registry.AddComponent(entity, new Position { X = i * 10, Y = i * 10 }); 45 | registry.AddComponent(entity, new Velocity { X = 2, Y = 2 }); 46 | 47 | if (i % 5 == 0) registry.AddComponent(entity, new Fart { Power = 666 }); 48 | } 49 | 50 | RunPrinterSystem(registry); 51 | RunVelocitySystem(registry); 52 | RunVelocityGroupSystem(registry); 53 | RunPrinterSystem(registry); 54 | 55 | using (new SystemTimer("RunVelocitySystem")) 56 | RunVelocitySystem(registry); 57 | 58 | using (new SystemTimer("RunVelocityGroupSystem")) 59 | RunVelocityGroupSystem(registry); 60 | } 61 | 62 | static void RunVelocitySystem(Registry registry) 63 | { 64 | var view = registry.View(); 65 | foreach (var entity in view) 66 | { 67 | ref Position pos = ref registry.GetComponent(entity); 68 | ref Velocity vel = ref registry.GetComponent(entity); 69 | pos.X += vel.X; 70 | pos.Y += vel.Y; 71 | } 72 | } 73 | 74 | static void RunVelocityGroupSystem(Registry registry) 75 | { 76 | var view = registry.Group(); 77 | foreach (var entity in view) 78 | { 79 | ref Position pos = ref registry.GetComponent(entity); 80 | ref Velocity vel = ref registry.GetComponent(entity); 81 | pos.X += vel.X; 82 | pos.Y += vel.Y; 83 | } 84 | } 85 | 86 | static void RunPrinterSystem(Registry registry) 87 | { 88 | Console.WriteLine("----- Printer -----"); 89 | var view = registry.View(); 90 | foreach (var entity in view) 91 | { 92 | var pos = registry.GetComponent(entity); 93 | Console.WriteLine($"entity: {entity}, pos: {pos.X},{pos.Y}"); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ecs/View.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace SimpleECS 6 | { 7 | public struct View : IEnumerable 8 | { 9 | Registry registry; 10 | 11 | public View(Registry registry) => this.registry = registry; 12 | 13 | public IEnumerator GetEnumerator() => registry.Assure().Set.GetEnumerator(); 14 | 15 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 16 | } 17 | 18 | public struct View : IEnumerable 19 | { 20 | struct Enumerator : IEnumerator 21 | { 22 | Registry registry; 23 | IComponentStore store; 24 | IEnumerator setEnumerator; 25 | 26 | public Enumerator(Registry registry) 27 | { 28 | this.registry = registry; 29 | var store1 = registry.Assure(); 30 | var store2 = registry.Assure(); 31 | 32 | if (store1.Count > store2.Count) 33 | { 34 | setEnumerator = store2.Entities.GetEnumerator(); 35 | store = store1; 36 | } 37 | else 38 | { 39 | setEnumerator = store1.Entities.GetEnumerator(); 40 | store = store2; 41 | } 42 | } 43 | 44 | public uint Current => setEnumerator.Current; 45 | 46 | object IEnumerator.Current => setEnumerator.Current; 47 | 48 | public void Dispose() 49 | {} 50 | 51 | public bool MoveNext() 52 | { 53 | while (setEnumerator.MoveNext()) 54 | { 55 | var entityId = setEnumerator.Current; 56 | if (!store.Contains(entityId)) continue; 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | public void Reset() => setEnumerator.Reset(); 63 | } 64 | 65 | Registry registry; 66 | 67 | public View(Registry registry) => this.registry = registry; 68 | 69 | public IEnumerator GetEnumerator() => new Enumerator(registry); 70 | 71 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 72 | } 73 | 74 | public struct View : IEnumerable 75 | { 76 | struct Enumerator : IEnumerator 77 | { 78 | static IComponentStore[] sorter = new IComponentStore[3]; 79 | Registry registry; 80 | IComponentStore store1; 81 | IComponentStore store2; 82 | IEnumerator setEnumerator; 83 | 84 | public Enumerator(Registry registry) 85 | { 86 | this.registry = registry; 87 | 88 | sorter[0] = registry.Assure(); 89 | sorter[1] = registry.Assure(); 90 | sorter[2] = registry.Assure(); 91 | Array.Sort(sorter, (first, second) => first.Entities.Count.CompareTo(second.Entities.Count)); 92 | 93 | setEnumerator = sorter[0].Entities.GetEnumerator(); 94 | store1 = sorter[1]; 95 | store2 = sorter[2]; 96 | } 97 | 98 | public uint Current => setEnumerator.Current; 99 | 100 | object IEnumerator.Current => setEnumerator.Current; 101 | 102 | public void Dispose() 103 | {} 104 | 105 | public bool MoveNext() 106 | { 107 | while (setEnumerator.MoveNext()) 108 | { 109 | var entityId = setEnumerator.Current; 110 | if (!store1.Contains(entityId) || !store2.Contains(entityId)) continue; 111 | return true; 112 | } 113 | return false; 114 | } 115 | 116 | public void Reset() => setEnumerator.Reset(); 117 | } 118 | 119 | 120 | Registry registry; 121 | 122 | public View(Registry registry) => this.registry = registry; 123 | 124 | public IEnumerator GetEnumerator() => new Enumerator(registry); 125 | 126 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /ecs/Registry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SimpleECS 5 | { 6 | public class Registry 7 | { 8 | internal class GroupData 9 | { 10 | public int HashCode; 11 | public SparseSet Entities; 12 | IComponentStore[] componentStores; 13 | 14 | public GroupData(Registry registry, int hashCode, params IComponentStore[] components) 15 | { 16 | HashCode = hashCode; 17 | Entities = new SparseSet(registry.maxEntities); 18 | componentStores = components; 19 | } 20 | 21 | internal void OnEntityAdded(uint entityId) 22 | { 23 | if (!Entities.Contains(entityId)) 24 | { 25 | foreach (var store in componentStores) 26 | if (!store.Contains(entityId)) return; 27 | Entities.Add(entityId); 28 | } 29 | } 30 | 31 | internal void OnEntityRemoved(uint entityId) 32 | { 33 | if (Entities.Contains(entityId)) Entities.Remove(entityId); 34 | } 35 | } 36 | 37 | readonly uint maxEntities; 38 | Dictionary data = new Dictionary(); 39 | uint nextEntity = 0; 40 | List Groups = new List(); 41 | 42 | public Registry(uint maxEntities) => this.maxEntities = maxEntities; 43 | 44 | public ComponentStore Assure() 45 | { 46 | var type = typeof(T); 47 | if (data.TryGetValue(type, out var store)) return (ComponentStore)data[type]; 48 | 49 | var newStore = new ComponentStore(maxEntities); 50 | data[type] = newStore; 51 | return newStore; 52 | } 53 | 54 | public Entity Create() => new Entity(nextEntity++); 55 | 56 | public void Destroy(Entity entity) 57 | { 58 | foreach (var store in data.Values) 59 | store.RemoveIfContains(entity.Id); 60 | } 61 | 62 | public void AddComponent(Entity entity, T component) => Assure().Add(entity, component); 63 | 64 | public ref T GetComponent(Entity entity) => ref Assure().Get(entity.Id); 65 | 66 | public bool TryGetComponent(Entity entity, ref T component) 67 | { 68 | var store = Assure(); 69 | if (store.Contains(entity.Id)) 70 | { 71 | component = store.Get(entity.Id); 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | 78 | public void RemoveComponent(Entity entity) => Assure().RemoveIfContains(entity.Id); 79 | 80 | public View View() => new View(this); 81 | 82 | public View View() => new View(this); 83 | 84 | public View View() => new View(this); 85 | 86 | public Group Group() 87 | { 88 | var hash = System.HashCode.Combine(typeof(T), typeof(U)); 89 | 90 | foreach (var group in Groups) 91 | if (group.HashCode == hash) return new Group(this, group); 92 | 93 | var groupData = new GroupData(this, hash, Assure(), Assure()); 94 | Groups.Add(groupData); 95 | 96 | Assure().OnAdd += groupData.OnEntityAdded; 97 | Assure().OnAdd += groupData.OnEntityAdded; 98 | 99 | Assure().OnRemove += groupData.OnEntityRemoved; 100 | Assure().OnRemove += groupData.OnEntityRemoved; 101 | 102 | foreach (var entityId in View()) groupData.Entities.Add(entityId); 103 | 104 | return new Group(this, groupData); 105 | } 106 | 107 | public Group Group() 108 | { 109 | var hash = System.HashCode.Combine(typeof(T), typeof(U), typeof(V)); 110 | 111 | foreach (var group in Groups) 112 | if (group.HashCode == hash) return new Group(this, group); 113 | 114 | var groupData = new GroupData(this, hash, Assure(), Assure(), Assure()); 115 | Groups.Add(groupData); 116 | 117 | Assure().OnAdd += groupData.OnEntityAdded; 118 | Assure().OnAdd += groupData.OnEntityAdded; 119 | Assure().OnAdd += groupData.OnEntityAdded; 120 | 121 | Assure().OnRemove += groupData.OnEntityRemoved; 122 | Assure().OnRemove += groupData.OnEntityRemoved; 123 | Assure().OnRemove += groupData.OnEntityRemoved; 124 | 125 | foreach (var entityId in View()) groupData.Entities.Add(entityId); 126 | 127 | return new Group(this, groupData); 128 | } 129 | } 130 | 131 | } --------------------------------------------------------------------------------