-
Notifications
You must be signed in to change notification settings - Fork 144
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
Way to make Engine#removeAllEntities() "atomic" operation #161
Comments
I do not think so, thanks to this check in Engine#removeEntity(Entity): if(entity.scheduledForRemoval) {
return;
} Have you really encountered this issue? If so, a test case would be very useful. I'm interested because I have also experienced issues with the delaying of add/remove operations. The only solution that seems to be able to work is snapshotting the entity arrays, which requires heavy refactoring of the |
Sorry for delay.
When you call this method outside of updating or notifing, it will just literately remove entities and notify listeners in every iteration, and somewhere inside this loop entity deletion happens during notifying. So, as you can see, there are no any entities with |
Thanks, I created a test case for this: final Engine engine = new Engine();
final Entity parent = new Entity();
final Entity child = new Entity();
// This system is needed to force processing of entity operations
engine.addSystem(new EntitySystem() {
});
engine.addEntity(parent);
engine.addEntity(child);
engine.addEntityListener(new EntityListener() {
@Override
public void entityRemoved(Entity entity) {
if (entity == parent) {
engine.removeEntity(child);
}
}
@Override
public void entityAdded(Entity entity) {
}
});
engine.removeAllEntities();
engine.addEntity(child);
engine.update(0f);
assertTrue(engine.getEntities().contains(child, true)); It might be necessary to write a few more tests to cover all possible situations, but this is at least a good start. |
This is basically the same test case, but modified to use final PooledEngine engine = new PooledEngine();
final Entity parent = engine.createEntity();
final Entity child = engine.createEntity();
// This system is needed to force processing of entity operations
engine.addSystem(new EntitySystem() {
});
engine.addEntity(parent);
engine.addEntity(child);
engine.addEntityListener(new EntityListener() {
@Override
public void entityRemoved(Entity entity) {
if (entity == parent) {
engine.removeEntity(child);
}
}
@Override
public void entityAdded(Entity entity) {
}
});
engine.removeAllEntities();
Entity another = engine.createEntity();
engine.addEntity(another);
engine.update(0f);
assertTrue(engine.getEntities().contains(another, true)); I'll look into solving this, probably by demanding |
Well, I think I've managed to implement this snapshotting. Check it out at my fork. However, this introduces breaking changes (it requires some extra care to be taken by custom |
Wow, it's almost new engine :) A lot of changes and I think @saltares should look on it. Just because this is a narrow place, so after operations have been processed, our loop will remove all entities anyway. |
I chose snapshotting because it results in more predictable behavior. It also simplifies |
|
Benchmark results with snapshotting:
Benchmark results without snapshotting:
I do not know if my entity listener priority patch affects the performance of the first benchmark, but it seems like there is no significant difference. (And Edit: These benchmark results are incorrect. Check the comments below. |
Could you try to perform the test with more entities? Like an order of magnitude or two more. |
Huge test (1.000.000 entities) with snapshotting:
Huge test without snapshotting:
Note that the benchmarks does not add entities while updating, so there is no need for Edit: These benchmark results are incorrect. Check the comments below. |
I then think we need to add/remove some entities while updating. That's what snapshot solves and that is what penalizes performance... I think we might have to solve this issue without adding a lot more overhead if possible. Ashley's performance is not brilliant as is right now. |
Actually, I don't think the snapshotting will be a major problem, as snapshots are recycled. The only thing that could kill the performance is nesting |
Whoops. I just noticed the benchmarks were run against the same version of ashley, |
I fixed this in the |
So, here are the new benchmark results:
Without snapshotting:
So I guess these benchmarks are irrelevant when it comes to snapshotting performance. Edit: I actually think the slightly better performance of the first benchmark is caused by the usage of |
Benchmarks in the millisecond range are useless. The only one that counta is the huge test. Even then one needs to take into account vm warm up etc. Benchmark suite should use the JHM benchmark harness. As for deletetion of entities: Ashley needs to decide if that should happen immediately or deferred. If the former, then only snapshoting is a working approach. If the latter, then there's a few things one can do. Simplest solution for the later: keep track if you are in engine.update(), in which case removeEntity will defer until the end of the update() call (last statements in update() will delete entities). If you are not in engine update or within the delete statements of update(), you can remove entities immediately. |
I think @metaphore is right on the money with his patch. Killing entities during an update is less predictable than killing them at the end of an update. E.g. what if you had a box2d physics entitysystem. Within a collision callback, you remove one or more entities -> boom goes box2d. It's a lot safer to do removal in one specific place when no system is doing anything anymore. |
|
One solution to this problem could be to actually make operations "atomic" - maybe by using a kind of double-buffering. I have written an own ECS implementation that uses |
This was fixed in a6fcfd0, thanks a lot. As for the current issue, sorry for taking so long to take a look at this. |
No; it was fixed in #168. :P
Are you keeping the concept of "operations" or have you got other plans in mind? I really think Ashley should introduce the concept of an index for entities, like I described in #171, and maybe even refactor component storage, as proposed in #87. But the current API makes things very hard to change without breaking stuff. You could take a look at artemis-odb or retinazer ;) |
My bad! :-(. I really wanna make Ashley better for everyone, including it being networking friendly. Right now it's a bit hard for me to spend enough time on it but hopefully that'll change in the near future. |
@antag99 it seems your fork is gone... Any chance you could make that available again? Maybe there's a way of using Snapshotting while not having to put additional responsibility on derived EntitySystems. |
Nope; I've got the source code, but no git repo. It was more of a quick hack, anyway.
In retinazer, I implemented an Another thing to be considered is when notifications take place. This can be delayed too, but I'm not sure whether it's a good idea or not. |
Just like to weigh in here if I can as I am really opposed to this ticket's request... in my project I have a removal System that removes any entity with a Remove component, but also has a boolean in it that fires a removeAll(). This system is always the last system to run. I have found working extensively with Ashley that deferring entity removals to the end of a system cycle with a component as a flag is a really smart way to work as aside from the functional consistency you can cancel entity removals mid frame or even cancel the entire removeAll operation before it occurs. Working in this way, if I have a load of stuff I want to action immediately after the removeAll has occured I have another System who's job is simply to process runnables that I have submitted at the start of each frame. this means if I want to create a tonne of entities etc (as I am loading a new level for example) straight after I have nuked everything, I will set the removeAll flag on my removal system then post a runnable to my ExecuteNextFrameSystem. This also gives you a thread safe way to submit stuff directly to your Ashley loop which was a big requirement for me. Other than building these features directly into the core of Ashley and normalising this behaviour I'm not sure how you will get around it directly. Personally I hate the idea of an atomic removeAll() as that nukes the per frame state consistency that makes Ashley so freaking awesome, deferring all of that stuff to the end of the frame is really smart and is really neat to work with, so I would actively fight the idea of atomic removals mid-frame, I think it actually breaks one of Ashley's fundamental design paradigms. IMO the removeAll method is a bit dangerous at the moment and should function like remove, in that once called it will register an event to be processed at the end of the current loop. Either that, or it should be a flag on engine that is checked at the end of each frame, that way you could decide mid-frame actually you don't want to nuke everything afterall and set it back to false should that edge case ever occur. An example for why my Remove component is so good is server side I have entities that represent players, if they are in combat and disconnect their connection drops but their entity stays in game until they are out of combat... it happens (more frequently than you'd think) that they reconnect the exact same frame their entity is being removed, this means people were logging in and being paired back up to their entity just as it got removed, much bugs ensued. By using the Remove component as they are paired back up with their entity I can check for that component and remove it, thereby cancelling the remove and rescuing the entity. I don't know if such a case exists for a full removeAll call, but hopefully this demonstrates the robust nature of working in this way. I think your efforts would be better suited to exploring why you have an atomic mid-frame removal operation in the first place to be honest as this seems to be a square peg round hole situation. Liam |
@carlislefox you misunderstand the initial problem. "Atomic" means not just instant removal of entities from the engine, but the approach in which engine should remove all entities right after update and have no pending operations remain for the next updates after it. Anyway the issue was about non update state of engine, where all entities should be removed instantly by design, but in illustrated case they weren't. Just consider that this is no longer the case, because |
Hi guys, here is today problem...
Overall situation:
In some moment of my game life-cycle I need to reset world. By "reset world" I mean remove all entities from engine and then recreate game objects from scratch. Simple code may looks so:
And consider all entity systems must survive through every reset and handle entity add/remove events correct. So now assume we have some sophisticate system that handles two types of entities at same time (like layer entities, and drawable entities that bound to layers). And when the system had notified about layer entity has been removed from the engine, system removes all drawable entities of that layer from the engine too.
Here I have to say that I use
PooledEngine
andPooledEntities
that I obtain only from engine. So here finally the problem (case when pt.1 occurs outside of engine update):resetWorld()
;Engine#notifying = true
);Engine#removeEntity(Entity)
for each drawable entity (which actually already deleted).Engine#removeEntity(Entity)
creates newEntityOperation
of remove type (because engine right now hasnotifying
flag);initWorld()
and here some new entities create from engine pool and get filled.update()
method and here engine finally processes all delete operations from pt.3.All that may go even bad if you call
resetWorld()
during engine passing update stage. But the problem is I have no ability to check it,Engine#updating
is private and has no accessor. My first question is should we add getter for it (same question aboutEngine#notifying
)?Now my temporary solution is to call extra
engine.update(0)
beforeinitWorld()
so all pendingEntityOperration
s will be gone. Which is not so good solution, because I cannot even imaging what may happen when I callEngine.update()
insideEngine.update()
(engine does not check that case...). And the other similar solution is to callinitWorld()
usingGdx.app.postRunnable()
which does pretty the same, but it's not fits my game architecture, because there are some other things insideupdate()
that should not be called in such state.Perhaps the best thing that may help is extra check inside
Engine#removeEntity(Entity)
that will just skip entities which is not active in the system right now (Engine#entities
doesn't contain it)?Conclusion
I'm trying to find a way to remove all entities during one engine update cycle, avoiding any pending
EntityOperation
s after that.Thanks for read that long story and I'm kindly waiting for your thoughts :)
The text was updated successfully, but these errors were encountered: