Migrate from 0.8 to 0.9

Bevy upgrade

bevy_ecs_ldtk has upgraded to Bevy and bevy_ecs_tilemap version 0.12. A Bevy 0.12 migration guide is available on Bevy's website.

LDtk upgrade

bevy_ecs_ldtk now supports LDtk 1.5.3, and is dropping support for previous versions. To update your game to LDtk 1.5.3, you should only need to install the new version of LDtk, open your project, and save it.

Default behavior for LdtkEntity and LdtkIntCell derive macros

Fields on an LdtkEntity- or LdtkIntCell-derived bundle are no longer constructed from the field's Default implementation, but the bundle's.

You may observe different behavior in 0.9 if the value for a field in your bundle's Default implementation differs from the field type's own Default implementation:

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
#[derive(Component)]
struct MyComponent(usize);

impl Default for MyComponent {
    fn default() -> MyComponent {
        MyComponent(1)
    }
}

#[derive(Bundle, LdtkEntity)]
struct MyBundle {
    component: MyComponent,
}

impl Default for MyBundle {
    fn default() -> MyBundle {
        MyBundle {
            component: MyComponent(2),
        }
    }
}

// In bevy_ecs_ldtk 0.8, the plugin would spawn an entity w/ MyComponent(1)

// In bevy_ecs_ldtk 0.9, the plugin now spawns the entity w/ MyComponent(2)
}

You may also need to implement Default for LdtkEntity types that did not have that implementation before:

// 0.8
#[derive(Bundle, LdtkEntity)]
struct MyBundle {
    component: MyComponentThatImplementsDefault,
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
#[derive(Default, Component)]
struct MyComponentThatImplementsDefault;
// 0.9
#[derive(Default, Bundle, LdtkEntity)]
struct MyBundle {
    component: MyComponentThatImplementsDefault,
}
}

Hierarchy of LDtk Entities

Layer entities (with a LayerMetadata component) are now spawned for LDtk Entity layers, just like any other layer. By default, LDtk Entities are now spawned as children to these layer entities instead of as children of the level.

// 0.8
fn get_level_of_entity(
    entities: Query<Entity, With<EntityInstance>>,
    parent_query: Query<&Parent>,
) {
    for entity in &entities {
        println!(
            "the level that {:?} belongs to is {:?}",
            entity,
            parent_query.iter_ancestors(entity).nth(0)
        );
    }
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9
fn get_level_of_entity(
    entities: Query<Entity, With<EntityInstance>>,
    parent_query: Query<&Parent>,
) {
    for entity in &entities {
        println!(
            "the level that {:?} belongs to is {:?}",
            entity,
            parent_query.iter_ancestors(entity).nth(1)
        );
    }
}
}

Asset Type Rework

Most breaking changes in this release are related to the asset types, previously LdtkAsset and LdtkLevel. These types have been heavily reworked to improve code quality, correctness, performance, and provide better APIs.

LdtkAsset is now LdtkProject, and other changes

LdtkAsset has now been renamed to LdtkProject. Any types and systems that depend on this type will need to be updated accordingly:

// 0.8
fn do_some_processing_with_ldtk_data(
    worlds: Query<&Handle<LdtkAsset>>,
    ldtk_assets: Res<Assets<LdtkAsset>>
) {
    // do something
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9
fn do_some_processing_with_ldtk_data(
    worlds: Query<&Handle<LdtkProject>>,
    ldtk_assets: Res<Assets<LdtkProject>>
) {
    // do something
}
}

Furthermore, all of its fields have been privatized, and are now only available via immutable accessor methods. Not all of these methods share the same name as their corresponding field in 0.8:

// 0.8
let ldtk_json = ldtk_project.project;
let tileset_map = ldtk_project.tileset_map;
let int_grid_image_handle = ldtk_project.int_grid_image_handle;
let level_map = ldtk_project.level_map;
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
fn foo(ldtk_project: LdtkProject) {
// 0.9
let ldtk_json = ldtk_project.json_data();
let tileset_map = ldtk_project.tileset_map();
let int_grid_image_handle = ldtk_project.int_grid_image_handle();
// the level_map is no longer available in the same way
}
}

LdtkAsset and LdtkJson level accessor methods have been moved

Level accessing methods have been completely redefined. Analogues to existing methods have been renamed and moved to traits:

