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::*;
#[derive(Default)]
pub struct TiledLoader;
pub const MAP_SCENE: &str = "MapScene";
impl AssetLoader for TiledLoader {
type Asset = TiledMapAsset;
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> {
let TiledMap {
layers,
grid_size,
tile_size,
tile_sets,
} = &tm;
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");
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,
Some(*spacing as u32 * UVec2::ONE),
Some(*margin as u32 * UVec2::ONE),
),
);
tilemap_textures.push(texture_handle);
tilemap_atlases.push(texture_atlas);
});
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 {
LayerType::TileLayer(tile_layer) => {
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),
));
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);
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()
},
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),
));
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.");
}
}
});
let mut e_c = world.spawn((
TiledMapContainer,
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);
load_context.add_loaded_labeled_asset(MAP_SCENE, loaded_scene)
};
Ok(TiledMapAsset {
map: tm,
scene,
tilemap_textures,
tilemap_atlases,
})
}