mlange-42 / arche

Arche is an archetype-based Entity Component System (ECS) for Go.
https://mlange-42.github.io/arche/
MIT License
131 stars 8 forks source link

How to optimize collision detection? #374

Closed TLINDEN closed 7 months ago

TLINDEN commented 7 months ago

Hi,

sorry to bother you with this noob question, but I hope you might have an idea how solve this.

The issue is this:

I have one player entity which moves around on a grid. It could collide with collectible items (which then have to disappear) or obstacles like holes or enemies.

My current aproach looks like this:

I have a query for player entities, where I check user input, deduce player movement etc. Inside this query loop I have another query loop over all collectibles. There I check if the position of the player overlaps with the one of the collectible and take appropriate action if it does.

While this works perfectly well, this leads to a growing recursive loop which is hard to maintain. And I need to add more entity types in the future.

So I wonder if there is another, more elegant, way to compare components of different entity types.

The best would be to have some kind of observer, which subscribes to the position component of the player with a callback and is attached to a collectible entity somehow. Then, whenever the position changes, the callback will be executed wherein then the actual collision check is being made. Or a method on the observer which returns true if the observed component changes, would be sufficient as well. I could then just iterate over all observers, check if something changed and then take action.

Does this make any sense?

Unfortunately I don't know how to do this with arche or if it supports such a feature (I didn't find something like this in the docs).

Any idea or hint would be much apreciated!

Best, Tom

mlange-42 commented 7 months ago

Hm, maybe you missed one important point about Arche?
This point is that you can store entities in any data structure you like, and retrieve components for them with World.Get(e, compId) or gneric.MapperX.Get(e).

So, you can store the player entitiy that you obtain from e.g. World.NewEntity in a resource (see World.Resources() etc.) to make it available to systems. (EDIT: or make the complete player a resource, at the cost of flexibility).

Then, you could just take it from the opposite side, iterate your collectibles, and check against the player you obtained as a resource in your system.

:warning: Note that you should always store the entitiy, not a pointer to it. And never store a component (pointer) you obtained from a query, World.Get or MapperX.Get!

If you think further, this offers a lot of opportunities. E.g., if only one collectible per cell is possible, you can have a grid [][]ecs.Entity, which contains the zero entity if a cell is empty, and the entity of the collectible otherwise. You can then check for the player's current cell grid[x][y].IsZero() (World.Alive(...) if applicable), and inspect the entity in the cell with World.Get, World.Has, or the generic equivalents.

When we come to such data structures and their bookkeeping, it may be worth noting that ecs.Listener is helpful here, to automate updating on entity creation or removal.

mlange-42 commented 7 months ago

BTW, for more advanced collision detection, you may want to check out this demo which uses Gonum's kdtree for very efficient "spatial indexing" (i.e. proximity queries):

https://mlange-42.github.io/arche-demo/flocking/

But this is definitely overkill for your use case.

TLINDEN commented 7 months ago

Hi,

I did as suggested and it works very nice. Although this adds even more layers of complexity to the code, in general it makes things way easier (and more fun to work with). Much appreciated!

Thanks a lot!

best, Tom

mlange-42 commented 7 months ago

Closing as answered.