// 0.8
ldtk_json.iter_levels();

ldtk_asset.iter_levels();

ldtk_asset.get_level(&LevelSelection::Uid(24));
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::{prelude::*, ldtk::LdtkJson};
fn foo(ldtk_json: LdtkJson, ldtk_project: LdtkProject) {
// 0.9
// in `RawLevelAccessor` trait:
ldtk_json.iter_raw_levels();

ldtk_project.iter_raw_levels();

// in `LevelMetadataAccessor` trait
ldtk_project.find_raw_level_by_level_selection(&LevelSelection::Uid(24));
}
}

Many new methods have been provided as well.

Internal-levels and external-levels support now behind separate features

There are two new cargo features, internal_levels and external_levels. internal_levels is enabled by default and allows loading of internal-levels LDtk projects. external_levels is not enabled by default and allows loading of external-levels LDtk projects. Some APIs are unique to the two cases.

If you have an LDtk project with internal levels, but have disabled default features, you will need to enable internal_levels:

# 0.8
bevy_ecs_ldtk = { version = "0.8", default-features = false, features = ["render"] }

# 0.9
bevy_ecs_ldtk = { version = "0.9", default-features = false, features = ["render", "internal_levels"] }

If you have an LDtk project with external levels, you will need to enable external_levels:

# 0.8
bevy_ecs_ldtk = "0.8"

# 0.9
bevy_ecs_ldtk = { version = "0.9", features = ["external_levels"] }

These features are not mutually exclusive, but at least one of them must be enabled.

Level Asset Changes

The level asset type has changed significantly. Most importantly, it is no longer the primary mechanism for storing loaded level data. In fact, it is only compiled and used within the external_levels feature (see previous section).

Level entities now have a LevelIid instead of a Handle<LdtkLevel>

The level asset it is no longer the main component marking level entities. In both internal-levels and external-levels projects, level entities will no longer have a handle to the level asset, but instead will have a LevelIid component:

// 0.8
fn print_level_entity(levels: Query<Entity, With<Handle<LdtkLevel>>>) {
    for entity in &levels {
        println!("level entity {:?} is currently spawned", entity);
    }
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9
fn print_level_entity(levels: Query<Entity, With<LevelIid>>) {
    for entity in &levels {
        println!("level entity {:?} is currently spawned", entity);
    }
}
}

Accessing level data from the level entity

Retrieving level data from the level entity can be done using the LevelIid component. If the data you need is not inside the level's layer_instances, you can access it on the LdtkProject asset:

// 0.8
fn print_level_uid(levels: Query<Handle<LdtkLevel>>, level_assets: Res<Assets<LdtkLevel>>) {
    for level_handle in &levels {
        let level_uid = level_assets.get(level_handle).unwrap().uid;
        println!("level w/ uid {level_uid}, is currently spawned");
    }
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9
fn print_level_uid(
    levels: Query<&LevelIid>,
    projects: Query<&Handle<LdtkProject>>,
    project_assets: Res<Assets<LdtkProject>>
) {
    for level_iid in &levels {
        let only_project = project_assets.get(projects.single()).unwrap();

        let level_uid = only_project.get_raw_level_by_iid(level_iid.get()).unwrap().uid;
        println!("level w/ uid {level_uid}, is currently spawned");
    }
}
}

If the level data you need is inside the level's layer_instances, you may want to retrieve a LoadedLevel. A Level might not have complete data - in the case that it's the "raw" level inside an external-levels project's LdtkProject asset. This new LoadedLevel type provides type guarantees that the level has complete data. For internal-levels (aka "standalone") projects, you can retrieve loaded level data with a LevelIid and LdtkProject alone:

#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9, w/ internal_levels enabled
fn print_level_uid(
    levels: Query<&LevelIid>,
    projects: Query<&Handle<LdtkProject>>,
    project_assets: Res<Assets<LdtkProject>>
) {
    for level_iid in &levels {
        let only_project = project_assets.get(projects.single()).unwrap();

        let layer_count = only_project
            .as_standalone()
            .get_loaded_level_by_iid(level_iid.get())
            .unwrap()
            .layer_instances()
            .len();
        println!("level has {layer_count} layers");
    }
}
}

For external-levels (aka "parent") projects, you will need to additionally access the LdtkExternalLevel asset store:

