-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Commands should be infallible whenever possible #10166
Comments
In my opinion this is not a fact but an opinion. For me, it's a bug if a system tries to act on an entity that was despawned by another. It can most of the times be ignored without consequences, but it's still a bug. |
|
Could this maybe be an issue of naming more than anything else? I'm not necessarily saying commands like |
The "bug" is not necessarily in the double despawn, it's in the conditions that lead to it. If an entity is despawned at the same time:
in the end, the entity is despawned, and from the ECS point of view we don't care anymore about it. But there's an issue between those two systems, and we need a way to give that information back. |
I'll speak objectively then.
Is there an alternative "don't panic" that I'm not seeing? Many users have legitimate issues with these panics, all of us have a track record of choosing to reduce noisy things (ambiguities, #9822, #10145, etc.), and if apps continue to grow in complexity and number of plugins, then IMO it's inevitable users will encounter an increasing number of "false positives" here. |
Don't panic and have a proper error management. I'm ok with not panicking if we have a way to handle this from inside the app, I would actually prefer that to panics. Panics are used as the "easy" error management due to the asynchronous nature of commands. There was a tentative in #2241 to add error handling. |
Could we setup an impl Command for Despawn {
fn apply(self, world: &mut World) {
if !world.despawn(self.entity) {
world.send_event(CommandDespawnErrorEvent(self.entity));
}
}
} Then a (possibly default) system could listen to those events and choose how to handle them? This would lean towards the "commands should never panic" side of this debate. |
An event per command would be very hard to use without a way to link back to the context of the command creation, or at least the system. I like:
|
You could link back to the system by inserting the |
IIRC @HackerFoo also mentioned he did this for his personal fork of Bevy at one point |
How many use cases are there for needing to handle a duplicate despawn? To be clear I'm not opposed to having a more robust solution for handling fallible commands if it doesn't come with a performance regression but I think panic-ing is definitely the wrong behaviour. I find it very easy to imagine cases where it's undesirable: Similarly I could imagine an entity being despawned because it's too old and also out of bounds or some other combination of factors. You might not even see the panic the majority of the time and hit it unexpectedly when you hit the edge case. I feel that when running systems in an ECS you are already operating on a snapshot of the world from the last time commands where applied. You might be modifying a component that an entity already has a remove command queued for (even if that is unlikely in practice). If multiple systems agree that an entity should be despawned I don't see that as an error at all. IMO I agree with @maniwani that there should be no guarantees about the state of the world when you run a command. An entity might not have the component you are trying to remove, it might already have the one you are trying to insert. It might already be despawned when you try to despawn it. I could be convinced otherwise if I was shown how this was restricting some extremely desirable feature but I don't think that's the case at the moment. These assumptions make batching and applying commands much simpler as well as being a simpler mental model. |
I have similar observations. We use bevy to visualization large chunks of data with complex multi-level parent-children structures. In the app we have few systems for synchronizing and checking data related to an external source. When the parent intends to recursively despawn, some of systems and events may still handle children entities in the current or next one tick. Currently, to avoid this problem, we need to ensure that the systems are in the right order. Which still does not guarantee work without panic. It's easy to make a mistake and unnecessarily complicates the project. |
# Objective avoid panics from `calculate_bounds` systems if entities are despawned in PostUpdate. there's a running general discussion (#10166) about command panicking. in the meantime we may as well fix up some cases where it's clear a failure to insert is safe. ## Solution change `.insert(aabb)` to `.try_insert(aabb)`
# Objective avoid panics from `calculate_bounds` systems if entities are despawned in PostUpdate. there's a running general discussion (#10166) about command panicking. in the meantime we may as well fix up some cases where it's clear a failure to insert is safe. ## Solution change `.insert(aabb)` to `.try_insert(aabb)`
# Objective avoid panics from `calculate_bounds` systems if entities are despawned in PostUpdate. there's a running general discussion (bevyengine#10166) about command panicking. in the meantime we may as well fix up some cases where it's clear a failure to insert is safe. ## Solution change `.insert(aabb)` to `.try_insert(aabb)`
In support of what's already been said, I think this is clearly a bevy bug. It causes sporadic hard-to-debug crashes for correct user logic like despawning an entity when its lifetime expires as well as when it exceeds some despawn radius, and having that rarely occur on the same frame. EDIT: It seems double despawn is a warning now instead of a panic, which is better. I'm not sure which version changed it. There are examples for other combinations of commands that still panic like despawn + insert anyways. Every game will run into this bug eventually, so every game has to reinvent the same 40-line Additionally, command error handling is something I've never felt the need for. In the vast majority of cases, I want command failure to be ignored. But even in the minority of cases where some command failure is actually causing a bug, I'd want debug logs to identify and fix the logic / scheduling bug in my code, not an error handling system to somehow "handle the command failure gracefully" on the fly. |
It might not be the most ecs-friendly idea, but can't we just have an optional callback for cases when error handling is neccesary? let some_context = todo!()
let entity_commands = todo!()
// no panic, silent failure (warn!)
entity_commands.insert_new(Transform);
// no panic, failure is handled in the closure
entity_commands.insert_new(Transform).on_error(move |world| {
println!("failure: {some_context}");
}); Pros:
Cons:
PS: instead of the builder we could just add more arguments to pub struct Insert<T: Bundle> {
entity: Entity,
bundle: T,
on_error: Option<Box<dyn FnOnce(&mut World) + Send + 'static>>,
}
impl<T: Bundle> Command for Insert<T> {
fn apply(self, world: &mut World) {
if let Some(mut entity) = world.get_entity_mut(self.entity) {
entity.insert(self.bundle);
} else if let Some(on_error) = self.on_error {
on_error(world);
} else {
warn!("Could not insert a bundle (of type `{}`) for entity {:?}", std::any::type_name::<T>(), self.entity);
}
}
}
pub struct InsertBuilder<'a, 'e, T: Bundle> {
command: ManuallyDrop<Insert<T>>,
commands: &'a mut EntityCommands<'e>,
}
impl<'a, 'e, T: Bundle> InsertBuilder<'a, 'e, T> {
pub fn on_error(mut self, f: impl FnOnce(&mut World) + Send + 'static) -> Self {
self.command.on_error = Some(Box::new(f));
self
}
}
impl<'a, 'e, T: Bundle> Drop for InsertBuilder<'a, 'e, T> {
fn drop(&mut self) {
// SAFETY: we always take command only during drop
let command = unsafe { ManuallyDrop::take(&mut self.command) };
self.commands.add(command);
}
}
impl<'e> EntityCommands<'e> {
pub fn insert_new<'a, T: Bundle>(&'a mut self, bundle: T) -> InsertBuilder<'a, 'e, T> {
let command = Insert {
bundle,
entity: self.entity,
on_error: None,
};
InsertBuilder {
commands: self,
command: ManuallyDrop::new(command),
}
}
} |
Just wanted to add that I also support making all commands infallible. I'm already having to roll for updated_entity in updates {
// some descendants may be in the updates query
updated_entity.despawn_descendants();
// may spawn new children and/or insert components
apply_update(updated_entity);
} this creates issues because an early entity in the query might despawn several entities further ahead in the query, causing them to not exist when their commands are processed, crashing the game. One solution would be to build up a set of every despawned entity to then skip them later in the loop, but that seems suboptimal both performance-wise and code-cleanliness-wise. |
Originally posted by @maniwani in #10154 (comment).
Having seen the problems that can arise, I think I generally agree. It's very hard to get a usable error handling solution (#2004, #2241) without severely compromising performance. And in any case, panicking is a very bad default, both for beginners (frustration), prototyping (wasted time) and production games (crashes are very bad).
Steps to fix
try_insert
methoddebug
warning in commandsThe text was updated successfully, but these errors were encountered: