Mutably accessing sister objects from within object in hashmap

I'm currently experimenting on a game in Rust and am having an issue working out the best way to handle the tiles. My end goal is that all tiles in a world will have an update method, and are able to modify and access other tiles in the same world, ideally as well as other properties of the "world", all tiles should be able to create, remove and update other tiles.

I've put together some sample code quickly which should hopefully show how I'd like it to work, is there a way that I can achieve what I'm looking for? Apologies for the mistakes in the code, I threw it together quickly.

// Tile needs to be able to modify itself during an update tick, as well as 
// other tiles stored in the same world.

use std::rc::Rc;
use std::cell::*;
use std::collections::HashMap;

struct Tile {
    counter: i32,
    pos: (i32, i32)
}

impl Tile {
    pub fn new(x: i32, y: i32) -> Tile {
        Tile {
            counter: 0,
            pos: (x, y)
        }
    }
    
    pub fn update(&mut self, world: RefCell<World>) {
        let mut w = world.borrow_mut();
        if w.get_tile_at(self.pos.0 + 1, self.pos.1).is_some() {
            w.destroy_tile(self.pos.0 + 1, self.pos.1);
            self.counter += 1;
        }
    }
}

struct World {
    tiles: HashMap<(i32, i32), Rc<RefCell<Tile>>>
}

impl World {
    pub fn new() -> World {
        World {
            tiles: HashMap::new()
        }
    }
    pub fn set_tile_at(&mut self, x: i32, y: i32, tile: Tile) {
        self.tiles.insert((x, y), Rc::new(RefCell::new(tile)));
    }
    pub fn get_tile_at(&self, x: i32, y: i32) -> Option<RefMut<Tile>> {
        match self.tiles.get(&(x, y)) {
            Some(t_ref) => Some(t_ref.borrow_mut()),
            _ => None
        }
    }
    
    pub fn destroy_tile(&mut self, x: i32, y: i32) {
        self.tiles.remove(&(x, y));
    }
}

struct App {
    world: RefCell<World>
}

impl App {
    pub fn new() -> App {
        App {
            world: RefCell::new(World::new())
        }
    }
    
    pub fn update(&mut self) {
        let mut tile = self.world.borrow().get_tile_at(0, 0).unwrap();
        tile.update(self.world);
    }
}

fn main() {
    let mut app = App::new();
    {
        let mut world = app.world.borrow_mut();
        world.set_tile_at(0, 0, Tile::new(0, 0));
        world.set_tile_at(1, 0, Tile::new(1, 0));
    }

    app.update();
    
    {
        let world = app.world.borrow();
        let t1 = world.get_tile_at(1, 0);
        assert!(t1.is_none());
    
        let t0 = world.get_tile_at(0, 0);
        assert!(t0.is_some());
        assert!(t0.unwrap().counter == 1);
    }
    
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:67:24
   |
67 |         let mut tile = self.world.borrow().get_tile_at(0, 0).unwrap();
   |                        ^^^^^^^^^^^^^^^^^^^                           - temporary value is freed at the end of this statement
   |                        |
   |                        creates a temporary which is freed while still in use
68 |         tile.update(self.world);
   |         ---- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

error[E0507]: cannot move out of borrowed content
  --> src/main.rs:68:21
   |
68 |         tile.update(self.world);
   |                     ^^^^^^^^^^ cannot move out of borrowed content

error[E0505]: cannot move out of `self.world` because it is borrowed
  --> src/main.rs:68:21
   |
67 |         let mut tile = self.world.borrow().get_tile_at(0, 0).unwrap();
   |                        ---------- borrow of `self.world` occurs here
68 |         tile.update(self.world);
   |                     ^^^^^^^^^^ move out of `self.world` occurs here
69 |     }
   |     - borrow might be used here, when `tile` is dropped and runs the destructor for type `std::cell::RefMut<'_, Tile>`

error: aborting due to 3 previous errors

Some errors have detailed explanations: E0505, E0507, E0716.
For more information about an error, try `rustc --explain E0505`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.

I would lift the update functionality from Tile to World, leaving Tile as basically just data. Something like this:

impl World {
    pub fn update_all(&mut self) {
        for location in self.tiles.keys() {
            self.update_tile(location)
        }
    }

    pub fn update_tile(&mut self, location: (i32, i32)) {
        ...
    }
}

(code not tested)

That way the code that might touch the whole world is associated with the world, rather than the tile. This way, you won't need any RefCells or anything.

1 Like

While this is possible at the moment, this could very quickly get messy and I'd like to avoid this if possible, would there be any other way to achieve this?

I'm using a system not too dissimilar from this in a project I'm working on now. My approach was to separate deciding what action to take from applying the action to the world. The way I would apply this in your case is something like this:

  • There is an enum Action that represents possible actions that tiles can take. For example, Action::Move(3, 4) represents moving to the position (3, 4).
  • Tile has a choose_action(&self, world: &World) -> Action method that decides what action to take. Note this function does not require unique access to either self or the world.
  • World has an apply_action(&mut self, action: Action, x: i32, y: i32) method that applies the effects of the Tile at position (x, y) performing the given Action. This is the only method that requires mutable access to the World.
  • In the game loop, you first request an action from the Tile, and then apply it to the World.

This design unlocks several possibilities; for example, instead of alternating between choose_action and apply_action, you could collect all the Actions first and then reorder them before applying them to the World, which is something you might want to do in a turn-based game (to implement something like player initiative). If apply_action is messy, you can push the logic down into Action and split it among functions however you like.

Point being, there are many ways to skin a cat. You may find that lifting update into World frees you to solve the "messiness" problem in some other way than putting it into Tile. In my case, I did something like Action (my other internals are slightly different). But there might be another good way to make World::update non-messy that doesn't involve multiple mutable references.

This is definitely a good option, thanks.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.