Avoiding Rc everywhere

Hi, coming from the GC land, I can't help but see Rc as a solution to a lot of my problems.
I'm writing a game server and it has a lot of inter-connected parts, so it's references are a giant messy web. There obviously must be a better way than this.

My code:

struct Server {
	// players in the whole server
	// here I can't see any other way than using Rc because both hashmaps need the player but can't own it.
	players_by_name: HashMap<String, Rc<RefCell<Player>>,
	players_by_uuid: HashMap<Uuid, Rc<RefCell<Player>>,
	worlds: Vec<Rc<RefCell<World>>>,
}

struct World {
	// players in this specific world..
	players_by_name: HashMap<String, Rc<RefCell<Player>>,
	players_by_uuid: HashMap<Uuid, Rc<RefCell<Player>>,
}

struct Player {
	// player needs to know what world it's in. Reference cycle?
	world: Rc<RefCell<World>>,
}

Rc's everywhere. This would be the natural way to do this in a GC's language. What would be the best way to do this in Rust?

3 Likes

This is a great topic.

There are probably several ways to design this. A few ideas/thoughts offhand:

  1. Server owns the Player instances, but creates cheap (i.e. copyable) ids for them. Other data structures store the id. One particular hazard here is an id may refer to a player that's no longer in the Server (if the code that logs out a player doesn't notify all the right places).
  2. Players move out of Server into a World once they enter the world. It looks like a player can only be in a single world at a time. Listing all players on the server could involve iterating over all worlds, and getting the players from there - if there aren't that many worlds and this isn't a particularly hot function, it might be fine.
  3. Worlds can have ids as well, and so the same approach applies to Player knowing the world it's in.
  4. If data flow can be made unidirectional, then some of the backreferences can go away. For example, if the World "drives" a Player entirely, then Player should presumably not need to know anything about the World it's in - the World would interact with the Player purely through Player's API. But, this is hard to speculate about without knowing more about how the game interaction flows.

I agree that a GC'd language makes this setup fairly trivial. However, there's significant cognitive overhead associated with complex interaction graphs - simplifying the dataflow, even in a GC'd runtime, helps with that. Rust may steer you in that directly a bit more forcefully.

Also worth mentioning that you can use Weak with Rc to maintain weak refs.

Finally, entity component system (ECS)/data oriented designs are quite popular in some parts of the gaming world, rather than "object oriented" ones. If you're unfamiliar with this, it might be an interesting area to look into; there are some ECS frameworks in Rust, although I can't speak to them intelligently.

5 Likes

Thank you for the reply <3

Moving players to worlds seem like a pretty good idea. I might be able to make data flow unidirectional that way as well, I just need to pass more parameters to functions, but I guess it’s worth it.

1 Like