diff --git a/src/Arch.Tests/CommandBufferTest.cs b/src/Arch.Tests/CommandBufferTest.cs index 94d7e455..b56d10de 100644 --- a/src/Arch.Tests/CommandBufferTest.cs +++ b/src/Arch.Tests/CommandBufferTest.cs @@ -57,14 +57,14 @@ public void CommandBuffer() var world = World.Create(); var commandBuffer = new CommandBuffer.CommandBuffer(world); - var entity = commandBuffer.Create(new ComponentType[] { typeof(Transform), typeof(Rotation), typeof(int) }); - commandBuffer.Set(in entity, new Transform { X = 20, Y = 20 }); - commandBuffer.Add(in entity, new Ai()); - commandBuffer.Remove(in entity); + var pentity = commandBuffer.Create(new ComponentType[] { typeof(Transform), typeof(Rotation), typeof(int) }); + commandBuffer.Set(in pentity, new Transform { X = 20, Y = 20 }); + commandBuffer.Add(in pentity, new Ai()); + commandBuffer.Remove(in pentity); commandBuffer.Playback(); - entity = new Entity(0, 0); + var entity = new Entity(0, 0); That(world.Get(entity).X, Is.EqualTo(20)); That(world.Get(entity).Y, Is.EqualTo(20)); IsTrue(world.Has(entity)); @@ -78,7 +78,7 @@ public void CommandBufferCreateMultipleEntities() { var world = World.Create(); - var entities = new List(); + var entities = new List(); using (var commandBuffer = new CommandBuffer.CommandBuffer(world)) { entities.Add(commandBuffer.Create(new ComponentType[] { typeof(Transform) })); @@ -177,8 +177,6 @@ public void CommandBufferModify() That(world.TryGet(entities[0], out _), Is.True); That(world.TryGet(entities[0], out _), Is.False); }); - - World.Destroy(world); } @@ -202,20 +200,43 @@ public void CommandBufferCombined() commandBuffer.Playback(); - bufferedEntity = new Entity(1, 0); + var realBufferedEntity = new Entity(1, 0); That(world.Get(entity).X, Is.EqualTo(20)); That(world.Get(entity).Y, Is.EqualTo(20)); IsTrue(world.Has(entity)); IsFalse(world.Has(entity)); - That(world.Get(bufferedEntity).X, Is.EqualTo(20)); - That(world.Get(bufferedEntity).Y, Is.EqualTo(20)); - IsTrue(world.Has(bufferedEntity)); - IsFalse(world.Has(bufferedEntity)); + That(world.Get(realBufferedEntity).X, Is.EqualTo(20)); + That(world.Get(realBufferedEntity).Y, Is.EqualTo(20)); + IsTrue(world.Has(realBufferedEntity)); + IsFalse(world.Has(realBufferedEntity)); World.Destroy(world); } + + [Test] + public void CommandBufferEntityErrors() + { + using var world = World.Create(); + using var buffer1 = new CommandBuffer.CommandBuffer(world); + using var buffer2 = new CommandBuffer.CommandBuffer(world); + + var e = buffer1.Create(Array.Empty()); + + // Use entity with the correct buffer + buffer1.Add(e, new Transform()); + + // Use the entity with the world - this doesn't even type check now! + //world.Get(e); + + // Use entity with the wrong buffer + Throws(() => buffer2.Add(e, new Transform())); + + // Playback buffer and then try to use the entity + buffer1.Playback(); + Throws(() => buffer1.Add(e, new Transform())); + } } [TestFixture] diff --git a/src/Arch/CommandBuffer/CommandBuffer.cs b/src/Arch/CommandBuffer/CommandBuffer.cs index b0e6d6f7..d578b881 100644 --- a/src/Arch/CommandBuffer/CommandBuffer.cs +++ b/src/Arch/CommandBuffer/CommandBuffer.cs @@ -57,6 +57,41 @@ public BufferedEntityInfo(int index, int setIndex, int addIndex, int removeIndex } } +/// +/// Represents an Entity that will be created when is called. +/// +public readonly record struct PendingEntity +{ + private int Id { get; } + private CommandBuffer CommandBuffer { get; } + private ushort Generation { get; } + + /// + /// An Entity that has been created in a CommandBuffer, but hasn't yet been added to the scene + /// + public PendingEntity(int id, CommandBuffer cmd) + { + Id = id; + CommandBuffer = cmd; + Generation = cmd.Generation; + } + + public Entity ToEntity(CommandBuffer cmd) + { + if (cmd != CommandBuffer) + { + throw new InvalidOperationException("Cannot use a `PendingEntity` created by one `CommandBuffer` with a different `CommandBuffer`"); + } + + if (cmd.Generation != Generation) + { + throw new InvalidOperationException("Cannot use a `PendingEntity` created from a previous generation"); + } + + return new Entity(Id, cmd.World.Id); + } +} + /// /// The class /// stores operation to 's between to play and implement them at a later time in the . @@ -84,6 +119,7 @@ public CommandBuffer(World world, int initialCapacity = 128) Destroys = new PooledList(initialCapacity); _addTypes = new PooledList(16); _removeTypes = new PooledList(16); + Generation = 0; } /// @@ -96,6 +132,11 @@ public CommandBuffer(World world, int initialCapacity = 128) /// public int Size { get; private set; } + /// + /// Incremented every time is called. Used to ensure that a from another generation is not used. + /// + internal ushort Generation { get; private set; } + /// /// All 's created or modified in this . /// @@ -151,6 +192,12 @@ internal void Register(in Entity entity, out BufferedEntityInfo info) Size++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Register(in PendingEntity entity, out BufferedEntityInfo info) + { + Register(entity.ToEntity(this), out info); + } + /// TODO : Probably just run this if the wrapped entity is negative? To save some overhead? /// /// Resolves an originally either from a or to its real . @@ -173,11 +220,11 @@ internal Entity Resolve(Entity entity) /// The 's component structure/. /// The buffered with an index of -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Entity Create(ComponentType[] types) + public PendingEntity Create(ComponentType[] types) { lock (this) { - var entity = new Entity(-(Size + 1), World.Id); + var entity = new PendingEntity(-(Size + 1), this); Register(entity, out _); var command = new CreateCommand(Size - 1, types); @@ -187,6 +234,16 @@ public Entity Create(ComponentType[] types) } } + /// + /// Record a Destroy operation for a . + /// Will be destroyed during . + /// + /// The to destroy. + public void Destroy(in PendingEntity entity) + { + Destroy(entity.ToEntity(this)); + } + /// /// Record a Destroy operation for an (buffered) . /// Will be destroyed during . @@ -206,6 +263,20 @@ public void Destroy(in Entity entity) } } + /// + /// Records a set operation for a . + /// Overwrites previous values. + /// Will be set during . + /// + /// The component type. + /// The . + /// The component value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(in PendingEntity entity, in T? component = default) + { + Set(entity.ToEntity(this), component); + } + /// /// Records a set operation for an (buffered) . /// Overwrites previous values. @@ -229,6 +300,19 @@ public void Set(in Entity entity, in T? component = default) Sets.Set(info.SetIndex, in component); } + /// + /// Records a add operation for a . + /// Overwrites previous values. + /// Will be added during . + /// + /// The component type. + /// The . + /// The component value. + public void Add(in PendingEntity entity, in T? component = default) + { + Add(entity.ToEntity(this), component); + } + /// /// Records a add operation for an (buffered) . /// Overwrites previous values. @@ -253,6 +337,17 @@ public void Add(in Entity entity, in T? component = default) Sets.Set(info.SetIndex, in component); } + /// + /// Records a remove operation for a . + /// Will be removed during . + /// + /// The component type. + /// The . + public void Remove(in PendingEntity entity) + { + Remove(entity.ToEntity(this)); + } + /// /// Records a remove operation for an (buffered) . /// Will be removed during . @@ -282,7 +377,7 @@ public void Remove(in Entity entity) /// A of 's, those are added to the . [SkipLocalsInit] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void AddRange(World world, Entity entity, IList components) + private static void AddRange(World world, Entity entity, IList components) { var oldArchetype = world.EntityInfo.GetArchetype(entity.Id); @@ -442,6 +537,11 @@ public void Playback() Destroys.Clear(); _addTypes.Clear(); _removeTypes.Clear(); + + unchecked + { + Generation++; + } } /// @@ -459,5 +559,10 @@ public void Dispose() _addTypes.Dispose(); _removeTypes.Dispose(); GC.SuppressFinalize(this); + + unchecked + { + Generation++; + } } }