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

Observer pattern #21

Closed
baudren opened this issue Apr 2, 2016 · 12 comments
Closed

Observer pattern #21

baudren opened this issue Apr 2, 2016 · 12 comments

Comments

@baudren
Copy link

baudren commented Apr 2, 2016

Is there any plan to implement something equivalent to the Observer pattern (see for instance in entitas? They discuss about it in their Unite talk (from their readme), around the 19 minutes mark.

Essentially, it allows to register a Processor to react to a change in a given group (a set of entity with a given assembly of components). It can be either when a new entity enters, or leaves. This reduces often the amount of code needed to connect systems between them, and can also improves performance (no query is done by such a method if no changes have been detected).

To give an example, considered a system charged to generate a bunch of data the first time a terrain is created, and another system in charge of finding which terrains to load. One could link the two with an additional component, NewTerrainComponent, and have the second system add this tag, then the first system treat this terrain and remove the component.

Merging both into one system goes away, in my sense, from the single responsibility pattern of ECS. An observer system, however, could simply declare an action on a new entity being added to the group of entities with TerrainComponents.

It may be a somehow controversial feature - I think not everyone agree that it is part of an ECS system. It may be also necessary to introduce the concept of groups for this to work, so I realise it will not be a small one-time hack. This issue is just to know whether or not something like this would be considered at some point, or if esper is considered more or less feature complete as-is.

@benmoran56
Copy link
Owner

@baudren, I've not used Entitas myself, but I did watch the video and look over the documentation.

Unless I'm missing something, it seems like the main purpose of the Entitas Groups is query performance. In esper, there is both an _entities and _components database "under the hood" (thanks to @SpotlightKid). This means that querying for single Components doesn't require iterating over everything. Multiple Component queries, likewise, are also very efficient. My understanding is that the Entitas Groups mainly serve the same purpose, in which case there is probably no benefit to implimenting this concept in esper. Please correct me if I'm wrong on this.

Is the main purpose you have to simply add/remove components from an entity within a Processor (System)? Each Processor in esper has a reference to the World, so something like this following is already possible:

def process(self):
    for ent, (vel, jmp) in self.world.get_components(Velocity, Jump):
        if vel.y == 0:
            vel.y = jmp.power
            self.world.remove_component(ent, Jump)

Is this what you mean, or something else? Sorry if I'm not following, but I've not had any need for this type of pattern in my use of ECS in the past.

All that said, esper is considered feature complete, but it is still open to improvements at this point. Particularly, and performance improvements. Additional features will certainly be considered providing they do not impact performance negatively, do not require external dependencies, and fall within the scope of the project. A lightweight (optional) event system might be a possibility, but I'm still experimenting with other things at the moment.

@baudren
Copy link
Author

baudren commented Apr 4, 2016

Thanks for the answer, @benmoran56. No, the usage is different from what you presented. The groups serve two purposes, as far as I can tell: one of performance (already taken care of by @SpotlightKid 's implementation), and one of reactivity.

Briefly, the reactivity allows you to trigger a part of a processor out of the responsibility chain of the processors. For instance, triggering some particle effects thanks to your particle processor when triggering an attack animation. If you would add some entities with a particle components, in a turn

As an example of reactivity, consider a simple battle system, where players can perform attacks which emit particles on hit, because graphics are nice, sometimes. You are within a turn of your system, where a player just attacked, so you created a bunch of particles. These should be treated by your ParticleProcessor, but it sits at the end of the line of processors, and you are still within your TurnProcessor because you can still perform some actions.

If the ParticleProcessor can define a react method to whenever a certain group of entities gets new members with InstantParticleComponent, it can be processed at any time during your Processor chain, without either system knowing about each other.

An equivalent way of doing it would be to use a publisher/subscriber pattern, where the TurnProcessor would fire an event, to be received and processed by the ParticleProcessor. But using a group as a way of receiving these events (onEntityAdded, onComponentRemoved), you can obtain a well reactive interface without coupling the processors.

Let me know if it makes more sense, now :-)

@benmoran56
Copy link
Owner

Oh I see what you mean now. I have to say I'm not very fond of this style of inter-Processor communication. I much prefer handling

It seems to me that any any possible react methods would be better off as dedicated Processors themselves. Tacking on a particle Emmiter component that would later get picked up by a ParticleProcessor is is how I've always done these types of things. If the ParticleProcessor is already run, then it gets picked up on the next frame (which at 60fps would be only ~16ms delayed). I guess I'm not seeing the advantage of having instantaneous interaction with another Processor. In your example, are you talking about a World that is only updated once per player turn? Whereas the next call to World.process is waiting for user input?

The biggest hangup to implementing something like this would be that adding Groups would likely have a negative impact on performance. The current component queries are as fast as we could make them, so additional group creation and updating would require some overhead.

A publisher/subscriber patter, simple event handler, would be possible to implement. It can be added as an optional thing, without any negative impact. I'm thinking something basic, such as this popular example here: http://www.valuedlessons.com/2008/04/events-in-python.html

@baudren
Copy link
Author

baudren commented Apr 5, 2016

Yes, I am talking about a turn-based game, that waits for user input. But it can also bring value for other types of games.

To take the example introduced in entitas' unite talk, the score system has a standard update method, to display the score. But the changing of the score is done through watching the group of entities representing the bubbles. If one leaves this group, it add plus one to the score. If you don't have such a system, you would need the destroy-bubble system to somehow tag the entities that were deleted, for the score system to update it later, which introduces unnecessary components, at best.

So having a react method and a normal method would be a normal thing for a processor, whose goal is simply to regroup all code belonging to a certain responsibility.

About performance, that's probably true. For a turn-based game, it is not as crucial as it may be for you, but I understand the importance of making it as fast as possible.

And about just introducing a simple event handler, if it does not take advantage of the group watching, I am not sure that it would add enough functionality to warrant implementing it, but let me know what you think.

@benmoran56
Copy link
Owner

Ok, all of your examples make a whole lot more sense now when put into that context.

Yes, performance is a top priority. Esper has to be usable in typical 60fps games, so maximizing the number of queries per ms is essential. I think it's pretty good at this, as it's much faster that other ES libraries out there. Still, I don't want to consider anything that would slow this down.

That said, I'm still open to this idea if it can be implemented optionally and without penalty if not used. Is this something you would like to try working on? I'd be happy to mock up ideas with you in another branch to see how it could look.

@baudren
Copy link
Author

baudren commented Apr 6, 2016

Thanks for the offer. I will be implementing something that works for me on my code, but my top priority is not to keep dependencies minimal, but rather keep using libraries that make what I want so that I can actually write a game. So I am currently looking at pyzmq with the tornado event loop, which does not fit your definition of minimal, I think :)

