From 0f4052728e97ea72658074076dd3a1aa46ad1145 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Fri, 4 Mar 2022 18:01:44 -0800 Subject: [PATCH] initial commit --- .editorconfig | 14 ++++++ .gitignore | 2 + MoonTools.ECS.csproj | 9 ++++ src/ComponentDepot.cs | 76 ++++++++++++++++++++++++++++++ src/ComponentStorage.cs | 89 ++++++++++++++++++++++++++++++++++++ src/Entity.cs | 11 +++++ src/EntityComponentReader.cs | 42 +++++++++++++++++ src/EntityStorage.cs | 16 +++++++ src/IDStorage.cs | 27 +++++++++++ src/Renderer.cs | 6 +++ src/System.cs | 27 +++++++++++ src/World.cs | 39 ++++++++++++++++ 12 files changed, 358 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 MoonTools.ECS.csproj create mode 100644 src/ComponentDepot.cs create mode 100644 src/ComponentStorage.cs create mode 100644 src/Entity.cs create mode 100644 src/EntityComponentReader.cs create mode 100644 src/EntityStorage.cs create mode 100644 src/IDStorage.cs create mode 100644 src/Renderer.cs create mode 100644 src/System.cs create mode 100644 src/World.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b7f51b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.cs] +csharp_space_after_cast = true +charset = utf-8-bom +max_line_length = 100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/MoonTools.ECS.csproj b/MoonTools.ECS.csproj new file mode 100644 index 0000000..bafd05b --- /dev/null +++ b/MoonTools.ECS.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/ComponentDepot.cs b/src/ComponentDepot.cs new file mode 100644 index 0000000..1e2aeea --- /dev/null +++ b/src/ComponentDepot.cs @@ -0,0 +1,76 @@ +namespace MoonTools.ECS; + +internal class ComponentDepot +{ + private Dictionary storages = new Dictionary(); + + private Dictionary> entityComponentMap = new Dictionary>(); + + private ComponentStorage Lookup() where TComponent : struct + { + // TODO: is it possible to optimize this? + if (!storages.ContainsKey(typeof(TComponent))) + { + storages.Add(typeof(TComponent), new ComponentStorage()); + } + + return storages[typeof(TComponent)] as ComponentStorage; + } + + public bool Some() where TComponent : struct + { + return Lookup().Any(); + } + + public bool Has(int entityID) where TComponent : struct + { + return Lookup().Has(entityID); + } + + public ref readonly TComponent Get(int entityID) where TComponent : struct + { + return ref Lookup().Get(entityID); + } + + public void Set(int entityID, in TComponent component) where TComponent : struct + { + Lookup().Set(entityID, component); + + if (!entityComponentMap.ContainsKey(entityID)) + { + entityComponentMap.Add(entityID, new HashSet()); + } + + entityComponentMap[entityID].Add(typeof(TComponent)); + } + + public ReadOnlySpan ReadEntities() where TComponent : struct + { + return Lookup().AllEntities(); + } + + public ReadOnlySpan ReadComponents() where TComponent : struct + { + return Lookup().AllComponents(); + } + + public void Remove(int entityID) where TComponent : struct + { + Lookup().Remove(entityID); + + entityComponentMap[entityID].Remove(typeof(TComponent)); + } + + public void OnEntityDestroy(int entityID) + { + if (entityComponentMap.ContainsKey(entityID)) + { + foreach (var type in entityComponentMap[entityID]) + { + storages[type].Remove(entityID); + } + + entityComponentMap.Remove(entityID); + } + } +} diff --git a/src/ComponentStorage.cs b/src/ComponentStorage.cs new file mode 100644 index 0000000..c2a736a --- /dev/null +++ b/src/ComponentStorage.cs @@ -0,0 +1,89 @@ +namespace MoonTools.ECS; + +internal abstract class ComponentStorage +{ + public abstract void Remove(int entityID); +} + +internal class ComponentStorage : ComponentStorage where TComponent : struct +{ + private int nextID; + private IDStorage idStorage = new IDStorage(); + private readonly Dictionary entityIDToStorageIndex = new Dictionary(); + private Entity[] storageIndexToEntities = new Entity[64]; + private TComponent[] components = new TComponent[64]; + + public bool Any() + { + return nextID > 0; + } + + public bool Has(int entityID) + { + return entityIDToStorageIndex.ContainsKey(entityID); + } + + public ref readonly TComponent Get(int entityID) + { + return ref components[entityIDToStorageIndex[entityID]]; + } + + public void Set(int entityID, in TComponent component) + { + if (!entityIDToStorageIndex.ContainsKey(entityID)) + { + var index = nextID; + nextID += 1; + + if (index >= components.Length) + { + Array.Resize(ref components, components.Length * 2); + Array.Resize(ref storageIndexToEntities, storageIndexToEntities.Length * 2); + } + + entityIDToStorageIndex[entityID] = index; + storageIndexToEntities[index] = new Entity(entityID); + } + + components[entityIDToStorageIndex[entityID]] = component; + } + + public override void Remove(int entityID) + { + if (entityIDToStorageIndex.ContainsKey(entityID)) + { + var storageIndex = entityIDToStorageIndex[entityID]; + entityIDToStorageIndex.Remove(entityID); + + var lastElementIndex = nextID - 1; + + // move a component into the hole to maintain contiguous memory + if (entityIDToStorageIndex.Count > 0 && storageIndex != lastElementIndex) + { + var lastEntity = storageIndexToEntities[lastElementIndex]; + + entityIDToStorageIndex[lastEntity.ID] = storageIndex; + storageIndexToEntities[storageIndex] = lastEntity; + components[storageIndex] = components[lastElementIndex]; + } + + nextID -= 1; + } + } + + public void Clear() + { + nextID = 0; + entityIDToStorageIndex.Clear(); + } + + public ReadOnlySpan AllEntities() + { + return new ReadOnlySpan(storageIndexToEntities, 0, nextID); + } + + public ReadOnlySpan AllComponents() + { + return new ReadOnlySpan(components, 0, nextID); + } +} diff --git a/src/Entity.cs b/src/Entity.cs new file mode 100644 index 0000000..4036efe --- /dev/null +++ b/src/Entity.cs @@ -0,0 +1,11 @@ +namespace MoonTools.ECS; + +public struct Entity +{ + public int ID { get; } + + internal Entity(int id) + { + ID = id; + } +} diff --git a/src/EntityComponentReader.cs b/src/EntityComponentReader.cs new file mode 100644 index 0000000..c87a411 --- /dev/null +++ b/src/EntityComponentReader.cs @@ -0,0 +1,42 @@ +namespace MoonTools.ECS; + +public abstract class EntityComponentReader +{ + internal EntityStorage EntityStorage; + internal ComponentDepot ComponentDepot; + + internal void RegisterEntityStorage(EntityStorage entityStorage) + { + EntityStorage = entityStorage; + } + + internal void RegisterComponentDepot(ComponentDepot componentDepot) + { + ComponentDepot = componentDepot; + } + + protected ReadOnlySpan ReadEntities() where TComponent : struct + { + return ComponentDepot.ReadEntities(); + } + + protected ReadOnlySpan ReadComponents() where TComponent : struct + { + return ComponentDepot.ReadComponents(); + } + + protected bool Has(in Entity entity) where TComponent : struct + { + return ComponentDepot.Has(entity.ID); + } + + protected bool Some() where TComponent : struct + { + return ComponentDepot.Some(); + } + + protected TComponent Get(in Entity entity) where TComponent : struct + { + return ComponentDepot.Get(entity.ID); + } +} diff --git a/src/EntityStorage.cs b/src/EntityStorage.cs new file mode 100644 index 0000000..ebbed67 --- /dev/null +++ b/src/EntityStorage.cs @@ -0,0 +1,16 @@ +namespace MoonTools.ECS; + +internal class EntityStorage +{ + public IDStorage idStorage = new IDStorage(); + + public Entity Create() + { + return new Entity(idStorage.NextID()); + } + + public void Destroy(in Entity entity) + { + idStorage.Release(entity.ID); + } +} diff --git a/src/IDStorage.cs b/src/IDStorage.cs new file mode 100644 index 0000000..011c274 --- /dev/null +++ b/src/IDStorage.cs @@ -0,0 +1,27 @@ +namespace MoonTools.ECS; + +internal class IDStorage +{ + private int nextID = 0; + + private readonly Stack availableIDs = new Stack(); + + public int NextID() + { + if (availableIDs.Count > 0) + { + return availableIDs.Pop(); + } + else + { + var id = nextID; + nextID += 1; + return id; + } + } + + public void Release(int id) + { + availableIDs.Push(id); + } +} diff --git a/src/Renderer.cs b/src/Renderer.cs new file mode 100644 index 0000000..c1559ac --- /dev/null +++ b/src/Renderer.cs @@ -0,0 +1,6 @@ +namespace MoonTools.ECS; + +public abstract class Renderer : EntityComponentReader +{ + public abstract void Draw(TimeSpan delta); +} diff --git a/src/System.cs b/src/System.cs new file mode 100644 index 0000000..d3d7273 --- /dev/null +++ b/src/System.cs @@ -0,0 +1,27 @@ +namespace MoonTools.ECS; + +public abstract class System : EntityComponentReader +{ + public abstract void Update(TimeSpan delta); + + protected Entity CreateEntity() + { + return EntityStorage.Create(); + } + + protected void Set(in Entity entity, in TComponent component) where TComponent : struct + { + ComponentDepot.Set(entity.ID, component); + } + + protected void Remove(in Entity entity) where TComponent : struct + { + ComponentDepot.Remove(entity.ID); + } + + protected void Destroy(in Entity entity) + { + ComponentDepot.OnEntityDestroy(entity.ID); + EntityStorage.Destroy(entity); + } +} diff --git a/src/World.cs b/src/World.cs new file mode 100644 index 0000000..b0f7b87 --- /dev/null +++ b/src/World.cs @@ -0,0 +1,39 @@ +namespace MoonTools.ECS; + +public class World +{ + private readonly List systems = new List(); + private readonly List renderers = new List(); + private EntityStorage EntityStorage { get; } = new EntityStorage(); + private ComponentDepot ComponentDepot { get; } = new ComponentDepot(); + + public void AddSystem(System system) + { + system.RegisterEntityStorage(EntityStorage); + system.RegisterComponentDepot(ComponentDepot); + systems.Add(system); + } + + public void AddRenderer(Renderer renderer) + { + renderer.RegisterEntityStorage(EntityStorage); + renderer.RegisterComponentDepot(ComponentDepot); + renderers.Add(renderer); + } + + public void Update(TimeSpan delta) + { + foreach (var system in systems) + { + system.Update(delta); + } + } + + public void Draw(TimeSpan delta) + { + foreach (var renderer in renderers) + { + renderer.Draw(delta); + } + } +}