use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
// 0.9, w/ external_levels enabled
fn print_level_uid(
    levels: Query<&LevelIid>,
    projects: Query<&Handle<LdtkProject>>,
    project_assets: Res<Assets<LdtkProject>>
    level_assets: Res<Assets<LdtkExternalLevel>>,
) {
    for level_iid in &levels {
        let only_project = project_assets.get(projects.single()).unwrap();

        let layer_count = only_project
            .as_parent()
            .get_external_level_by_iid(&level_assets, level_iid.get())
            .unwrap()
            .layer_instances()
            .len();
        println!("level has {layer_count} layers");
    }
}

Module restructure

Some types related to assets have been removed, or privatized, or moved.

Those that were removed/privatized were generally not intended to be used by users:

  • LevelMap
  • TilesetMap
  • LdtkLevelLoader
  • LdtkLoader

Those that were moved have been moved into the assets module, and are still exposed in the prelude:

  • LdtkProject
  • LdtkExternalLevel

LevelIid everywhere

LevelIid is a new component on level entities that stores the level's iid as a string. It has been reused throughout the API.

In LevelSet

LevelSet uses it, but can still be constructed from strings using from_iids:

// 0.8
let level_set = LevelSet {
    iids: [
        "e5eb2d73-60bb-4779-8b33-38a63da8d1db".to_string(),
        "855fab73-2854-419f-a3c6-4ed8466592f6".to_string(),
    ].into_iter().collect(),
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
fn f() {
// 0.9
let level_set = LevelSet::from_iids(
    [
        "e5eb2d73-60bb-4779-8b33-38a63da8d1db",
        "855fab73-2854-419f-a3c6-4ed8466592f6",
    ]
);
}
}

In LevelEvent

use std::any::{Any, TypeId};
// 0.8
fn assert_level_event_type(mut level_events: EventReader<LevelEvent>) {
    for level_event in level_events.iter() {
        use LevelEvent::*;
        let level_iid = match level_event {
            SpawnTriggered(level_iid) | Spawned(level_iid) | Transformed(level_iid) | Despawned(level_iid) => level_iid,
        };

        assert_eq!(level_iid.type_id(), TypeId::of::<String>());
    }
}
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
use bevy::prelude::*;
use std::any::{Any, TypeId};
// 0.9
fn assert_level_event_type(mut level_events: EventReader<LevelEvent>) {
    for level_event in level_events.read() {
        use LevelEvent::*;
        let level_iid = match level_event {
            SpawnTriggered(level_iid) | Spawned(level_iid) | Transformed(level_iid) | Despawned(level_iid) => level_iid,
        };

        assert_eq!(level_iid.type_id(), TypeId::of::<LevelIid>());
    }
}
}

In LevelSelection::Iid

LevelSelection uses it, but can still be constructed with a string via the iid method:

// 0.8
let level_selection = LevelSelection::Iid("e5eb2d73-60bb-4779-8b33-38a63da8d1db".to_string());
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
fn f() {
// 0.9
let level_selection = LevelSelection::iid("e5eb2d73-60bb-4779-8b33-38a63da8d1db");
}
}

LevelSelection index variant now stores a world index

The LevelSelection::Index variant has been replaced by LevelSelection::Indices. Internally, this contains a new LevelIndices type, which stores an optional world index in addition to the level index. However, you can still construct a LevelSelection from a single level index using the index method:

// 0.8
let level_selection = LevelSelection::Index(2);
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
fn f() {
// 0.9
let level_selection = LevelSelection::index(2);
}
}

LevelSet::from_iid replaced with LevelSet::from_iids

LevelSet::from_iid has been replaced by LevelSet::from_iids. This new method can accept any iterator of strings rather than just one:

// 0.8
let level_set = LevelSet::from_iid("e5eb2d73-60bb-4779-8b33-38a63da8d1db");
#![allow(unused)]
fn main() {
use bevy_ecs_ldtk::prelude::*;
fn f() {
// 0.9
let level_set = LevelSet::from_iids(["e5eb2d73-60bb-4779-8b33-38a63da8d1db"]);

// or many..
let level_set = LevelSet::from_iids(
    [
        "e5eb2d73-60bb-4779-8b33-38a63da8d1db",
        "855fab73-2854-419f-a3c6-4ed8466592f6",
    ]
);
}
}