World Transfer + Entity Tags (#4)

- removed Snapshot system
- Entities can now be transferred between Worlds along with their Components and Relations using a Filter
- Entities can now be given a string tag on creation
- Manipulators can update an Entity's string tag
set_order
cosmonaut 2023-07-10 22:36:34 +00:00
parent f7d4fcdee7
commit 4d45d05618
7 changed files with 138 additions and 132 deletions

View File

@ -103,7 +103,17 @@ namespace MoonTools.ECS
public void CreateMissingStorages(ComponentDepot other)
{
for (var i = 0; i < ComponentTypeIndices.Count; i += 1)
while (other.ComponentTypeIndices.Count >= storages.Length)
{
Array.Resize(ref storages, storages.Length * 2);
}
while (other.ComponentTypeIndices.Count >= other.storages.Length)
{
Array.Resize(ref other.storages, other.storages.Length * 2);
}
for (var i = 0; i < other.ComponentTypeIndices.Count; i += 1)
{
if (storages[i] == null && other.storages[i] != null)
{

View File

@ -20,6 +20,11 @@ namespace MoonTools.ECS
World = world;
}
protected string GetTag(in Entity entity)
{
return World.GetTag(entity);
}
protected ReadOnlySpan<TComponent> ReadComponents<TComponent>() where TComponent : unmanaged
{
return ComponentDepot.ReadComponents<TComponent>();

View File

@ -15,17 +15,24 @@ namespace MoonTools.ECS
public int Count => nextID - availableIDs.Count;
public Entity Create()
public Dictionary<int, string> Tags = new Dictionary<int, string>();
public Entity Create(string tag)
{
var entity = new Entity(NextID());
if (!EntityToComponentTypeIndices.ContainsKey(entity.ID))
{
EntityToComponentTypeIndices.Add(entity.ID, new HashSet<int>());
}
if (!EntityToRelationTypeIndices.ContainsKey(entity.ID))
{
EntityToRelationTypeIndices.Add(entity.ID, new HashSet<int>());
}
Tags[entity.ID] = tag;
return entity;
}
@ -34,10 +41,16 @@ namespace MoonTools.ECS
return Taken(entity.ID);
}
public void Tag(in Entity entity, string tag)
{
Tags[entity.ID] = tag;
}
public void Destroy(in Entity entity)
{
EntityToComponentTypeIndices[entity.ID].Clear();
EntityToRelationTypeIndices[entity.ID].Clear();
Tags.Remove(entity.ID);
Release(entity.ID);
}
@ -68,6 +81,11 @@ namespace MoonTools.ECS
EntityToRelationTypeIndices[entityId].Remove(relationIndex);
}
public string Tag(int entityID)
{
return Tags[entityID];
}
public HashSet<int> ComponentTypeIndices(int entityID)
{
return EntityToComponentTypeIndices[entityID];

View File

@ -6,7 +6,8 @@ namespace MoonTools.ECS
{
}
protected Entity CreateEntity() => World.CreateEntity();
protected Entity CreateEntity(string tag = "") => World.CreateEntity(tag);
protected void Tag(Entity entity, string tag) => World.Tag(entity, tag);
protected void Set<TComponent>(in Entity entity, in TComponent component) where TComponent : unmanaged => World.Set<TComponent>(entity, component);
protected void Remove<TComponent>(in Entity entity) where TComponent : unmanaged => World.Remove<TComponent>(entity);
protected void Relate<TRelationKind>(in Entity entityA, in Entity entityB, TRelationKind relationData) where TRelationKind : unmanaged => World.Relate(entityA, entityB, relationData);

View File

@ -156,7 +156,17 @@ namespace MoonTools.ECS
public void CreateMissingStorages(RelationDepot other)
{
for (var i = 0; i < RelationTypeIndices.Count; i += 1)
while (other.RelationTypeIndices.Count >= storages.Length)
{
Array.Resize(ref storages, storages.Length * 2);
}
while (other.RelationTypeIndices.Count >= other.storages.Length)
{
Array.Resize(ref other.storages, other.storages.Length * 2);
}
for (var i = 0; i < other.RelationTypeIndices.Count; i += 1)
{
if (storages[i] == null && other.storages[i] != null)
{

View File

@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
namespace MoonTools.ECS
{
public class Snapshot
{
private World World;
private Filter? Filter;
private EntityStorage SnapshotEntityStorage;
private ComponentDepot SnapshotComponentDepot;
private RelationDepot SnapshotRelationDepot;
private List<int> SnapshotToWorldID = new List<int>();
private Dictionary<int, int> WorldToSnapshotID = new Dictionary<int, int>();
internal Snapshot(World world)
{
World = world;
SnapshotEntityStorage = new EntityStorage();
SnapshotComponentDepot = new ComponentDepot(World.ComponentTypeIndices);
SnapshotRelationDepot = new RelationDepot(World.RelationTypeIndices);
}
public unsafe void Take(Filter filter)
{
Clear();
Filter = filter;
SnapshotComponentDepot.CreateMissingStorages(World.ComponentDepot);
SnapshotRelationDepot.CreateMissingStorages(World.RelationDepot);
foreach (var worldEntity in filter.Entities)
{
var snapshotEntity = SnapshotEntityStorage.Create();
WorldToSnapshotID.Add(worldEntity.ID, snapshotEntity.ID);
foreach (var componentTypeIndex in World.EntityStorage.ComponentTypeIndices(worldEntity.ID))
{
SnapshotEntityStorage.SetComponent(snapshotEntity.ID, componentTypeIndex);
SnapshotComponentDepot.Set(snapshotEntity.ID, componentTypeIndex, World.ComponentDepot.UntypedGet(worldEntity.ID, componentTypeIndex));
}
}
foreach (var worldEntity in filter.Entities)
{
var snapshotEntityID = WorldToSnapshotID[worldEntity.ID];
foreach (var relationTypeIndex in World.EntityStorage.RelationTypeIndices(worldEntity.ID))
{
SnapshotEntityStorage.AddRelationKind(snapshotEntityID, relationTypeIndex);
foreach (var otherEntityID in World.RelationDepot.OutRelations(worldEntity.ID, relationTypeIndex))
{
#if DEBUG
if (!World.FilterStorage.CheckSatisfied(otherEntityID, Filter.Signature))
{
throw new InvalidOperationException($"Snapshot entity {worldEntity.ID} is related to non-snapshot entity {otherEntityID}!");
}
#endif
var relationStorageIndex = World.RelationDepot.GetStorageIndex(relationTypeIndex, worldEntity, otherEntityID);
var otherSnapshotID = WorldToSnapshotID[otherEntityID];
SnapshotEntityStorage.AddRelationKind(otherSnapshotID, relationTypeIndex);
SnapshotRelationDepot.Set(snapshotEntityID, otherSnapshotID, relationTypeIndex, World.RelationDepot.Get(relationTypeIndex, relationStorageIndex));
}
}
}
}
public unsafe void Restore()
{
if (Filter == null)
{
return;
}
foreach (var entity in Filter.Entities)
{
World.Destroy(entity);
}
for (var i = 0; i < SnapshotEntityStorage.Count; i += 1)
{
var entity = World.CreateEntity();
SnapshotToWorldID.Add(entity.ID);
foreach (var componentTypeIndex in SnapshotEntityStorage.ComponentTypeIndices(i))
{
World.EntityStorage.SetComponent(entity.ID, componentTypeIndex);
World.FilterStorage.Check(entity.ID, componentTypeIndex);
World.ComponentDepot.Set(entity.ID, componentTypeIndex, SnapshotComponentDepot.UntypedGet(i, componentTypeIndex));
}
}
for (var i = 0; i < SnapshotEntityStorage.Count; i += 1)
{
var worldID = SnapshotToWorldID[i];
foreach (var relationTypeIndex in SnapshotEntityStorage.RelationTypeIndices(i))
{
World.EntityStorage.AddRelationKind(worldID, relationTypeIndex);
foreach (var otherEntityID in SnapshotRelationDepot.OutRelations(i, relationTypeIndex))
{
var relationStorageIndex = SnapshotRelationDepot.GetStorageIndex(relationTypeIndex, i, otherEntityID);
var otherEntityWorldID = SnapshotToWorldID[otherEntityID];
World.RelationDepot.Set(worldID, otherEntityWorldID, relationTypeIndex, SnapshotRelationDepot.Get(relationTypeIndex, relationStorageIndex));
}
}
}
}
private void Clear()
{
SnapshotEntityStorage.Clear();
SnapshotComponentDepot.Clear();
SnapshotRelationDepot.Clear();
SnapshotToWorldID.Clear();
WorldToSnapshotID.Clear();
}
}
}

View File

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
namespace MoonTools.ECS
{
public class World
{
internal readonly TypeIndices ComponentTypeIndices = new TypeIndices();
internal readonly TypeIndices RelationTypeIndices = new TypeIndices();
internal readonly static TypeIndices ComponentTypeIndices = new TypeIndices();
internal readonly static TypeIndices RelationTypeIndices = new TypeIndices();
internal readonly EntityStorage EntityStorage = new EntityStorage();
internal readonly ComponentDepot ComponentDepot;
internal readonly MessageDepot MessageDepot = new MessageDepot();
@ -23,9 +24,19 @@ namespace MoonTools.ECS
TemplateComponentDepot = new ComponentDepot(ComponentTypeIndices);
}
public Entity CreateEntity()
public Entity CreateEntity(string tag = "")
{
return EntityStorage.Create();
return EntityStorage.Create(tag);
}
public void Tag(Entity entity, string tag)
{
EntityStorage.Tag(entity, tag);
}
public string GetTag(Entity entity)
{
return EntityStorage.Tag(entity);
}
public void Set<TComponent>(Entity entity, in TComponent component) where TComponent : unmanaged
@ -45,6 +56,17 @@ namespace MoonTools.ECS
}
}
// untyped version for Transfer
internal unsafe void Set(Entity entity, int componentTypeIndex, void* component)
{
ComponentDepot.Set(entity.ID, componentTypeIndex, component);
if (EntityStorage.SetComponent(entity.ID, componentTypeIndex))
{
FilterStorage.Check(entity.ID, componentTypeIndex);
}
}
public void Remove<TComponent>(in Entity entity) where TComponent : unmanaged
{
if (EntityStorage.RemoveComponent(entity.ID, ComponentTypeIndices.GetIndex<TComponent>()))
@ -63,6 +85,14 @@ namespace MoonTools.ECS
EntityStorage.AddRelationKind(entityB.ID, relationTypeIndex);
}
// untyped version for Transfer
internal unsafe void Relate(Entity entityA, Entity entityB, int relationTypeIndex, void* relationData)
{
RelationDepot.Set(entityA, entityB, relationTypeIndex, relationData);
EntityStorage.AddRelationKind(entityA.ID, relationTypeIndex);
EntityStorage.AddRelationKind(entityB.ID, relationTypeIndex);
}
public void Unrelate<TRelationKind>(in Entity entityA, in Entity entityB) where TRelationKind : unmanaged
{
var (aEmpty, bEmpty) = RelationDepot.Remove<TRelationKind>(entityA, entityB);
@ -118,9 +148,63 @@ namespace MoonTools.ECS
MessageDepot.Clear();
}
public Snapshot CreateSnapshot()
private Dictionary<int, int> WorldToTransferID = new Dictionary<int, int>();
// FIXME: there's probably a better way to handle Filters so they are not world-bound
public unsafe void Transfer(World other, Filter filter, Filter otherFilter)
{
return new Snapshot(this);
WorldToTransferID.Clear();
other.ComponentDepot.CreateMissingStorages(ComponentDepot);
other.RelationDepot.CreateMissingStorages(RelationDepot);
// destroy all entities matching the filter
foreach (var entity in otherFilter.Entities)
{
other.Destroy(entity);
}
// create entities
foreach (var entity in filter.Entities)
{
var otherWorldEntity = other.CreateEntity(GetTag(entity));
WorldToTransferID.Add(entity.ID, otherWorldEntity.ID);
}
// set relations before components so filters don't freak out
foreach (var entity in filter.Entities)
{
var otherWorldEntityA = WorldToTransferID[entity.ID];
foreach (var relationTypeIndex in EntityStorage.RelationTypeIndices(entity.ID))
{
foreach (var entityB in RelationDepot.OutRelations(entity.ID, relationTypeIndex))
{
var storageIndex = RelationDepot.GetStorageIndex(relationTypeIndex, entity.ID, entityB);
int otherWorldEntityB;
if (WorldToTransferID.TryGetValue(entityB, out otherWorldEntityB))
{
other.Relate(otherWorldEntityA, otherWorldEntityB, relationTypeIndex, RelationDepot.Get(relationTypeIndex, storageIndex));
}
else
{
// related entity is not in the filter
throw new Exception($"Missing transfer entity! {EntityStorage.Tag(entity.ID)} related to {EntityStorage.Tag(entityB.ID)}");
}
}
}
}
// set components
foreach (var entity in filter.Entities)
{
var otherWorldEntity = WorldToTransferID[entity.ID];
foreach (var componentTypeIndex in EntityStorage.ComponentTypeIndices(entity.ID))
{
other.Set(otherWorldEntity, componentTypeIndex, ComponentDepot.UntypedGet(entity.ID, componentTypeIndex));
}
}
}
}
}