bevy_ecs_ldtk

repository·main·Indexed 21 days ago

https://github.com/trouv/bevy_ecs_ldtk

An ECS-friendly plugin for the Bevy game engine that integrates LDtk (Level Designer Toolkit) projects as assets. It enables level spawning and mapping of LDtk entities and tiles to Bevy components and bundles. Key features include support for all LDtk layer types, hot reloading, Wasm support via the atlas feature, and procedural macros (LdtkEntity and LdtkIntCell) for low-boilerplate spawning.

Tokens
25.3K
Snippets
71
Records
120
Agent score
74%

What's inside bevy_ecs_ldtk

  1. Overview of bevy_ecs_ldtk

    main

    bevy_ecs_ldtk is an ECS-friendly plugin for the Bevy game engine that integrates LDtk projects. It allows you to use LDtk projects as assets, spawn levels, and map LDtk entities or tiles to Bevy components and bundles.

    Key capabilities include:

    • Support for all LDtk layer types.
    • Loading external levels and implementing hot reloading.
    • Strategies for loading/unloading levels and managing level neighbors.
    • Low-boilerplate spawning of bundles for LDtk Entities and IntGrid tiles using derive macros.
    • serde types for LDtk data with quality-of-life improvements.
    • Wasm support (including tile spacing) via the atlas feature.
  2. Use `bevy_ecs_ldtk_macros` for LDtk entity and cell derivation

    main

    bevy_ecs_ldtk_macros provides procedural macros to simplify the implementation of LDtk-related data structures in Bevy. It allows you to automatically derive the necessary traits for entities and integer cells defined in your LDtk levels.

    Supported derives:

    • #[derive(LdtkEntity)]: Use this on structs representing entities defined in your LDtk file to enable automatic spawning and component management.
    • #[derive(LdtkIntCell)]: Use this on structs representing integer cells (tiles) to facilitate their integration with the LDtk level data.
  3. Use the Blueprint Pattern for complex entity spawning

    main

    The Blueprint Pattern is a recommended hybrid approach that combines the ergonomics of registration with the power of post-processing.

    Instead of querying for raw EntityInstance components (which requires manual string filtering), you register a bundle that includes a marker component. You then write your post-processing systems to query for that marker component using Added<MarkerComponent>. This makes your systems cleaner, more performant, and avoids issues like accidentally overwriting the plugin-provided Transform.

    use bevy::prelude::*;
    use bevy_ecs_ldtk::prelude::*;
    
    fn main() {
        App::new()
            // 1. Register a bundle that includes a marker component (Player)
            .register_ldtk_entity::<PlayerBundle>("Player")
            .add_systems(Update, process_player)
            .run();
    }
    
    #[derive(Default, Component)]
    struct Player; // This is our marker component
    
    #[derive(Default, Component)]
    struct PlayerChild;
    
    #[derive(Default, Bundle, LdtkEntity)]
    struct PlayerBundle {
        player: Player,
        #[sprite]
        sprite: Sprite,
    }
    
    // 2. Post-process by querying for the marker component instead of EntityInstance
    fn process_player(
        mut commands: Commands,
        new_players: Query<Entity, Added<Player>>,
    ) {
        for player_entity in new_players.iter() {
            commands
                .spawn(PlayerChild)
                .insert(ChildOf(player_entity));
        }
    }
  4. Handle layers with colliding tiles

    main

    Because bevy_ecs_tilemap only allows one tile per position, bevy_ecs_ldtk supports LDtk layers with colliding tiles (multiple tiles in one location) by spawning multiple tilemaps.

    Important: Each of these tilemaps will have its own LayerMetadata component. Therefore, you cannot assume there is only one LayerMetadata entity per LDtk layer.

  5. Update LdtkEntity and LdtkIntCell derive macros

    main

    In 0.9, fields on a bundle derived with #[derive(LdtkEntity)] or #[derive(LdtkIntCell)] are constructed from the bundle's Default implementation, not the field's own Default implementation.

    If your bundle's Default provides a different value than the field's Default, the bundle's value will be used. Additionally, you may need to explicitly implement Default for your LdtkEntity bundles if they didn't have it before.

    #[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 0.9, the plugin spawns the entity with MyComponent(2)
  6. Select multiple levels using the `LevelSet` component

    main

    For complex level-spawning needs, use the LevelSet component instead of the global LevelSelection resource. LevelSet allows you to select a specific set of levels by their iids.

    Key Characteristics:

    • Per-World Selection: If you have multiple LdtkWorldBundle instances, LevelSet allows you to select different levels for each world, whereas LevelSelection is global.
    • Declarative/Idempotent Updates: When the LevelSet is updated, the plugin performs change detection. It only spawns levels that are in the set but not yet spawned, and despawns levels that are currently spawned but no longer in the set.
    • Limitations: You cannot use load_level_neighbors with this workflow. However, LevelSpawnBehavior::UseWorldTranslation still works.
    • Constraint: For LevelSet to function correctly, the LevelSelection resource must not exist in the world.
  7. Understand core concepts and architecture

    main

    The 'Explanation' section provides deep dives into the mental models required to use the library effectively, including:

    • Level Selection: How to manage and switch between different levels.
    • Game Logic Integration: How to bridge LDtk data with Bevy game logic.
    • Anatomy of the World: Understanding the structural hierarchy of the loaded world.
    • Plugin Schedule: How the plugin interacts with the Bevy schedule.
    • Asset Model: How LDtk assets are represented within Bevy.
    • Limitations: Known constraints of the current implementation.
  8. Understand Z order and rendering depth

    main

    To manage render order, bevy_ecs_ldtk uses the z value of Transform components.

    Scope of Z order:

    • Z order is explicitly applied to level backgrounds, layer entities, and worldly entities.
    • Tiles and non-worldly entities inherit Z order via their GlobalTransform.

    Z value logic:

    • The system starts at z = 0 for the background-most entities and increments by 1 for each layer above.
    • Backgrounds: Background colors usually get z = 0 and background images get z = 1. If an image is missing, z = 1 is used by the next layer. If backgrounds are disabled, both 0 and 1 are freed for the next layer.
    • Layers: Each layer generally increments z by 1. However, because layers with colliding tiles spawn multiple layer entities, each additional entity will also increment the z value.

    Best Practice: Avoid making hardcoded assumptions about specific z values for layers, as the presence of backgrounds or colliding tiles can shift the sequence.

  9. How worldly entities work

    main

    Using the LdtkEntity derive macro allows you to define "worldly" entities. These are entities designed to persist and traverse between levels (e.g., a player character).

    Key behaviors of worldly entities:

    1. Hierarchy Shift: Instead of being children of an Entity layer, worldly entities become children of the world entity (after one update). This makes them independent of their origin level, allowing them to persist even if the level that spawned them is unloaded.
    2. Persistence: A worldly entity will not be spawned if it already exists. This prevents duplicate entities (like two players) when an origin level is despawned and subsequently respawned.
  10. Use `GridCoords` for tile-based positioning

    main

    To implement tile-based mechanics, you need to track an entity's position in grid-space rather than just its Bevy Transform.

    bevy_ecs_ldtk provides the GridCoords component for this purpose. You can integrate it with the LdtkEntity derive macro by adding the #[grid_coords] attribute to your bundle. When the entity is spawned from LDtk, it will automatically receive a GridCoords component matching its position in the grid.

    It is recommended to also add a marker component (e.g., struct Player;) and derive Default so bevy_ecs_ldtk can use the default implementation during spawning.

    #[derive(Component, Default)]
    struct Player;
    
    #[derive(LdtkEntity)]
    struct PlayerBundle {
        #[grid_coords]
        grid_coords: GridCoords,
        player: Player,
        // ... other components
    }
  11. Calculate level bounds using asset data and transforms

    main

    To determine the physical area a level occupies in the world, you must combine its spatial transform with its dimensions from the LDtk asset data:

    1. Access Asset Data: Query for the LdtkProjectHandle and look up the LdtkProject in the asset store.
    2. Get Level Data: Use the level entity's LevelIid component to retrieve the raw level data.
    3. Compute Bounds:
      • The lower-left bound is the x and y values from the level's GlobalTransform.
      • The upper-right bound is calculated by adding the level's px_wid (pixel width) and pix_hei (pixel height) to the lower-left coordinates.
    4. Result: This creates a Rect representing the level's bounds in world space.