Skip to content

Commands apply at unexpected times with exclusive World access #14621

@jrobsonchase

Description

@jrobsonchase

Bevy version

0.14.0 and main (c1c003d)

What you did

world.commands().add(...) from within another command. Take the following example:

let mut world = World::new();

let mut cmd = world.commands();

// Command 1
cmd.add(|world: &mut World| {
    let entity_1 = world.spawn_empty().id();

    println!("A");

    // Command 3
    world.commands().add(|world: &mut World| {
        println!("BUZZ!");
    });

    println!("B");

    world.entity_mut(entity_1).insert(A);

    let a = world.get::<A>(entity_1);

    println!("C");

    world.spawn(A);

    println!("D");

    world.flush();

    println!("E");
});

// Command 2
cmd.add(|_: &mut World| {
    println!("F");
});

world.flush();

When does BUZZ! get printed?

What went wrong

It ends up being in the place I would least expect, which is why I'm calling this a bug rather than simply lacking documentation.

Loosely ordered by where I would expect it to occur:

  • After F? If the &mut World passed to each command is the same world as the outermost, then I would expect world.commands() to always refer to the same queue, so Command 1 is added, Command 2 is added, Command 1 runs, it pushes Command 3 into the queue, Command 2 runs, Command 3 runs.
  • After D or E? If each command gets a new "nested" queue that's flushed after it finishes, then it would happen between Command 1 and Command 2. But we're also calling world.flush(), and the docs for world.commands() says they'll run after a call to World::flush. So it should be after D.
  • After A? We have exclusive World access, so Commands are a bit redundant. If we ignore the bit about World::flush in the World::commands docs, then maybe we'll guess that they simply execute immediately, and World::commands is just for convenience when you have something that accepts a Commands argument. Or maybe they flush when the Commands is immediately dropped after our add?
  • After B? Maybe there's an implicit World::flush any time we actually mutate the world or when we read the result of a mutation.
  • After C? If it's none of the above, this is the only option left, apart from "never".

And the winner is... after C! It comes down to World::flush being implicit in some methods, but not others. There isn't any mention of this in any of their docs. Even if it was called out explicitly in the docs for each method if it does a flush, I wouldn't want to have to keep that context in my head when reasoning about when a command will run.

To me, the only three possibilities for when a command should run given exclusive world access are:

  1. At the end of the outermost command queue
  2. Immediately after the parent command
  3. Immediately after being added, or at least before any other world access. No explicit flushing or "will they/won't they" implicit flushing.

Additional information

This comes out of my attempts to get lifecycle Observers to behave the way I want, and they make things even harder to reason about. Specifically, the OnAdd trigger will occur immediately upon component addition, but if you add commands to the queue in the trigger, they'll be executed whenever the command that did the component addition does a flush, which can be wildly unpredictable. For example, what if the command pre-spawns a bunch of entities, then adds components? Your observer's commands will run at the end of that command after all components have been added. What if instead, it interleaves entity spawning and component adding? Then you'll get a flush before each new entity, and your observer's commands will occur midway through the command that triggered it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-BugAn unexpected or incorrect behaviorC-UsabilityA targeted quality-of-life change that makes Bevy easier to useS-Needs-DesignThis issue requires design work to think about how it would best be accomplishedX-Needs-SMEThis type of work requires an SME to approve it.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions