bevy_tiled_loader 0.1.0

Asset loader for the Tiled data parsed.
Documentation

use bevy::asset::{io::Reader, AssetLoader, AssetPath, AsyncReadExt};
use bevy::asset::{Handle, LoadContext};
use bevy::math::{UVec2, Vec2};
use bevy::prelude::*;
use bevy::prelude::SpatialBundle;
use bevy::scene::Scene;
use bevy::sprite::{Anchor, Sprite, TextureAtlas, TextureAtlasLayout};
use bevy::transform::components::Transform;

#[cfg(feature = "rapier2d_colliders")]
use bevy_rapier2d::prelude::*;
use tiled_parse::relations::{get_tile_id, get_tileset_for_gid};

use crate::types::{TiledId, TiledMapAsset, TiledMapContainer};
use tiled_parse::data_types::*;
use tiled_parse::parse::*;

/// Load `*.tmx` via `AssetServer.load("MY_MAP.tmx")`
///
/// Note that objects within tile's tileset are NOT marked with ANY TiledId.
#[derive(Default)]
pub struct TiledLoader;

pub const MAP_SCENE: &str = "MapScene";

// TODO:
// Improve error
impl AssetLoader for TiledLoader {
    type Asset = TiledMapAsset;
    /// TODO:
    /// Add settings
    type Settings = ();
    type Error = std::io::Error;

    async fn load(
        &self,
        reader: &mut dyn bevy::asset::io::Reader,
        _settings: &Self::Settings,
        load_context: &mut bevy::asset::LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
        let mut data = Vec::new();
        reader.read_to_end(&mut data).await?;

        let data_as_utf8 = std::str::from_utf8(&data).map_err(|e| {
            std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("Could not load TMX map: {e}"),
            )
        })?;

        let tm: TiledMap = parse(data_as_utf8).map_err(|e| {
            std::io::Error::new(std::io::ErrorKind::Other, format!("Could not load TMX map"))
        })?;

        load_tmx(load_context, tm)
    }

    fn extensions(&self) -> &[&str] {
        static EXTENSIONS: &[&str] = &["tmx"];
        EXTENSIONS
    }
}

