├── .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 | }
--------------------------------------------------------------------------------