The technique I’d gravitate to is a variation on 3: Get 2 worlds, reading from one and writing to the other, and at the end of each frame the new world becomes the current world.
fn update(&self, world_in: &World, world_out: &mut World)
self
is part of world_in
but not part of world_out
, which should satisfy the borrow-checker. If one is altering world objects other than self
, that update
will require a method that takes an object from world_in
and returns an Option
holding the corresponding object in world_out
, if it exists.
In this method each call to update
reads from the same immutable state, which rules out all order-of-update surprises*, while creating at most 1 new World
per update cycle. Note that method 3 does not rule out order-of-update surprises because each subsequent call to update
is reading from a different World
.
* Suppose that in a frame, object a: A
is going to generate an object b: B
. In that same frame, object c: C
will remove a
from the world. The update loop updates all objects of type A
, then all objects of type B
, then all objects of type C
. Therefore in this one frame: a
creates b
, b
executes its first frame of action, and then c
removes a
.
What if the order is reversed? In that case, c
removes a
and b
will not be created. If the update order is B
, A
, C
, then b
will be created but will not execute its first update until the next frame. This kind of same-frame interaction can make surprises which are difficult to reproduce.