Supporting update() method in a game


#1

How do you write games in Rust?

In other languages, it’s pretty common to have objects representing things in a game - Player, Item, Rocket - and have them all implement a common interface with an update() method. rocket.update() asks a rocket to advance x pixels along its trajectory, test for collisions, etc.

In Rust:

trait Update {
    fn update(&mut self, &mut World);
}

impl Update for Monster { ... }
impl Update for Rocket { ... }

This doesn’t work, though, because all the update-able things, the monsters, the rockets, are actually part of the World. We can’t have both &mut self and &mut World at the same time.

I’ll post my workarounds below but they all seem painful - is there a better way?


#2

Some possible workarounds:

  • use cells for everything in the world model, so you can pass non-mut references to update() and still mutate things

  • always move an object out of the World before calling the update method, then put it back afterwards

    (this is not great, because asking the world model simple questions like “is this elevator overloaded?” will get bogus results if you’ve temporarily banished one of the occupants from the universe)

  • use immutable data and make update() return a new World object

  • use non-mut references and make update() save all side effects until the end, then return them as a value (or closure) for the game’s simulation loop to apply (this is kinda monadic if you squint)


#3

I personally used work-around #1 in the past. I then switched to work-around #4 when one thread was no longer enough.


#4

I’ve thought of one more technique that I’ve seen used:

  • For objects which have simple or related behavior, don’t do per-object update(). Instead, have a single function that updates them all at once. This can be clearer and faster, too.

#5

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.