Skip to content

Instantly share code, notes, and snippets.

@prime31
Last active November 16, 2023 10:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save prime31/99c66a4aeb4fc0e75173d5ea80f75a97 to your computer and use it in GitHub Desktop.
Save prime31/99c66a4aeb4fc0e75173d5ea80f75a97 to your computer and use it in GitHub Desktop.
C# Basic ECS implementation example. Not production ready, optimized or even fully functional. It is here to illustrate the patterns for creating an ECS from scratch.
using System;
namespace SimpleECS
{
struct Position
{
public float X, Y;
}
struct Velocity
{
public float X, Y;
}
struct Fart
{
public int Power;
}
class Program
{
static void Main(string[] args)
{
var registry = new Registry(100);
for (var i = 0; i < 20; i++)
{
var entity = registry.Create();
registry.AddComponent<Position>(entity, new Position { X = i * 10, Y = i * 10 });
registry.AddComponent<Velocity>(entity, new Velocity { X = 2, Y = 2 });
if (i % 5 == 0) registry.AddComponent<Fart>(entity, new Fart { Power = 666 });
}
RunPrinterSystem(registry);
RunVelocitySystem(registry);
RunPrinterSystem(registry);
RunVelocitySystem(registry);
}
static void RunVelocitySystem(Registry registry)
{
var view = registry.View<Velocity, Position>();
foreach (var entity in view)
{
ref Position pos = ref registry.GetComponent<Position>(entity);
ref Velocity vel = ref registry.GetComponent<Velocity>(entity);
pos.X += vel.X;
pos.Y += vel.Y;
}
}
static void RunPrinterSystem(Registry registry)
{
Console.WriteLine("----- Printer -----");
var view = registry.View<Velocity, Position, Fart>();
foreach (var entity in view)
{
var pos = registry.GetComponent<Position>(entity);
Console.WriteLine($"entity: {entity}, pos: {pos.X},{pos.Y}");
}
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using Entity = System.Int32;
public class Registry
{
readonly int maxEntities;
Dictionary<Type, IComponentStore> data = new Dictionary<Type, IComponentStore>();
Entity nextEntity = 0;
public Registry(int maxEntities) => this.maxEntities = maxEntities;
public ComponentStore<T> Assure<T>()
{
var type = typeof(T);
if (data.TryGetValue(type, out var store)) return (ComponentStore<T>)data[type];
var newStore = new ComponentStore<T>(maxEntities);
data[type] = newStore;
return newStore;
}
public Entity Create() => nextEntity++;
public void Destroy(Entity entity)
{
foreach (var store in data.Values)
store.RemoveIfContains(entity);
}
public void AddComponent<T>(Entity entity, T component) => Assure<T>().Add(entity, component);
public ref T GetComponent<T>(Entity entity) => ref Assure<T>().Get(entity);
public bool TryGetComponent<T>(Entity entity, ref T component)
{
var store = Assure<T>();
if (store.Contains(entity))
{
component = store.Get(entity);
return true;
}
return false;
}
public void RemoveComponent<T>(Entity entity) => Assure<T>().RemoveIfContains(entity);
public View<T> View<T>() => new View<T>(this);
public View<T, U> View<T, U>() => new View<T, U>(this);
public View<T, U, V> View<T, U, V>() => new View<T, U, V>(this);
}
public struct View<T> : IEnumerable<Entity>
{
Registry registry;
public View(Registry registry) => this.registry = registry;
public IEnumerator<Entity> GetEnumerator() => registry.Assure<T>().Set.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public struct View<T, U> : IEnumerable<Entity>
{
Registry registry;
public View(Registry registry) => this.registry = registry;
public IEnumerator<Entity> GetEnumerator()
{
var store2 = registry.Assure<U>();
foreach (var entity in registry.Assure<T>().Set)
{
if (!store2.Contains(entity)) continue;
yield return entity;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public struct View<T, U, V> : IEnumerable<Entity>
{
Registry registry;
public View(Registry registry) => this.registry = registry;
public IEnumerator<Entity> GetEnumerator()
{
var store2 = registry.Assure<U>();
var store3 = registry.Assure<V>();
foreach (var entity in registry.Assure<T>().Set)
{
if (!store2.Contains(entity) || !store3.Contains(entity)) continue;
yield return entity;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class SparseSet : IEnumerable<int>
{
readonly int max;
int size;
int[] dense;
int[] sparse;
public int Count => size;
public SparseSet(int maxValue)
{
max = maxValue + 1;
size = 0;
dense = new int[max];
sparse = new int[max];
}
public void Add(int value)
{
if (value >= 0 && value < max && !Contains(value))
{
dense[size] = value;
sparse[value] = size;
size++;
}
}
public void Remove(int value)
{
if (Contains(value))
{
dense[sparse[value]] = dense[size - 1];
sparse[dense[size - 1]] = sparse[value];
size--;
}
}
public int Index(int value) => sparse[value];
public bool Contains(int value)
{
if (value >= max || value < 0)
return false;
else
return sparse[value] < size && dense[sparse[value]] == value;
}
public void Clear() => size = 0;
public IEnumerator<int> GetEnumerator()
{
var i = 0;
while (i < size)
{
yield return dense[i];
i++;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override bool Equals(object obj) => throw new Exception("Why are you comparing SparseSets?");
public override int GetHashCode() => System.HashCode.Combine(max, size, dense, sparse, Count);
}
public interface IComponentStore
{
void RemoveIfContains(int entityId);
}
public class ComponentStore<T> : IComponentStore
{
public SparseSet Set;
T[] instances;
public int Count => Set.Count;
public ComponentStore(int maxComponents)
{
Set = new SparseSet(maxComponents);
instances = new T[maxComponents];
}
public void Add(int entityId, T value)
{
Set.Add(entityId);
instances[Set.Index(entityId)] = value;
}
public ref T Get(int entityId) => ref instances[Set.Index(entityId)];
public bool Contains(int entityId) => Set.Contains(entityId);
public void RemoveIfContains(int entityId)
{
if (Contains(entityId)) Remove(entityId);
}
void Remove(int entityId) => Set.Remove(entityId);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment