Reputation: 375
I'm trying to learn Rust, but the only thing I do is keep hitting the wall trying to shoehorn familiar (to me) Java concepts into its type system. Or try to shoehorn Haskell concepts, etc.
I want to write a game with a Player
and many Resource
s. Each Resource
can be owned by one Player
:
struct Player {
points: i32,
}
struct Resource<'a> {
owner: Option<&'a Player>,
}
fn main() {
let mut player = Player { points: 0 };
let mut resources = Vec::new();
resources.push(Resource {
owner: Some(&player),
});
player.points = 30;
}
It doesn't compile, because I can't have the resource point to player, while at the same time modifying it:
error[E0506]: cannot assign to `player.points` because it is borrowed
--> src/main.rs:15:5
|
13 | owner: Some(&player),
| ------ borrow of `player.points` occurs here
14 | });
15 | player.points = 30;
| ^^^^^^^^^^^^^^^^^^ assignment to borrowed `player.points` occurs here
Moreover, if the Resource
owned a mutable reference to Player
, I couldn't even have two Resource
s with the same owner.
What is the Rust way to solve such cases?
I oversimplified my question, and while Shepmaster's answer is a correct answer to it, it's not what I wanted to get (because what I asked was not what I really wanted to ask). I'll try to rephrase it and add more context.
fn addPoints(&mut self, allResources: &ResourcesMap) -> ()
.Problems:
Resource
points to a Player
, I can't modify the player!Resource
s point to Player
because - the natural way to do such an operation would be to start from some of Player A's resources, move through the map to a player's B resource and from that resource to player B to subtract the points. It just doesn't seem natural in Rust (at least for me).
Upvotes: 17
Views: 9798
Reputation: 10916
This is a common question, and Rc<RefCell<T>>
is a common answer. It works fine in small examples, or when you only need a little bit of sharing. But when the state of your program is a graph with cycles, Rc
tends to cause memory leaks, and RefCell
tends to panic at runtime. You lose some of the compile-time correctness that you expect from Rust, and it's also painfully verbose. This is especially a problem in games.
A better approach for games — and for programs with graph/relational state in general — is to use indexes instead of references. Your example might look something like this (I'll avoid assuming that each resource is uniquely referenced by one player, to make the solution more general):
struct Player {
resource_ids: Vec<usize>,
points: i32,
}
struct Resource {
owner_id: usize,
}
struct GameState {
players: Vec<Player>,
resources: Vec<Resource>,
}
fn new_player(state: &mut GameState) -> usize {
state.players.push(Player { points: 0, resource_ids: Vec::new() });
state.players.len() - 1
}
fn new_resource(state: &mut GameState, owner_id: usize) -> usize {
state.resources.push(Resource { owner_id });
let new_id = state.resources.len() - 1;
state.players[owner_id].resource_ids.push(new_id);
state.players[owner_id].points += 30;
new_id
}
fn main() {
let mut state = GameState { players: Vec::new(), resources: Vec::new() };
let player_id = new_player(&mut state);
let resource_id = new_resource(&mut state, player_id);
}
Writing games is a deep topic, and there are many ways we might want to expand this example. Instead of using usize
for everything, we might want different index types for Player
and for Resource
, so we don't get them mixed up. If we need to support deletion, we might want to use HashMap
instead of Vec
. But this is a start. See also Using Rust For Game Development and Object Soup is Made of Indexes.
Upvotes: 4
Reputation: 31253
The cell documentation page has rather good examples. Rust will always try to protect you from doing bad things (like having two mutable references to the same thing). Therefor it's not quite as "easy" as using Rust's built-in references, since you need to do runtime-checking (Rust references are checked at compile-time).
The RefCell
type exists just for that. It checks the mutability rules at runtime. You will get some memory and computation-time overhead, but you end up with the same memory-safety that Rust promises in it's compile-time checks.
Your example ported to RefCell
looks like the following.
use std::cell::RefCell;
struct Player {
points: i32,
}
// the lifetime is still needed to guarantee that Resources
// don't outlive their player
struct Resource<'a> {
owner: &'a RefCell<Player>,
}
impl<'a> Resource<'a> {
fn test(&self) -> i32 {
self.owner.borrow().points
}
}
fn main() {
let player = RefCell::new(Player { points: 0 });
let mut resources = Vec::new();
resources.push(Resource { owner: &player });
player.borrow_mut().points = 30;
println!("{:?}", resources[0].test());
}
My concern is, if what I'm trying to do is trying to write Java code in Rust, can it be done in a Rust-way without sacrificing compile time safety? Avoid that shared mutable state at all?
You are not sacrificing compile-time-safety. Rust makes sure (at compile-time) that you are using your libraries correctly. Still, your program might panic at runtime if you use the borrow*
functions. If you use the try_borrow*
functions instead, you can check if it succeeded and if not, do some fallback operation.
You can also use a reference counted box of a RefCell
to your type (Rc<RefCell<Player>>
). Then you only need to make sure that you do not create cycles, or your memory will never be freed. This would be much more Java like (although Java automatically finds cycles).
Upvotes: 16
Reputation: 431489
Each Resource can be owned by one Player.
Make the types do that then:
struct Player {
points: i32,
resources: Vec<Resource>,
}
struct Resource {
gold: i32,
}
fn main() {
let player1 = Player {
points: 30,
resources: vec![Resource { gold: 54 }],
};
let player2 = Player {
points: 50,
resources: vec![Resource { gold: 99 }],
};
// If you really need an array of all the resources...
// Although this seems like you should just ask the Player to do something
let mut resources: Vec<_> = vec![];
resources.extend(player1.resources.iter());
resources.extend(player2.resources.iter());
}
Edit Thanks to @ziggystar for pointing out my original version allowed players to only have one Resource
. Now players may own N resources, but they still are the only owner of a resource.
Upvotes: 6