use bevy_ecs_tilemap::{
anchor::TilemapAnchor,
map::{TilemapId, TilemapSize, TilemapTexture, TilemapTileSize},
tiles::{TileBundle, TileFlip, TilePos, TileStorage, TileTextureIndex},
TilemapBundle,
};
use std::{collections::HashMap, io::ErrorKind};
use thiserror::Error;
use bevy::{asset::io::Reader, reflect::TypePath};
use bevy::{
asset::{AssetLoader, AssetPath, LoadContext},
log,
prelude::*,
};
use bevy_ecs_tilemap::map::TilemapType;
#[derive(Default)]
pub struct LdtkPlugin;
impl Plugin for LdtkPlugin {
fn build(&self, app: &mut App) {
app
.init_asset::<LdtkMap>()
.register_asset_loader(LdtkLoader)
.add_systems(Update, process_loaded_tile_maps);
}
}
#[derive(TypePath, Asset)]
pub struct LdtkMap {
pub project: ldtk_rust::Project,
pub tilesets: HashMap<i64, Handle<Image>>,
}
#[derive(Default, Debug, Component, Clone, Copy, Reflect)]
pub struct LdtkMapConfig {
pub selected_level: usize,
}
#[derive(Default, Debug, Component, Clone, Reflect)]
pub struct LdtkMapHandle(pub Handle<LdtkMap>);
#[derive(Default, Debug, Bundle)]
pub struct LdtkMapBundle {
pub ldtk_map: LdtkMapHandle,
pub ldtk_map_config: LdtkMapConfig,
pub transform: Transform,
pub global_transform: GlobalTransform,
}
pub struct LdtkLoader;
#[derive(Debug, Error)]
pub enum LdtkAssetLoaderError {
#[error("Could not load LDTk file: {0}")]
Io(#[from] std::io::Error),
}
impl AssetLoader for LdtkLoader {
type Asset = LdtkMap;
type Settings = ();
type Error = LdtkAssetLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let project: ldtk_rust::Project = serde_json::from_slice(&bytes).map_err(|e| {
std::io::Error::new(
ErrorKind::Other,
format!("Could not read contents of Ldtk map: {e}"),
)
})?;
let dependencies: Vec<(i64, AssetPath)> = project
.defs
.tilesets
.iter()
.filter_map(|tileset| {
tileset.rel_path.as_ref().map(|rel_path| {
(
tileset.uid,
load_context.path().path().parent().unwrap().join(rel_path).into(),
)
})
})
.collect();
let ldtk_map = LdtkMap {
project,
tilesets: dependencies
.iter()
.map(|dep| (dep.0, load_context.load(dep.1.clone())))
.collect(),
};
Ok(ldtk_map)
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &["ldtk"];
EXTENSIONS
}
}
pub fn process_loaded_tile_maps(
mut commands: Commands,
mut map_events: MessageReader<AssetEvent<LdtkMap>>,
maps: Res<Assets<LdtkMap>>,
mut query: Query<(Entity, &LdtkMapHandle, &LdtkMapConfig)>,
new_maps: Query<&LdtkMapHandle, Added<LdtkMapHandle>>,
) {
let mut changed_maps = Vec::<AssetId<LdtkMap>>::default();
for event in map_events.read() {
match event {
AssetEvent::Added { id } => {
log::info!("Map added!");
changed_maps.push(*id);
}
AssetEvent::Modified { id } => {
log::info!("Map changed!");
changed_maps.push(*id);
}
AssetEvent::Removed { id } => {
log::info!("Map removed!");
changed_maps.retain(|changed_handle| changed_handle == id);
}
_ => continue,
}
}
for new_map_handle in new_maps.iter() {
changed_maps.push(new_map_handle.0.id());
}
for changed_map in changed_maps.iter() {
for (entity, map_handle, map_config) in query.iter_mut() {
if map_handle.0.id() != *changed_map {
continue;
}
if let Some(ldtk_map) = maps.get(&map_handle.0) {
commands.entity(entity).despawn_descendants();
let mut tilesets = HashMap::new();
ldtk_map.project.defs.tilesets.iter().for_each(|tileset| {
tilesets.insert(
tileset.uid,
(
ldtk_map.tilesets.get(&tileset.uid).unwrap().clone(),
tileset,
),
);
});
let default_grid_size = ldtk_map.project.default_grid_size;
let level = &ldtk_map.project.levels[map_config.selected_level];
let map_tile_count_x = (level.px_wid / default_grid_size) as u32;
let map_tile_count_y = (level.px_hei / default_grid_size) as u32;
let size = TilemapSize {
x: map_tile_count_x,
y: map_tile_count_y,
};
for (layer_id, layer) in level
.layer_instances
.as_ref()
.unwrap()
.iter()
.rev()
.enumerate()
{
if let Some(uid) = layer.tileset_def_uid {
let (texture, tileset) = tilesets.get(&uid).unwrap().clone();
let tile_size = TilemapTileSize {
x: tileset.tile_grid_size as f32,
y: tileset.tile_grid_size as f32,
};
let map_entity = commands.spawn_empty().id();
let mut storage = TileStorage::empty(size);
let mut children = vec![];
for tile in layer.grid_tiles.iter().chain(layer.auto_layer_tiles.iter()) {
let mut position = TilePos {
x: (tile.px[0] / default_grid_size) as u32,
y: (tile.px[1] / default_grid_size) as u32,
};
position.y = map_tile_count_y - position.y - 1;
let flip = TileFlip {
x: tile.f & 0b1 == 0b1,
y: tile.f & 0b10 == 0b10,
d: false,
};
let tile_entity = commands
.spawn(TileBundle {
position,
tilemap_id: TilemapId(map_entity),
texture_index: TileTextureIndex(tile.t as u32),
flip,
..default()
})
.id();
storage.set(&position, tile_entity);
children.push(tile_entity);
}
let grid_size = tile_size.into();
let map_type = TilemapType::default();
commands
.entity(map_entity)
.insert((
TilemapBundle {
grid_size,
map_type,
size,
storage,
texture: TilemapTexture::Single(texture),
tile_size,
anchor: TilemapAnchor::TopLeft,
transform: Transform::from_xyz(0.0, 0.0, layer_id as f32),
..default()
},
))
.add_children(&children)
.set_parent(entity);
}
}
}
}
}
}