Closed Lehnart closed 2 years ago
Thanks for opening the ticket. I wasn't planning on adding Event Dispatching to esper. Mostly because other libraries exist, and are easy enough to mix in. For example the pyglet
EventDispatcher framework, which I use myself.
That said, this is not the first request for this that I've received. I'll have to think about this for a little bit.
It wouldn't be too hard to add a light-weight event dispatching system to esper
. I'm not sure how far I would want to take this though. Consider the following simple implimentation:
Event handlers could be registered like this:
self.world.set_handler('on_pickup', self.my_method)
self.world.set_hanlder('on_pickup', my_func)
Events can be dispatched by name, and from anywhere that has access to the World
:
self.world.dispatch_event('on_pickup', arg1, etc)
Events would be unregistered like this:
self.world.remove_hanlder('on_pickup', my_func)
Thanks for your answer. If I found time to do this, I would come back with a proposition on this.
I mocked up a quick prototype of this. The API is essentially the same as what I posted above. If you'd like to have a look, it's in the eventsystem
branch:
Looks nice and simple.
don't you think the event processing should be inside the processor? It bugs me that data/components may be read and modified in an event processing rather than in a processor.
(Just a heads-up, but that code I posted earlier is probably non-functional. Maybe it would be better to do esper.set_handler(), etc
?)
What do you think that might look like? Could you share some pseudocode maybe?
EDIT: nevermind, you already did in your first post :)
If Events are iterated over, instead of being dispatched directly, then I think some kind of internal event caching would be necessary. For example:
ProcessorA
ProcessorB.receive('on_coin') --> None
ProcessorC
ProcessorD.dispatch('on_coin', 3)
ProcessorE
ProcessorF.receive('on_coin') --> 3
If ProcessorD dispatches an event, it would have to be stored internally so that ProcessorB has a chance to receive it. What's not clear to me is how long is this cached for? I haven't used any event systems that work this way myself.
Actually, I have a working prototype on my side. I will push it there and we can discuss it.
#Somewhere in esper.py
class Event:
""" Not sure it is useful to have an Event class but let s go with it ... """
def __init__(self):
pass
def key(self) -> Type:
return self.__class__
class EventQueue:
def __init__(self):
""" Event queue is a dictionary. Keys are event class and values are list of event instances and their living duration as an integer """
self._queue = {}
def add(self, key: Type, event: object):
""" Add a new event of a given type inside the queue """
if key not in self._queue:
self._queue[key] = []
self._queue[key].append([event, 0])
def tick(self):
""" Increment lifetime of events """
for key in self._queue.keys():
for event in self._queue[key]:
event[1] += 1
for key in self._queue.keys():
self._queue[key] = [msg for msg in self._queue[key] if msg[1] < 2]
def get(self, key: Type) -> List:
""" get all events of a given type if they have been their for exactly one loop of process """
if key not in self._queue:
return []
return [event[0] for event in self._queue[key] if event[1] == 1]
EventQueue
is the class responsible for the dispatch of the events. Event have a kind of life time :
This way, each event can be read only once by a processor (assuming that each processor run during a world process loop).
World Class :
class World:
# ...
def publish(self, event: Event):
""" When someone want to publish a new event """
self._message_queue.add(event.key(), event)
def receive(self, event_class) -> List:
""" When a processor want to access given type of events """
return self._message_queue.get(event_class)
# ...
An example in a processor :
class MoveProcessor(Processor):
def process(self, *args, **kwargs):
move_events = self.world.receive(MoveEvent)
for move_event in move_events:
r = self.world.component_for_entity(move_event.ent, RectComponent)
r.move(move_event.dx, move_event.dy)
Let me know what you think of this.
2 more comments on this:
I just pushed out a new Esper release, with an event system. To keep it simple, it's direct dispatch. This is similar to event systems in libraries like pyglet
.
For lazily handling events, you can easily cache them for later iteration:
class MoveProcessor(Processor):
def __init__(self):
self.event_queue = []
esper.set_handler('on_move', self.move_handler)
def move_handler(self, *args):
self.event_queue.append(args)
def process(self, *args, **kwargs):
for args in self.event_queue:
...
do_something_with_event()
...
self.event_queue.clear()
Let me know if you have any questions about how to make it work.
Perfectly fine with this. Thanks a lot for your work!
No problem, and I hope it's useful. By the way, if you didn't see it, there is a short explanation at the bottom of the README.
Yep. I did read that and it was crystal clear. I ll test it soon in my own projects.
When using esper, I like to have all my processors decoupled. I mean no processor needs to
python import another_processor
to make his job. To achieve this, I use an event system.I have a python package containing Event classes that have no dependencies, not on Component neither on Processor. A processor can import any Event and can publish it thanks to a function of World. Here is a snippet of a processor that only publishes an event (Maybe I've should used message instead of event :D ):
Another snippet where a processor receive the event :
It could be great to have this mechanism inside the ECS library :