Make LevelSelection Follow Player

In games with GridVania/Free world layouts, it is common to make the player "worldly" and have them traverse levels freely. This level traversal requires levels to be spawned as/before the Player traverses to them, and for levels to be despawned as the player traverses away from them.

This guide demonstrates one strategy for managing levels like this: having the LevelSelection follow the player entity. This code comes from the collectathon cargo example.

Use world translation for levels and load level neighbors

Rather than spawning a level the moment the player travels to them, this guide instead loads levels before they reach them. Use the "load level neighbors" feature, so the plugin spawns not just the currently selected level, but its neighbors too.

use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
fn main() {
    App::new()
        // Other App builders
        .insert_resource(LdtkSettings {
            level_spawn_behavior: LevelSpawnBehavior::UseWorldTranslation {
                load_level_neighbors: true,
            },
            ..default()
        })
        .run();
}

Determine bounds of spawned levels and update level selection

With load_level_neighbors enabled, any level that the player can traverse to will already be spawned, barring teleportation. Use the transforms of the spawned levels and width/height info from the level's asset data to create a Rect of the level's bounds.

To access the level asset data, you first need to access the project asset data. Assuming you only have one project, query for the only Handle<LdtkProject> entity and look up its asset data in the LdtkProject asset store. Then, get the raw level data for every spawned level using the level entity's LevelIid component (there is a provided method for this).

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
#[derive(Component)]
struct Player;
fn level_selection_follow_player(
    players: Query<&GlobalTransform, With<Player>>,
    levels: Query<(&LevelIid, &GlobalTransform)>,
    ldtk_projects: Query<&Handle<LdtkProject>>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
    mut level_selection: ResMut<LevelSelection>,
) {
    if let Ok(player_transform) = players.get_single() {
        let ldtk_project = ldtk_project_assets
            .get(ldtk_projects.single())
            .expect("ldtk project should be loaded before player is spawned");

        for (level_iid, level_transform) in levels.iter() {
            let level = ldtk_project
                .get_raw_level_by_iid(level_iid.get())
                .expect("level should exist in only project");
        }
    }
}
}

The level's GlobalTransform's x/y value should be used as the lower-left bound of the Rect. Add the raw level's px_wid and pix_hei values to the lower-left bound to calculate the upper-right bound.

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_ldtk::ldtk::Level;
fn foo(level_transform: &GlobalTransform, level: &Level) {
            let level_bounds = Rect {
                min: Vec2::new(
                    level_transform.translation().x,
                    level_transform.translation().y,
                ),
                max: Vec2::new(
                    level_transform.translation().x + level.px_wid as f32,
                    level_transform.translation().y + level.px_hei as f32,
                ),
            };
}
}

After creating a Rect of the level bounds, check if the player is inside those bounds and update the LevelSelection resource accordingly. The full system should look something like this:

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
#[derive(Component)]
struct Player;
fn level_selection_follow_player(
    players: Query<&GlobalTransform, With<Player>>,
    levels: Query<(&LevelIid, &GlobalTransform)>,
    ldtk_projects: Query<&Handle<LdtkProject>>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
    mut level_selection: ResMut<LevelSelection>,
) {
    if let Ok(player_transform) = players.get_single() {
        let ldtk_project = ldtk_project_assets
            .get(ldtk_projects.single())
            .expect("ldtk project should be loaded before player is spawned");

        for (level_iid, level_transform) in levels.iter() {
            let level = ldtk_project
                .get_raw_level_by_iid(level_iid.get())
                .expect("level should exist in only project");

            let level_bounds = Rect {
                min: Vec2::new(
                    level_transform.translation().x,
                    level_transform.translation().y,
                ),
                max: Vec2::new(
                    level_transform.translation().x + level.px_wid as f32,
                    level_transform.translation().y + level.px_hei as f32,
                ),
            };

            if level_bounds.contains(player_transform.translation().truncate()) {
                *level_selection = LevelSelection::Iid(level_iid.clone());
            }
        }
    }
}
}