You mention that it is much faster that other ES libraries - I know that there is no entitas port in python, but did you compare it to this framework? Or you meant compared to other python ES?

@benmoran56
Copy link
Owner

I've not used it myself, but pyzmq looks like a nice library.

I've only compared esper to other Python ES libraries, such as ecs and ebs. From my benchmarks it's much faster. I'm not sure how it would stack up to Entitas, because I haven't used it and was unable to find any benchmarks with a quick Google. I am curious though. Esper is currently using Python dictionaries for the databases, which are quite fast as far as Python data types go. Other languages have the potential to be much faster with a more low level implementation, but I'm not sure how C# stacks up.

@benmoran56
Copy link
Owner

I'm going to close this for now. If you want to revisit this in the future, please get in touch or open another issue to start a new conversation.

@benmoran56
Copy link
Owner

benmoran56 commented May 13, 2016

I just had a quick throught on this today, and think there might be a simple solution to what you're trying to do. As it is, each Processor has access to the world. So, you could easily call any method in another Processor by doing something like the following:

for proc in self.world._processors:
    if type(proc) is OtherProcessor:
        proc.method()

Could you let me know if something like that would work, and would be useful? If so, I could easily add a small function to esper such as "self.world.get_processor(ProcessorType)", which could be used to easily call any processor's method: "self.world.get_processor(ProcessorType).method()"

I wouldn't use this for my own projects, but it would be just a few lines of code.

@baudren
Copy link
Author

baudren commented May 22, 2016

Hey @benmoran56 , thanks for the idea. It indeed works, and is a small workaround that is useful for me. Would be cool if you implemented the method that you suggests, but it is perfectly usable as is.

Thanks again for giving a thought to the problem!

@benmoran56
Copy link
Owner

Great, I'm glad that works for you. I went ahead and added a new World.get_processor method (which basically does the same as the above code). It's only a few lines, so no harm in adding it in if it can be useful. As I described above, you should now be able to do the following:
self.world.get_processor(ProcessorType).some_method()
from within your other Processor.

@baudren
Copy link
Author

baudren commented May 22, 2016

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants