Frage

I'm working on a Entity Component System for learning purpose. I made major changes with my design so I was wondering if I could pick a better design for my event system.

The purpose of the event system (if you don't know ECS), is to allow communication between process. For example, in a video game (that's always more easy to understand with video games) your collision system may want to tell to other system when there is a collision and with which entities.


With this little context, my current design for the event system was more :

  • When an event is emit -> save it into event manager (shared ptr)
  • Then call every subscribers and send them a shared ptr of the data (thanks to Bart van Ingen Schenau)
  • Inside the callback, save the event into a queue to consume it on the next update call

But events are read only, so I wonder if something like that won't be better:

  • When an event is emit -> call every subscribers and send them a copy of the event
  • Inside the callback, move the event into a queue to consume it on the next update call

The systems will be multithreading (which mean at least than events can be emit on any thread).

One of the most important part of my new design, is that system are "split" in blocs (you can see that like several update function). But events are for the whole system. So copies will mean that I'll need some garbage collector function for each system to free events which where consume. Well in fact I still need that with shared ptr.

There is probably a lot of other designs. So if you think of something good for an ECS generic (not only for video games), where events subscriber tend to regroup, give me your thought please.

War es hilfreich?

Lösung

In my case I don't bother with copying events or anything like that and there's no concern about thread-safety at the event processing level since it's all done by a single thread. :-D

That said, it still utilizes a concurrent queue since different threads could want to concurrently push events, but there is only one central handler popping events and invoking a process_events callback on systems that specify it (non-null function pointer on registering the system -- my SDK uses a C API). That central event handler also spends most of its time sleeping while waiting to be notified since I don't spam the system so much with events.

The systems then just loop through the read-only array of read-only events popped off by central event handler and do whatever they need to do, and all of that is happening inside the event-handling thread. By design it's encouraged that systems don't do any heavy work in the event processing callbacks. Instead if they have interesting and complex work to do, they can queue it themselves to process in their main processing loops which could be running in a different thread from the event handling callbacks or spawn off a new thread specifically in response to that one event.

I find this the simplest solution to reason about. I also try to take it easy on event-handling and minimize its use since I find it the most uncomfortable type of "inter-system communication" possible. Personally I always disliked event-driven programming, finding it a necessity but not something I want to lean on too much, and always trying to avoid it when possible. I like more easily-predictable control flows whenever possible where I'm never surprised about "when" something happens. I also dogmatically made it a superficial error (even though the system can handle it) to try to push events from the event handling callbacks. :-D I hate cascading events where one event triggers another recursively and so I deliberately made that painful to do with my system in hopes that anyone tempted to do it will think up something else with a "flatter, not deeper" design that doesn't cascade events throughout the system.

Andere Tipps

Both designs are in principle viable and which design is best depends more on how the events get used and how much information an event carries with it.

The main drawback of sending copies of events is that if you have events with a lot of data that get sent to a lot of subscribers, then that will consume quite a bit of memory. Also, with sending copies it will be harder to support variable-sized events (where eventA carries more information than eventB, but both can exist in the same queue).

The main problem with the shared pointer design, as described, is the problem of lifetime of the events. As the subscribers only get a weak_ptr and store that in a queue, it is hard to tell when all subscribers have handled the event and when it is safe to destroy the shared_ptr.
This can be easily remedied by sending shared_ptrs to the subscribers and letting the reference counting that is inherent in shared_ptr do its work. Accidental modification of the event can be prevented by using shared_ptr<const Event>.

Lizenziert unter: CC-BY-SA mit Zuschreibung
scroll top