Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entity Events #25

Closed
genaray opened this issue Dec 4, 2022 · 6 comments · Fixed by #98
Closed

Entity Events #25

genaray opened this issue Dec 4, 2022 · 6 comments · Fixed by #98
Labels
Arch.Extended This feature targets Arch.Extended enhancement New feature or request

Comments

@genaray
Copy link
Owner

genaray commented Dec 4, 2022

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 genaray added the enhancement New feature or request label Dec 4, 2022
@genaray
Copy link
Owner Author

genaray commented Mar 9, 2023

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
Copy link

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
Copy link
Owner Author

genaray commented Apr 4, 2023

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
Copy link
Contributor

clibequilibrium commented Apr 5, 2023

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
Copy link
Owner Author

genaray commented Apr 5, 2023

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
Copy link
Contributor

clibequilibrium commented Apr 6, 2023

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.

@genaray genaray added the Arch.Extended This feature targets Arch.Extended label Apr 24, 2023
@genaray genaray linked a pull request May 3, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Arch.Extended This feature targets Arch.Extended enhancement New feature or request
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants