17cupsofcoffee / tetra

🎮 A simple 2D game framework written in Rust
MIT License
920 stars 63 forks source link

Help with bound and concrete lifetimes #190

Closed teknico closed 4 years ago

teknico commented 4 years ago

(I could have asked on #games-and-graphics on the Rust Community Discord server but I wanted something more async and less transient.)

I'm half-way through writing a game with Tetra (this is my first Rust project). Everything went fine as long as I made clones; now I'm trying to use references, and have to deal with lifetimes. Every time I try to add them I end up in the same place, this:

error[E0271]: type mismatch resolving `for<'r> <for<'s> fn(&'s mut tetra::context::Context) -> \
std::result::Result<GameState<'s>, tetra::error::TetraError> {GameState::<'_>::new} as \
std::ops::FnOnce<(&'r mut tetra::context::Context,)>>::Output == std::result::Result<_, tetra::error::TetraError>`
   --> src/main.rs:115:10
    |
115 |         .run(GameState::new)
    |          ^^^ expected bound lifetime parameter, found concrete lifetime

error: aborting due to previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0271`.

that I don't quite understand. Places I looked at for explanations:

I'm still none the wiser. Help please? Thanks.

17cupsofcoffee commented 4 years ago

I've honestly never seen this error before, at least in the context of Tetra! But I think that's because I've never actually felt the need to use a lifetime on a State struct before - lifetimes on long-lived structs make it very easy to get yourself tied in knots and I try to avoid them where possible.

Could you give me a bit more info on how exactly you're using lifetimes in your GameState? That way I can hopefully understand your use case/why you might be getting errors 🙂

teknico commented 4 years ago

This is a Pacman-like game (actually a clone of a precursor from 1979, Head On 2 by Sega-Gremlin). The game field is represented as a matrix of ~900 tiles, each one with a set of static properties. The logic in various parts of the code needs those properties, so I was trying to avoid copying the matrix several times, but have just one instance of it.

The GameState struct includes (among other things) a Field and a Car:

struct GameState {
    field: Field,
    car: Car,
    ...
}

The Field struct includes a TilePropsMap:

pub struct Field {
    pub tile_props_map: TilePropsMap,
    ...
}

The Car code has to look at the tile properties, so its struct includes a reference to the TilePropsMap instance:

pub struct Car {
    position: Coords,
    direction: Direction,
    tile_props_map: &TilePropsMap,
}

When the GameState creates the Car, it needs to pass it the reference to the TilePropsMap instance:

        Ok(GameState {
            field: field,
            car_player: Car::new(
                ctx,
                player_position,
                Direction::Right,
                &field.tile_props_map,
            ),
            ...
        })

And that's where rustc starts asking for lifetimes. I'd like to put the TilePropsMap instance some place where I could declare it static, or with a 'static lifetime, or both, but I'm not sure how.

rjframe commented 4 years ago

@teknico You could use reference-counting (the book):

use std::rc::Rc;

struct GameState {
    field: Field,
    car: Car,
}

struct TileMap;

struct Field {
    map: Rc<TileMap>
}

struct Car {
    map: Rc<TileMap>
}

fn main() {
    let map = Rc::new(TileMap);

    let g = GameState {
        field: Field { map: Rc::clone(&map) },
        car: Car { map: Rc::clone(&map) }
    };
}

Or probably better, the TilePropsMap could hold an Rc to its data (tetra does this with some objects, like Texture).

Or pass the TilePropsMap reference into the functions that need it, as long as the caller has access to it:

impl Car {
    fn move(&mut self, map: &TilePropsMap) {}
}

// Or free function:
fn move_car(car: &mut Car, map: &TilePropsMap) {}
17cupsofcoffee commented 4 years ago

In your original example, you've created a struct that contains a reference to its own memory - this is something that can't be done in safe Rust without some fairly heavy constraints and complex code, and you're usually better off restructuring your code to avoid it.

To understand why, think about what would happen when you move your GameState from your code into Context::run - your struct has moved, but the references contained within would still be pointing at the old location, which isn't allowed in Rust. This is probably why you're getting some fairly horrendous error messages!

I would pretty much concur with the suggestions that @rjframe has made - the best way to think about it is in terms of 'who owns the data':

teknico commented 4 years ago

In your original example, you've created a struct that contains a reference to its own memory

IIUC you're referring to my GameState containing both a Field and a Car, and one of the Car fields is a reference to one of the Field fields, and that makes the GameState instance unmovable.

If one struct is the clear owner of the data, and another struct/function just uses that data, then pass it around.

So Car cannot have that reference as a field, and I need to pass the TilePropsMap in whenever I call one of the Car methods that needs it.

If multiple structs 'own' a piece of data, then you may want to use Rc.

Otherwise if I really want to keep that reference I need to use Rc.

I think I get it. Thanks a lot to both of you, @rjframe and @17cupsofcoffee!