genaray / Arch

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

Entity Events #25

Closed genaray closed 1 year ago

genaray commented 1 year ago

Being able to listen for entities event is also probably a nice feature. Many ECS frameworks actually do this. However, this should be an optional feature since it would heavily conflict with the "performance first" approach of this project.

world.OnEntityAdded += (in Entity en) => {};
world.OnEntityRemoved += (in Entity en) => {};
world.OnAdded<T> += (in Entity en, ref T cmp) => {};
...
genaray commented 1 year ago

As an small addition following eventbus system would come in quite handy:

[Event(priority: 10, buffered: false)]
public void MakePlayerShoot(ref Shoot shoot){
   ... Make player shoot
}

[Event(priority: 1, buffered: false)]
public void TriggerSomethingElse(ref Shoot shoot){
   ... 
}

var shoot = new Shoot(...);
EventBus.Send(ref shoot);   // <-- Send method is generated and simply calls all [Event] methods in order? Where ever they are?
deviodesign commented 1 year ago

Being able to control an entity's lifecycle is indeed useful.

Another way I've seen it implemented is by adding a listener to the query description directly. Functionally more or less like your example world.OnAdded<T> += (in Entity en, ref T cmp) => {};:

class SleepingLifecycleListener : IEntityLifecycleListener
{
    public void Added(in Entity entity) { }
    public void Removed(in Entity entity) { }
}

sleepingDesc = new QueryDescription().WithAll<Health, Sleeping>()
sleepingDesc.AddListener(new SleepingLifecycleListener());

// Alternatively
sleepingDesc.OnEntityAdded += ...
sleepingDesc.OnEntityRemoved += ...
genaray commented 1 year ago
class SleepingLifecycleListener : IEntityLifecycleListener
{
    public void Added(in Entity entity) { }
    public void Removed(in Entity entity) { }
}

sleepingDesc = new QueryDescription().WithAll<Health, Sleeping>()
sleepingDesc.AddListener(new SleepingLifecycleListener());

// Alternatively
sleepingDesc.OnEntityAdded += ...
sleepingDesc.OnEntityRemoved += ...

Thats actually also a nice way of doing that ^^

The only problem with our approaches is... that it will cause a lot of address jumping during literally ALL operations. Since every single operation would fire a potential callback (Create, Add, Remove, Destroy, Set...).

So if i ( or someone else) has some free time in the feature to implement this, it will become a optional feature which can get stripped out with preprocessor variables ^^

clibequilibrium commented 1 year ago

Hello @genaray I have seen your event bus implementation however it is limited to use of static methods, do you intend to have a reactive system approach similar to what flecs does with EcsOnSet flag?

For example I want to initialize a FrameData when a component is added and alter the component data.

For instance

    [Query, OnAdded]
    [All<FrameData>]
    private void InitializeFrameData(ref FrameData frameData)
    {
       // is called only once as the system callback acts as a reactive system

Currently I am faking it with with an empty struct that is only queried once.

    [Query]
    [All<FrameData>, None<FrameDataInitialized>]
    private void InitializeFrameData(ref FrameData frameData, in Entity entity)
    {
       // is called only once as the system callback acts as a reactive system
       entity.Add<FrameDataInitialized>();
genaray commented 1 year ago

do you intend to have a reactive system approach similar to what flecs does

Yep totally :) However such behaviour always comes at a cost. You are either forced to fire such callbacks directly (which will cause address jumping and slows down all important operations) or to store the data into some buffer before applying them to the entity itself. Both ways are kinda bad.

Actually the static methods are not really a problem ^^ Since static methods could just call local methods to redirect potential calls... however this is not ideal of course.

public static void EventReceiver(ref SomeEvent event){
    someSystemInstance.OnSomeEvent(ref event);
    ...
} 

Im looking for a different solution. E.g. extending the EventBus by the capabilities of storing/buffering events. Then the user could fire and process those events by himself. And they could also be involved in source generation. This could look like this :

// Event struct
public struct OnAddedEvent<T>{ public Entity entity; public T theComponentData; }

// Create and fire event
var entity = World.Create(...);
var event = new OnAddedEvent(...);
EventBus.Send(ref event, buffer: true);  // Event gets buffered, not processed instantly

// Somewhere in update where events should be processed
EventBus.Process();  // Calls the static or at some point local event receivers 

This idea is not finished yet. However the idea of reactive systems is very complex and hard. Since it will always mess up the performance, that's why i came up with the idea of a manual eventbus. Since that one can be added by the user where it is required ^^

clibequilibrium commented 1 year ago

do you intend to have a reactive system approach similar to what flecs does

Yep totally :) However such behaviour always comes at a cost. You are either forced to fire such callbacks directly (which will cause address jumping and slows down all important operations) or to store the data into some buffer before applying them to the entity itself. Both ways are kinda bad.

Actually the static methods are not really a problem ^^ Since static methods could just call local methods to redirect potential calls... however this is not ideal of course.

public static void EventReceiver(ref SomeEvent event){
    someSystemInstance.OnSomeEvent(ref event);
    ...
} 

Im looking for a different solution. E.g. extending the EventBus by the capabilities of storing/buffering events. Then the user could fire and process those events by himself. And they could also be involved in source generation. This could look like this :

// Event struct
public struct OnAddedEvent<T>{ public Entity entity; public T theComponentData; }

// Create and fire event
var entity = World.Create(...);
var event = new OnAddedEvent(...);
EventBus.Send(ref event, buffer: true);  // Event gets buffered, not processed instantly

// Somewhere in update where events should be processed
EventBus.Process();  // Calls the static or at some point local event receivers 

This idea is not finished yet. However the idea of reactive systems is very complex and hard. Since it will always mess up the performance, that's why i came up with the idea of a manual eventbus. Since that one can be added by the user where it is required ^^

Totally agree with you about frame flow being interrupted by doing administrative logic while iterating entities. So maybe a CommandBuffer to instantiate the events at the end of frame, process in the next and destroy by the end?. It is possible to introduce a concept of deferred reactive entities.

Most use cases don't mind 1 frame delay to process the event.