Last active
May 31, 2020 09:07
-
-
Save f-space/f17529620fd772117b85c1b7208226ad to your computer and use it in GitHub Desktop.
A minimal ECS implementation with Unity-like API for a blog entry.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
public readonly struct Entity | |
{ | |
public readonly int ID; | |
public Entity(int id) => this.ID = id; | |
} | |
public interface IComponentData { } | |
public abstract partial class SystemBase | |
{ | |
public void Update() => OnUpdate(); | |
protected abstract void OnUpdate(); | |
} | |
public sealed partial class EntityManager | |
{ | |
private const int InitialCapacity = 8; | |
private int nextID; | |
private bool[] entities = new bool[InitialCapacity]; | |
public int EntityCapacity => this.entities.Length; | |
public Entity CreateEntity() | |
{ | |
var entity = new Entity(this.nextID++); | |
this.entities = EnsureCapacity(this.entities, entity.ID); | |
this.entities[entity.ID] = true; | |
return entity; | |
} | |
public void DestroyEntity(Entity entity) | |
{ | |
this.entities[entity.ID] = false; | |
} | |
public bool Exists(Entity entity) => this.entities[entity.ID]; | |
private static T[] EnsureCapacity<T>(T[] array, int index) | |
{ | |
if (index < array.Length) return array; | |
var capacity = array.Length * 2; | |
while (index >= capacity) capacity *= 2; | |
var newArray = new T[capacity]; | |
Array.Copy(array, newArray, array.Length); | |
return newArray; | |
} | |
} | |
public sealed partial class EntityManager | |
{ | |
internal class ComponentArray<T> | |
{ | |
public bool[] Used; | |
public T[] Data; | |
public ComponentArray(int capacity) | |
{ | |
this.Used = new bool[capacity]; | |
this.Data = new T[capacity]; | |
} | |
} | |
private readonly Dictionary<Type, object> components = new Dictionary<Type, object>(); | |
public void AddComponent<T>(Entity entity) where T : unmanaged, IComponentData | |
{ | |
VerifyEntityExists(entity); | |
var array = GetComponentArray<T>(); | |
if (EntityHasComponent(array, entity)) | |
{ | |
throw new InvalidOperationException("component already exists"); | |
} | |
array.Used = EnsureCapacity(array.Used, entity.ID); | |
array.Data = EnsureCapacity(array.Data, entity.ID); | |
array.Used[entity.ID] = true; | |
array.Data[entity.ID] = default; | |
} | |
public void RemoveComponent<T>(Entity entity) where T : unmanaged, IComponentData | |
{ | |
VerifyEntityExists(entity); | |
var array = GetComponentArray<T>(); | |
if (!EntityHasComponent(array, entity)) | |
{ | |
throw new InvalidOperationException("component not exist"); | |
} | |
array.Used[entity.ID] = false; | |
} | |
public bool HasComponent<T>(Entity entity) where T : unmanaged, IComponentData | |
{ | |
VerifyEntityExists(entity); | |
return EntityHasComponent(GetComponentArray<T>(), entity); | |
} | |
private void VerifyEntityExists(Entity entity) | |
{ | |
if (!Exists(entity)) | |
{ | |
throw new ArgumentException("entity already destroyed", nameof(entity)); | |
} | |
} | |
internal ComponentArray<T> GetComponentArray<T>() | |
{ | |
if (this.components.TryGetValue(typeof(T), out var result)) return (ComponentArray<T>)result; | |
var array = new ComponentArray<T>(this.EntityCapacity); | |
this.components.Add(typeof(T), array); | |
return array; | |
} | |
internal static bool EntityHasComponent<T>(ComponentArray<T> array, Entity entity) | |
=> entity.ID < array.Used.Length && array.Used[entity.ID]; | |
} | |
public sealed partial class EntityManager | |
{ | |
public T GetComponentData<T>(Entity entity) where T : unmanaged, IComponentData | |
{ | |
VerifyEntityExists(entity); | |
var array = GetComponentArray<T>(); | |
if (!EntityHasComponent(array, entity)) | |
{ | |
throw new InvalidOperationException("component not exist"); | |
} | |
return array.Data[entity.ID]; | |
} | |
public void SetComponentData<T>(Entity entity, T value) where T : unmanaged, IComponentData | |
{ | |
VerifyEntityExists(entity); | |
var array = GetComponentArray<T>(); | |
if (!EntityHasComponent(array, entity)) | |
{ | |
throw new InvalidOperationException("component not exist"); | |
} | |
array.Data[entity.ID] = value; | |
} | |
} | |
public sealed class World | |
{ | |
private readonly List<SystemBase> systems = new List<SystemBase>(); | |
public EntityManager EntityManager { get; } = new EntityManager(); | |
public T CreateSystem<T>() where T : SystemBase, new() | |
{ | |
var system = new T() { World = this }; | |
this.systems.Add(system); | |
return system; | |
} | |
public void Update() | |
{ | |
foreach (var system in systems) system.Update(); | |
} | |
} | |
public abstract partial class SystemBase | |
{ | |
public World World { get; internal set; } | |
public EntityManager EntityManager => this.World.EntityManager; | |
} | |
public readonly struct ForEachLambdaJobDescription | |
{ | |
private readonly SystemBase system; | |
private EntityManager EntityManager => this.system.EntityManager; | |
internal ForEachLambdaJobDescription(SystemBase system) => this.system = system; | |
public delegate void VR<T0>(Entity entity, ref T0 component0); | |
public ForEachLambdaJobDescription ForEach<T0>(VR<T0> action) | |
where T0 : unmanaged, IComponentData | |
{ | |
var manager = this.EntityManager; | |
var capacity = manager.EntityCapacity; | |
var components0 = manager.GetComponentArray<T0>(); | |
for (var i = 0; i < capacity; i++) | |
{ | |
var entity = new Entity(i); | |
if (manager.Exists(entity)) | |
{ | |
if (!EntityManager.EntityHasComponent(components0, entity)) continue; | |
action( | |
entity, | |
ref components0.Data[entity.ID] | |
); | |
} | |
} | |
return this; | |
} | |
public delegate void VRR<T0, T1>(Entity entity, ref T0 component0, ref T1 component1); | |
public ForEachLambdaJobDescription ForEach<T0, T1>(VRR<T0, T1> action) | |
where T0 : unmanaged, IComponentData | |
where T1 : unmanaged, IComponentData | |
{ | |
var manager = this.EntityManager; | |
var capacity = manager.EntityCapacity; | |
var components0 = manager.GetComponentArray<T0>(); | |
var components1 = manager.GetComponentArray<T1>(); | |
for (var i = 0; i < capacity; i++) | |
{ | |
var entity = new Entity(i); | |
if (manager.Exists(entity)) | |
{ | |
if (!EntityManager.EntityHasComponent(components0, entity)) continue; | |
if (!EntityManager.EntityHasComponent(components1, entity)) continue; | |
action( | |
entity, | |
ref components0.Data[entity.ID], | |
ref components1.Data[entity.ID] | |
); | |
} | |
} | |
return this; | |
} | |
public void Run() { } | |
} | |
public abstract partial class SystemBase | |
{ | |
protected ForEachLambdaJobDescription Entities => new ForEachLambdaJobDescription(this); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment