genaray / Arch

A high-performance C# based Archetype & Chunks Entity Component System (ECS) with optional multithreading.
Apache License 2.0
924 stars 78 forks source link

Outsource features to `Arch.Extended` #121

Open genaray opened 1 year ago

genaray commented 1 year ago

The newly introduced Relationships-Feature can be provided outside the core e.g. in Arch.Extended. The same goes with the CommandBuffer and the new Buffers in total.

This would keep the core slim and more organized.

skelitheprogrammer commented 6 months ago

Is it really worth removing the Command Buffer? Referring to the explanation of Command Buffer, there is a desire, on the contrary, to make it one of the root things of Arch. In other ecs libraries, Command Buffer analogues are a plug for the multithreading problem, which is not excluded with Arch. They look like they were made from the side.

Is World really supposed to have operations to interact with Entity (Add/Set/Remove Component. Maybe even Create?)? Maybe this should be the area of responsibility of Command Buffer, and World should have Extension functionality?

If you look at the same Wikipedia, then in the terminology there is nothing about World, Command Buffer. These are convenient tools for working with ECS pattern (World - entity hub/center/storage; Command Buffer - operation recorder). If you follow the path of lightweight and performance, then you can go very deep, which will create more problems (for example, create common World interface and put World in a separate package, like concrete implementation).

What if, the command buffer will work under the hood (not forgetting the ability to create your own instances), leaving the same extension methods

jzapdot commented 6 months ago

It seems like CommandBuffers are currently the best way to make structural changes in entity queries in Arch. Nearly all of my systems execute query logic akin to this:

World.Query(_queryDescription, entity =>
{
    // Do work to stage adding/removing components or creating/destroying entities by using a command buffer.
});

// Effect all changes by playing back the command buffer
_commandBuffer.Playback();

Are there reasonable or better alternatives to this type of design pattern? If not, it seems like it would be hard to justify extracting them as an optional feature.

skelitheprogrammer commented 4 months ago

What if we re-imagine Command Buffer?

We will have an IBufferCommand interface whose instances will be stored inside CommandBuffer and executed continuously.

public interface IBufferCommand
{
    void Execute();
}

public sealed partial class CommandBuffer : IDisposable
{
    private readonly List<IBufferCommand> _commands = new(); //or FIFO structure

    public void AddCommand(IBufferCommand command)
    {
        _commands.Add(command);
    }

    public void Playback(bool reset = true)
    {
        lock (this)
        {
            foreach (var item in _commands)
            {
                item.Execute();
            }

            if (reset)
            {
                Reset();
            }
        }
    }

    public void Reset()
    {
        _commands.Clear();
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }
}

This way we can encapsulate commands (Create/Destroy/Set/Add) in their own implementations and so can create custom commands to integrate them into the flow.

We can create a CreateEntityDeferredCommand that will do the same logic as the current CommandBuffer.Create(types).

public readonly struct CreateEntityDeferredCommand: IBufferCommand
{
    private readonly World _world;

    private readonly ComponentType[] _types;

    private readonly object[] _components;

    public CreateEntityCommand(World world, ComponentType[] types, object[] components) : this()
    {
        _world = world;
        _types = types;
        _components = components;
    }

    public void Execute()
    {
        _world.Create(_types).SetRange(_components);
    }
}

and instead of buffer.AddCommand(new CreateEntityDeferredCommand(...)) we can make some builder struct that allows the user to specify components using a fluent api. With this we can create an extension method to wrap everything in a nice form.

public static partial class CommandBufferExtensions
{
  public static CreateEntityDeferredBuilder Create(this CommandBuffer buffer, World world, in ComponentType[] types)
  {
     CreateEntityDeferredBuilder builder = new(world, types);
     buffer.AddCommand(builder); //or some better place to add command
     return builder;
  }
}
buffer.Create(world, types).Set(C1).Set(C2);

buffer.Set(someActualEntity, C1);
buffer.Add(someActualEntity, C2);
//or maybe wrap up with some api to create chain of modifications on a particular entity at the time

buffer.Remove<C1>(someActualEntity);
buffer.Destroy(someActualEntity)

buffer.Playback();

The same is true for other commands.

Thus, we can move the CommandBuffer to the Extended repo because it no longer applies only to the core. The user can add their own behavior to synchronize with deferred execution.

UPD: Made a branch where I will implement something similliar