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
Removing a component changes entity reference within a System update method #73
Comments
On this topic, it seems that basically this is because removing components can update the underlying array in the ReadOnlySpan of entities while it is being iterated through. That is very problematic behavior and should be clearly documented and warned against if that is a behavior of the system that can only be prevented by not using it in that manner. |
ah I thought I had written something in the readme but it is just for multi-threading, not the simpler case >_<
I will add some info in the readme, thanks for bringing it up, sometimes I forget that obvious things are only obvious because I am the one who made it... Special case where it will accidentally work:
|
Might I suggest changing the API to deal with this as part of your library? Removing a component seems a very typical usage pattern, and mutating the Span under enumeration as a result is definitely problematic behavior. Relying on the user to understand this and use workarounds like copying the array (slow for performance intensive cases) or using Recorder API (a bit unwieldy) is not ideal - we want the typical expected usage to behave as expected (the "Pit of Success"). One way to solve this is to write a wrapper struct around Entity which you enumerate from EntitySet. This struct would record mutation changes that would otherwise change your underlying array. Let's say it's called EntityMutator for the purposes of this argument. Then you could write a custom version of ReadOnlySpan which your EntitySet would return. This Span would be simple, it would only differ from ReadOnlySpan in returning an enumerator that would wrap each Entity in an EntityMutator as it is returned in the Current method, then apply those changes after enumeration is finished or by explicitly calling a method. This would make the default usage the correct one, with minimal performance overhead (essentially just wrapping an entity in another struct), and require only one iteration through the array still. |
What you are desbribing as EntityMutator is basically the EntityCommandRecorder and EntityRecord. The main focus of current implementation of EntitySet and AEntitySystem was to have a default iteration as fast as possible with no garbage generated for what I thought was the main usage: updating component values without changing the composition of entities. I really get your "pit of success", I am starting to think of "free" way (memory wise) to freeze an EntitySet while operating on it (maybe disable its subscriptions so the content won't change, but then either you would have to record all messages to replay them after the processing so not memory free, or recheck every entities individually if it still need to stay there which would be expensive), or maybe some extension methods to help the setup and usage of EntityRecord, this probably won't satisfy you as an answer and I hope I (or someone else) will come up with something better but I really don't want to make those cases the default one for now. |
I understand that you want to keep the default case very fast and efficient. And I think you've done that very well. It's the best ECS in C# that I saw as far as efficiency, memory locality, and performance (I profiled it too). That's why I'm using it for a game :) To your point about it being like IEnumerable, I completely agree and that is exactly my point. To modify an IEnumerable's underlying collection while iterating is a blatant user error. But the interface doesn't easily let you do so. To to be able to do so, you would have to go outside the interface. Also, it's the collection itself that would have to be modified, but modifying the state of the items is safe. Here in the ECS, modifying the state of any item through the ECS is unstable and leads to the collection being modified while iterating, which I think is very unintuitive behavior and easy for a user to cause, leading to a difficult debugging exercise (without an exception). I do think that there are a range of types of logic needed for a game, and the system can robustly acomodate more than one. For example, here are two use cases. 1. Go through a large set of entities with a position and a velocity, and update the position based on velocity. This is a clear use case for your ECS system and fits well with the type of logic you have in mind. It is safe and easy to write with your system. 2. A status effect is added/removed on a target unit from a source depending on some skill checks, defense checks, etc. This type of code is really different, and does not take advantage of or need the memory locality or non-allocation that your code provides. What is needed is clarity and expressiveness of code. I contend that in the course of writing a typical game, there is far more code of the second type written than the first. So I do think it is important to consider supporting the second type of logic. I think your system already has the solution at hand. You already have the EntityCommandRecorder and recommend using it for these situations. I think you can just build your system to help the user and make that usage easy. For example, you can have two types of virtual methods for Entity Systems. Keep the one you have for the first type of code - high N cache-sensitive "functional" style code. Provide a second type of virtual method, passing in an EntityMutator to the user code. You can create an EntityCommandRecorder before iterating and calling user code, and the EntityMutator can just be a simple wrapper that forwards Set/Remove calls to the EntityCommandRecorder, and then after the loop you can execute the commands. The user can operate on the entity with pretty similar logic and the stateful mutations are handled. That's pretty simple; it uses your existing code systems, and helps the user follow the pit of success. |
I tried to make the api open so it should be possible to define your own base system types but you are right, those use cases are common enough that there should probably be already offered here. |
3009d77 added AEntityBufferedSystem |
Another way to look at it is that the ECS approach is treating game state akin to a database. The data is laid out contiguously and efficiently, and a query is constructed with an EntitySet to iterate over the contents. It would be very problematic in a DB if a query that modified items would mutate the selection, causing references to jump, skipping over elements, etc. A DB query therefore pulls an immutable selection. What if this code followed that model and didn't try to propagate component changes immediately? It doesn't need to be updated so reactively perhaps. Maybe instead of recording the actions and replaying them, the part of the system that is mutating the collection should be reconsidered. |
To return something immutable, it means you need to copy something somewhere, which you probably don't need nor want (for performance) in every cases. I believe the AEntityBufferedSystem fills this need.
I think there is only two ways to handle mutations, either synchronously like it is done for now (which is not thread safe), or by queuing the changes to process them later (which require something to store the changes, so it would cost memory and might stress the gc, and a method to "commit" them safely). Honestly I choose the synchronous way because I am lazy, and didn't want to be responsible for gc garbage generation and incurs a not needed cost in all cases. Currently this is what I am leaning to do:
Things that still bother me:
I believe there is some balance to find in there, for example maybe I will only introduce the ReadOnlyEntity type for the AEntityDeferredSystem, still thinking about this. |
so I finally got around working on an analyzer, the first version is available with some basic validation which I feel is much better than what I proposed previously. You would be warned from improper usage inside |
Within an AEntitySystem update method, if a component is removed from the current entity reference, such that the entity no longer matches the system's EntitySet filter, then the entity reference will actually jump to a different entity matching the EntitySet filter. In other words, the Update method is now suddenly operating on a different entity. This would obviously lead to unexpected behavior within the rest of the update method.
Given this behavior, this really should be documented somewhere.
The text was updated successfully, but these errors were encountered: