dubble is a crate which provides a generic implementation of double-buffering.
Typically in a game engine, there's a core loop which updates everything in the game world each frame. However, this creates a small paradox. Let's say the position of object B is dependent on the position of object A, but the position of A is also somewhat dependent on the position of B. For example, consider a simulation of a solar system, where gravitational forces between A and B affect the motion of each other.
How do we decide which position to update first in the core game loop? The final positions of A and B are going to depend on the order in which the objects are updated.
Double-buffering is a solution to this problem. We store two versions of A and B. One version is for reading, and the other is for writing. When we update the position of A, we read from the reading version of B, and store the new position in the writing version of A. Likewise, when we update the position of B, we read from the reading version of A and store the new position in the writing version of B.
Finally, before the next iteration of the core game loop, the reading versions of both A and B are updated with the values in their corresponding writing versions.
Note that, when the position of A was updated, the reading version of A was not changed, only the writing version. Likewise, when the position of B was updated, we used the reading version of A, which was left unchanged when the position of A was updated. Notice that now, the order in which things are updated doesn't matter, since they both used the "old" version of the things that they depended on.
This technique is called double-buffering, since it required two copies (buffers) of the thing in question.
The most basic initialiser is
new(), which initialises the read and write versions of the object being double-buffered.
use dubble::DoubleBuffered; // assuming you have a `Player` struct let mut player = DoubleBuffered::new(Player::new());
construct_with(), which accepts any
Fn() -> T and uses that to construct the read and write versions.
let mut player = DoubleBuffered::construct_with(Player::new);
DoubleBuffered also implements
Default as long as the wrapped type does, so you could also do
let mut player = DoubleBuffered::<Player>::default();
write() are the basic methods which return references to the read and write versions respectively. These references are immutable/mutable respectively.
// reading // read() -> &T player.read().attack(monster); // writing // write() -> &mut T player.write().dmg_hp(10.0);
But to make things easier, the buffer also implements
DerefMut<Target=T>, so the
write() calls can be omitted. The trait implementations are simply wrappers around
update() will clone the write version onto the read version.
let mut player = DoubleBuffered::construct_with(Player::new); // player starts with 100 hp assert!(player.health == 100); // do some damage to the player player.dmg_hp(10); // before the call to `update()`, the health will not have changed assert!(player.health == 100); // ... update other stuff ... // now update the player's health player.update(); assert!(player.health == 90);
IndexMut so long as the wrapped type does, so you can use a
DoubleBuffered<Vec<Actor>> and update everything in one call to
let mut actors = DoubleBuffered::construct_with(Vec::<Actor>::new); actors.push(the_player); actors.push(monster_1); actors.push(monster_2); actors.push(monster_3); // ... player and monsters update based on reads of the other's state ... // update everyone actors.update();
Although the double-buffer itself can be sent across threads (it is
Sync so long as
T is as well), the contents cannot be accessed by other threads without violating aliasing and mutability rules. Solution here for now is to wrap this type in a