I want to have a struct named Outcome, which holds references to entities. I then want to find the entity that it points to, borrow it mutably and change it according to an effect from Outcome. My code now looks like this
fn main() {
let mut entities = vec![
Entity {
name: "George".to_string(),
current_hp: 200.0,
damage: 10.0,
},
Entity {
name: "Jacob".to_string(),
current_hp: 100.0,
damage: 5.0,
},
];
let outcome = Outcome {
caster: &entities[0],
target: &entities[1],
effect: Effect::Damage(entities[0].damage),
};
match outcome.effect {
Effect::Damage(amount) => {
outcome.target.current_hp -= amount;
}
}
}
This of course doesn't work, as I am trying to modify an immutable reference. Can I somehow convert an immutable reference to a mutable reference when I have the mutable vector in scope? Or is there perhaps a more rusty way to solve this issue?
(for info, Outcome is a struct returned by a function, which i pass the immutable references to, and it returns them back with an effect).
The only viable solution I found would be changing the immutable reference in an unsafe block like this
match outcome.effect {
Effect::Damage(amount) => unsafe {
let target = outcome.target as *const Entity as *mut Entity;
(*target).current_hp -= amount;
},
}
You can use indices into the mutable vector:
fn main() {
let mut entities = vec![
Entity {
name: "George".to_string(),
current_hp: 200.0,
damage: 10.0,
},
Entity {
name: "Jacob".to_string(),
current_hp: 100.0,
damage: 5.0,
},
];
let outcome = Outcome {
caster: 0,
target: 1,
effect: Effect::Damage(entities[0].damage),
};
match outcome.effect {
Effect::Damage(amount) => {
entities[outcome.target].current_hp -= amount;
}
}
}
This requires a certain discipline: you obviously can't reorder the vector, or the indices will also become invalid. To support despawning entities without shifting elements, you can wrap them in an Option, and despawning an entity sets its slot to None
. Spawning an entity would entail searching the vector for a free slot (aka a None
), or appending a Some(entity)
to the end of the vector to create a new slot if there are no free slots.
This leads to a subtle issue: in between an index being generated and used, the entity it pointed to can die and the slot can be reused for a different entity, causing the effect to be applied to the wrong entity. This can be fixed using a generational index, where an entity index is actually a tuple (index, generation)
. Each slot in the vector becomes (usize /*generation*/, Option<Entity>)
, and when an entity is spawned in a re-used slot it increments the generation of the slot. Accessing the entity with an index (now via a getter method) would check the generation of the index against the generation of the slot, and return None
if the generations don't match.
Once you've implemented all that, you've gotten part of the way towards an Entity-Component System like Bevy's ECS, Hecs, Legion, or other libraries that are popular in the Rust gamedev ecosystem because they solve exactly these issues :-)