I'm trying to make a small game in Rust. I want to use something similar to the entity-component-system pattern to handle all the game objects.
My general idea is to have a GameLoop
struct which holds all the necessary data to update and draw the game (the screen, a timestamp, ...).
The World
struct is supposed to hold all the game entities and update them in the dispatch
function. It calls all the registered callbacks which are stored in the World
struct as well (those are the "systems"). They're redundant in the sample code, though.
I've tried to break down the code as much as possible and only include the relevant parts.
use std::marker::PhantomData;
struct Something;
///The "somethings" are things like the display, a timestamp, ...
struct GameLoop {
sth: Something,
sth2: Something,
}
///C = Context
///The type that is passed to every entity to give it access to things like the delta time
struct World<C> {
phantom: PhantomData<C>, //This is here so Rust doesn't complain about the type parameter not being used
}
///The data that is passed to the system functions every frame
struct TickData<'a> {
delta: u64,
sth: &'a Something,
sth2: &'a mut Something,
}
impl GameLoop {
fn new() -> GameLoop {
GameLoop {
sth: Something {},
sth2: Something {},
}
}
///One game "tick" - Supposed to do things like calculating delta time, swapping buffers, ...
///Those are then passed to the callback
fn update<F: FnMut(u64, &Something, &mut Something)>(&mut self, f: &mut F) {
f(0, &self.sth, &mut self.sth2);
}
}
impl<C> World<C> {
fn new() -> World<C> {
World { phantom: PhantomData }
}
///Supposed to update all the game entities
fn dispatch(&mut self, context: &mut C) {
//...
}
}
impl<'a> TickData<'a> {
fn new<'b>(delta: u64, sth: &'b Something, sth2: &'b mut Something) -> TickData<'b> {
TickData {
delta: delta,
sth: sth,
sth2: sth2,
}
}
}
fn main() {
let mut game_loop = GameLoop::new();
let mut world = World::<TickData>::new();
//The game update function, called once per frame
let mut update_fnc = |delta: u64, sth: &Something, sth2: &mut Something| {
let mut tick_data = TickData::new(delta, sth, sth2);
&world.dispatch(&mut tick_data); //If this line is commented out, it compiles fine
//...
};
loop {
game_loop.update(&mut update_fnc); //Calculate the delta time, call the specified function and swap buffers
}
}
There seems to be a problem with borrowing / lifetimes. The compiler is everything else but verbose.
The problem seems to be the &world.dispatch(&mut tick_data)
call in the update function of the game, which is supposed to update all the game entities. If I comment it out the program compiles without any errors.
This is what the compiler tells me:
error[E0495]: cannot infer an appropriate lifetime for lifetime parameter 'b in function call due to conflicting requirements
--> src/main.rs:66:29
|
66 | let mut tick_data = TickData::new(delta, sth, sth2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the body at 65:77...
--> src/main.rs:65:78
|
65 | let mut update_fnc = |delta: u64, sth: &Something, sth2: &mut Something| {
| ______________________________________________________________________________^ starting here...
66 | | let mut tick_data = TickData::new(delta, sth, sth2);
67 | |
68 | | &world.dispatch(&mut tick_data); //If this line is commented out, it compiles fine
69 | |
70 | | //...
71 | | };
| |_____^ ...ending here
note: ...so that reference does not outlive borrowed content
--> src/main.rs:66:55
|
66 | let mut tick_data = TickData::new(delta, sth, sth2);
| ^^^^
note: but, the lifetime must be valid for the expression at 74:25...
--> src/main.rs:74:26
|
74 | game_loop.update(&mut update_fnc); //Calculate the delta time, call the specified function and swap buffers
| ^^^^^^^^^^^^^^^
note: ...so that reference is valid at the time of borrow
--> src/main.rs:74:26
|
74 | game_loop.update(&mut update_fnc); //Calculate the delta time, call the specified function and swap buffers
| ^^^^^^^^^^^^^^^
I simply can't spot the cause of the error. The functions get called in a kind of procedural way and since I'm only borrowing most of the data there should be no problem with the lifetimes.
When I remove the references from the TickData
struct so it only contains values that are implemented for the Copy
trait, it works as well.
I'm usually not the kind of person to post a wall of code and ask people to fix it but I'm really clueless right now.
There's no one right solution to your code. It appears overly complicated and I don't know why you've made some of the design decisions you have. If nothing I say applies, then I apologize and you'll have to wait for the next answerer.
Reducing your problem is the right idea, but why did you stop? It can be reduced all the way down to
By trial-and-error, one can find some "solutions":
or
or
For an extremely thorough analysis of this problem, better than I can give, check out Why does linking lifetimes matter only with mutable references?.
Let's look at another aspect, back at your main method:
We know that
TickData
has a lifetime in it, so what is it in this case? We can't specify it like a type, it must be inferred from the usage. So where is it used?One analogy to look to is a
Vec
. We create aVec
and thenpush
things onto it later. Thosepush
es tell us what the concrete type ofT
is. What does your code do:You create a type that you've said contains a
TickData
(that's whatPhantomData
does), then you call a method that "push"es that type (fn dispatch(&mut self, context: &mut C)
), therefore the second argument must be of the type contained, resolving the final type.This leads to another problem: there's no clue how long the lifetimes of those arguments are.
However, simply annotating the lifetimes isn't enough:
This further complication is because we are passing the mutable reference
sth2
todispatch
. The definition ofdispatch
allows it to store that mutable reference inside itself - the lifetimes and types match and it's a&mut self
.This could lead to multiple mutable aliases, which is disallowed.
I don't know why you've parameterized your
World
, but you might be able to just move theC
to thedispatch
method, removingPhantomData
completely. This removes the ability forWorld
to storeC
.