Confusing and way too verbose lifetime annotation. Any way to simpify this?

I was writing a basic physics engine. The collison detection and response part was very redundant in terms of code and data, so I put them in a struct to reuse them. The problem is, I'm using a ECS game engine bevy, which utilizes a lot of generic. So I ended up with this:

struct Collider<'a, 'b: 'a, 'c: 'a, 'e, 'f, 'l> {
    translation: &'a mut Vec3,
    collision_box: &'a CollisionBox,
    hori_dis: f32,
    vert_dis: f32,
    map_query: &'a mut MapQuery<'b, 'c>,     // From bevy plugin bevy_ecs_map
    tile_query: &'a Query<'e, 'f, &'l Tile>,     // From bevy engine
    /* snip */
}

impl<'a, 'b: 'a, 'c: 'a,  'e: 'a, 'f: 'a, 'l: 'a + 'f> Collider<'a, 'b, 'c, 'e, 'f, 'l>
{ /* snip */ }

Query's documentation and signature: Query in bevy::ecs::prelude - Rust
The &'a Query<'e, 'f, &'l Tile> is the most confusing part. It works but TBH I have no idea how. I just sat in front of my computer, searching on the web to no avail, and changing the lifetime parameters randomly for over an hour. Messages from the compiler were cryptic, like data flow from tile_query to tile_query, which was the same variable.
Another confusing point is how lifetime of generic type is inferred. For example:

struct S<T> {
    t: T,
}
struct Wrong<'a> {
    ref_to_s: &'a S<&i32>,
}
struct Correct<'a> {
    ref_to_s: &'a S<&'a i32>,
}

There is no lifetime parameter in S's signature, but somehow the named lifetime parameter in Correct is required. And if the compiler can figure out what lifetime it should be, what's the point of annotating it? In the Collider example above, I tried to use _ for lifetime but got denied by the compiler. Why should the programmer writing Collider care what's inside Query, when the ligitimacy of &Tile is enforced within Query anyway? Can I assume that as long as tile_query: &Query is alive, *tile_query is alive, and the data refered by *tile_query should be alive?

I tried to look for some information from the Book, Rustonomicon and Rust by Examples, but didn't find anything about such generic reference where the lifetime is hidden. And I still don't know how my code worked. Is there some more ergonomic and less confusing way to write such struct?

80% of the time I tried to write the ', it's with great pain and waste of time. It would be a great relief to me if someone could generously share the "correct" way doing this.

Why are you putting the map_query and tile_query in a struct containing collider data? Have you tried a design like

#[derive(Component)]
struct Collider {
    translation: Vec3,
    collision_box: CollisionBox,
    hori_dis: f32,
    vert_dis: f32,
}

and then have whichever system you use that processes colliders query for a Collider and separately a map_query and tile_query?

1 Like

Also you may get more help on the official Bevy discord:

The Collider doesn't actually store any sate, it's a temporary struct for avoiding code and data redundancy. The system I use has the signature below:

fn move_things(
    mut query: Query<(
        &mut VerticalVelocity,
        &mut HorizontalVelocity,
        &mut Transform,
        &CollisionBox,
    )>,
    time: Res<Time>,
    mut map_query: MapQuery,
    tile_query: Query<&Tile>,
) {/* snip */}

At first I put my logic into functions, but they need map_query and tile_query to access the map. Their parameters were like:

translation: &mut Vec3,
collision_box: &CollisionBox,
hori_dis: f32,
vert_dis: f32,
map_query: &mut MapQuery,
tile_query: &Query<&Tile>,
edge_tile_x: u32,
edge_tile_y: u32,
left: u32,
right: u32,
bottom: u32,
top: u32,
/* snip */

So I put them into a struct to avoid too many parameters. This is one of the functions I used, and there were many of them:

x_collided = collide_hori(
    collision_box,
    &mut new_x,
    &mut left,
    &mut right,
    &bottom,
    &top,
    hori_direct,
    map_query,
    tile_query,
);

And the struct can store some temporary variables, which in my earlier functional implementation were calculated again and again, leading to much boilerplate (the function call statement was pretty long as you can see) and waste of resources.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.