fn load_tmx(load_context: &mut LoadContext, tm: TiledMap) -> Result<TiledMapAsset, std::io::Error> {
    // TODO:
    // Might need some way to get tilemap_texture from a Tile's GID (To get the tile's texture).
    let TiledMap {
        layers,
        grid_size,
        tile_size,
        tile_sets,
    } = &tm;

    // TODO:
    // Review how tile set images are stored into these `Vec`s.
    // Some tilesets have more than one image (check TMX docs to verify what that actually means)
    let mut tilemap_textures = Vec::with_capacity(tile_sets.len());
    let mut tilemap_atlases = Vec::with_capacity(tile_sets.len());

    tile_sets.iter().for_each(|ts| {
        let TileSet {
            tile_size,
            first_gid,
            name,
            spacing,
            margin,
            image,
            tile_stuff,
        } = ts;

        let tiled_parse::data_types::Image {
            source,
            format,
            dimensions: (columns, rows),
        } = image;

        let tmx_dir = load_context
            .path()
            .parent()
            .expect("The asset load context was empty.");
        let tile_path = tmx_dir.join(&source);
        let asset_path = AssetPath::from(tile_path);

        let texture_handle: Handle<bevy::prelude::Image> = load_context.load(asset_path.clone());

        let file_name = source
            .file_name()
            .expect("Should have file name")
            .to_str()
            .expect("Valid utf8");

        // TODO:
        // I don't know if I should use "add_labeled_asset", and if the arguments are
        // conventional
        let texture_atlas: Handle<bevy::prelude::TextureAtlasLayout> = load_context
            .add_labeled_asset(
                file_name.into(),
                TextureAtlasLayout::from_grid(
                    UVec2::new(tile_size.0, tile_size.1),
                    *columns,
                    *rows,
                    // TODO:
                    // I'm not sure this translates correctly
                    Some(*spacing as u32 * UVec2::ONE),
                    Some(*margin as u32 * UVec2::ONE),
                ),
            );

        tilemap_textures.push(texture_handle);
        tilemap_atlases.push(texture_atlas);
    });

    // Load scene
    let scene = {
        let scene_load_context = load_context.begin_labeled_asset();
        let mut world = World::default();

        let world_root_id = world.spawn(SpatialBundle::INHERITED_IDENTITY).id();

        let mut layer_ents = Vec::new();

        let tile_size_f32 = (tile_size.0 as f32, tile_size.1 as f32);

        layers.iter_breadth().enumerate().for_each(|(i, x)| {
            let TiledLayer {
                id, name, content, ..
            } = x;

            match content {
                // TODO:
                // Handle other layer types
                LayerType::TileLayer(tile_layer) => {
                    // TODO:
                    // Assigning z-index to `i` won't work for GroupLayers because `layers` currently iterate as a breadth first
                    // iterator...
                    let mut spatial_bundle = SpatialBundle::INHERITED_IDENTITY;
                    spatial_bundle.transform.translation = Vec2::ZERO.extend(i as f32);

                    let layer_ent = world
                        .spawn((Name::new(name.clone()), spatial_bundle, TiledId::Layer(*id)))
                        .id();

                    layer_ents.push(layer_ent);

                    tile_layer
                        .indexed_iter()
                        .filter_map(|(p, t)| t.map(|v| (p, v)))
                        .for_each(
                            |(
                                tile_pos,
                                LayerTile {
                                    tile: tile_gid,
                                    flip_h,
                                    flip_v,
                                    flip_d,
                                },
                            )| {
                                if flip_d {
                                    panic!("`flip_d` is not yet implemented");
                                }
                                let (world_pos_x, world_pos_y) = (
                                    tile_size_f32.0 * tile_pos.0 as f32,
                                    -tile_size_f32.1 * tile_pos.1 as f32,
                                );

                                let tile_tileset = get_tileset_for_gid(tile_sets, tile_gid)
                                    .expect("Tile should belong to tileset");

                                let tileset_index = tile_sets
                                    .iter()
                                    .position(|ts| ts.first_gid == tile_tileset.first_gid)
                                    .expect("Yes");

                                let local_tile_id = get_tile_id(tile_tileset, tile_gid);

                                let tile_aux_info_opt = tile_tileset.tile_stuff.get(&local_tile_id);

                                let mut tile_entity = world.spawn((
                                    Sprite {
                                        image: tilemap_textures.get(tileset_index).unwrap().clone(),
                                        texture_atlas: Some(TextureAtlas {
                                            layout: tilemap_atlases
                                                .get(tileset_index)
                                                .unwrap()
                                                .clone(),
                                            index: local_tile_id as usize,
                                        }),
                                        flip_x: flip_h,
                                        flip_y: flip_v,
                                        anchor: Anchor::TopLeft,
                                        ..Default::default()
                                    },
                                    Transform::from_xyz(world_pos_x, world_pos_y, 0.),
                                    TiledId::Tile(tile_gid.0),
                                ));

                                // FIXME: Dynamic RigidBody's move independently from the Sprite.
                                if let Some(tile_aux_info) = tile_aux_info_opt {
                                    #[cfg(feature = "rapier2d_colliders")]
                                    crate::rapier_colliders::add_child_colliders(
                                        &mut tile_entity,
                                        &tile_aux_info.objects,
                                    );

                                    #[cfg(feature = "avian2d_colliders")]
                                    crate::avian_colliders::add_child_colliders(
                                        &mut tile_entity,
                                        &tile_aux_info.objects,
                                    );
                                }

                                tile_entity.set_parent(layer_ent);
                            },
                        );
                }
                LayerType::Group => println!("Group layer {name}"),
                LayerType::ObjectLayer(os) => {
                    let layer_entity = world.spawn((
                        Name::new(name.clone()),
                        Transform::IDENTITY,
                        TiledId::Layer(*id),
                    ));

                    let layer_entity_id = layer_entity.id();

                    layer_ents.push(layer_entity_id);

                    // FIXME: Dynamic RigidBody's move independently from the Sprite.
                    os.iter().for_each(|o| {
                        let Object {
                            id,
                            position,
                            size,
                            rotation,
                            otype,
                            ..
                        } = o;

                        if let &ObjectType::Tile(tile_gid) = otype {
                            let scale_factor = if let Some((width, height)) = size {
                                Vec2::new(width / (tile_size_f32.0), height / (tile_size_f32.1))
                            } else {
                                Vec2::ONE
                            };

                            let world_pos = Vec2::new(
                                position.0,
                                -(position.1 - scale_factor.y * tile_size_f32.1),
                            );

                            let tile_tileset = get_tileset_for_gid(tile_sets, tile_gid)
                                .expect("Tile should belong to tileset");

                            let tileset_index = tile_sets
                                .iter()
                                .position(|ts| ts.first_gid == tile_tileset.first_gid)
                                .expect("Yes");

                            let local_tile_id = get_tile_id(tile_tileset, tile_gid);

                            let tile_aux_info_opt = tile_tileset.tile_stuff.get(&local_tile_id);

                            let mut tile_entity = world.spawn((
                                Sprite {
                                    image: tilemap_textures.get(tileset_index).unwrap().clone(),
                                    texture_atlas: Some(TextureAtlas {
                                        layout: tilemap_atlases.get(tileset_index).unwrap().clone(),
                                        index: local_tile_id as usize,
                                    }),
                                    anchor: Anchor::TopLeft,
                                    ..Default::default()
                                },
                                // FIXME: Use a correct z-index.
                                Transform::from_translation(world_pos.extend(3.))
                                    .with_rotation(Quat::from_axis_angle(
                                        Vec3::Z,
                                        rotation.to_radians(),
                                    ))
                                    .with_scale(scale_factor.extend(1.)),
                                TiledId::Object(*id),
                            ));

                            // FIXME: Dynamic RigidBody's move independently from the Sprite.
                            if let Some(tile_aux_info) = tile_aux_info_opt {
                                #[cfg(feature = "rapier2d_colliders")]
                                crate::rapier_colliders::add_child_colliders(
                                    &mut tile_entity,
                                    &tile_aux_info.objects,
                                );

                                #[cfg(feature = "avian2d_colliders")]
                                crate::avian_colliders::add_child_colliders(
                                    &mut tile_entity,
                                    &tile_aux_info.objects,
                                );
                            }

                            tile_entity.set_parent(layer_entity_id);
                        } else {
                            let mut obj_ent = world.spawn_empty();

                            #[cfg(feature = "rapier2d_colliders")]
                            crate::rapier_colliders::insert_collider(&mut obj_ent, o);

                            #[cfg(feature = "avian2d_colliders")]
                            crate::avian_colliders::insert_collider(&mut obj_ent, o);

                            obj_ent.set_parent(layer_entity_id);
                        }
                    });
                }
                _ => {
                    eprintln!("Layer `{name} : {content:#?}` is not currently handled.");
                }
            }
        });

        // TODO:
        // I'm not convinced this `per-entity` thing is very good.
        let mut e_c = world.spawn((
            TiledMapContainer,
            // TODO:
            // There may be some situation where it won't just be 0 ?
            SpatialBundle::INHERITED_IDENTITY,
        ));

        e_c.add_children(&layer_ents);
        e_c.set_parent(world_root_id);
        let loaded_scene = scene_load_context.finish(Scene::new(world), None);
        // TODO:
        // Figure out what to use as a label.
        load_context.add_loaded_labeled_asset(MAP_SCENE, loaded_scene)
    };

    Ok(TiledMapAsset {
        map: tm,
        scene,
        tilemap_textures,
        tilemap_atlases,
    })
}

// fn handle_parallax(
//     camera_trans_q: Query<&Transform, With<Camera>>,
//     mut parallax_layer: Query<(&mut Transform, &LayerParallax), Without<Camera>>,
// ) {
//     let Ok(cam_transform) = camera_trans_q.get_single() else {
//         return;
//     };
//
//     parallax_layer
//         .iter_mut()
//         .for_each(|(mut layer_transform, layer_parallax)| {
//             let dist_from_layer_center = cam_transform.translation - layer_parallax.offset;
//
//             layer_transform.translation = layer_parallax.center
//                 - Vec3::new(
//                     dist_from_layer_center.x * (layer_parallax.parallax.x - 1.),
//                     dist_from_layer_center.y * (layer_parallax.parallax.y - 1.),
//                     0.,
//                 );
//         })
// }