use std::io::Cursor;
use std::path::Path;
use std::sync::Arc;
use bevy::log::{info, warn};
use bevy::{
asset::{AssetLoader, io::Reader},
platform::collections::HashMap,
prelude::{
Added, Asset, AssetApp, AssetEvent, AssetId, Assets, Bundle, Commands, Component, Entity,
GlobalTransform, Handle, Image, MessageReader, Plugin, Query, Res, Transform, Update,
},
reflect::TypePath,
};
use bevy_ecs_tilemap::prelude::*;
use thiserror::Error;
#[allow(dead_code)]
#[derive(Default)]
pub struct TiledMapPlugin;
impl Plugin for TiledMapPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.init_asset::<TiledMap>()
.register_asset_loader(TiledLoader)
.add_systems(Update, process_loaded_maps);
}
}
#[allow(dead_code)]
#[derive(TypePath, Asset)]
pub struct TiledMap {
pub map: tiled::Map,
pub tilemap_textures: HashMap<usize, TilemapTexture>,
#[cfg(not(feature = "atlas"))]
pub tile_image_offsets: HashMap<(usize, tiled::TileId), u32>,
}
#[allow(dead_code)]
#[derive(Component, Default)]
pub struct TiledLayersStorage {
pub storage: HashMap<u32, Entity>,
}
#[allow(dead_code)]
#[derive(Component, Default)]
pub struct TiledMapHandle(pub Handle<TiledMap>);
#[allow(dead_code)]
#[derive(Default, Bundle)]
pub struct TiledMapBundle {
pub tiled_map: TiledMapHandle,
pub storage: TiledLayersStorage,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub render_settings: TilemapRenderSettings,
}
#[allow(dead_code)]
struct BytesResourceReader {
bytes: Arc<[u8]>,
}
#[allow(dead_code)]
impl BytesResourceReader {
fn new(bytes: &[u8]) -> Self {
Self {
bytes: Arc::from(bytes),
}
}
}
impl tiled::ResourceReader for BytesResourceReader {
type Resource = Cursor<Arc<[u8]>>;
type Error = std::io::Error;
fn read_from(&mut self, _path: &Path) -> std::result::Result<Self::Resource, Self::Error> {
Ok(Cursor::new(self.bytes.clone()))
}
}
#[allow(dead_code)]
#[derive(TypePath)]
pub struct TiledLoader;
#[allow(dead_code)]
#[derive(Debug, Error)]
pub enum TiledAssetLoaderError {
#[error("Could not load Tiled file: {0}")]
Io(#[from] std::io::Error),
}
impl AssetLoader for TiledLoader {
type Asset = TiledMap;
type Settings = ();
type Error = TiledAssetLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut bevy::asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let mut loader = tiled::Loader::with_cache_and_reader(
tiled::DefaultResourceCache::new(),
BytesResourceReader::new(&bytes),
);
let map = loader
.load_tmx_map(load_context.path().path())
.map_err(|e| std::io::Error::other(format!("Could not load TMX map: {e}")))?;
let mut tilemap_textures = HashMap::default();
#[cfg(not(feature = "atlas"))]
let mut tile_image_offsets = HashMap::default();
for (tileset_index, tileset) in map.tilesets().iter().enumerate() {
let tilemap_texture = match &tileset.image {
None => {
#[cfg(feature = "atlas")]
{
info!(
"Skipping image collection tileset '{}' which is incompatible with atlas feature",
tileset.name
);
continue;
}
#[cfg(not(feature = "atlas"))]
{
let mut tile_images: Vec<Handle<Image>> = Vec::new();
for (tile_id, tile) in tileset.tiles() {
if let Some(img) = &tile.image {
let asset_path = load_context
.path()
.resolve_embed(&img.source.to_string_lossy())
.expect("The asset load context was empty.");
info!(
"Loading tile image from {asset_path:?} as image ({tileset_index}, {tile_id})"
);
let texture: Handle<Image> = load_context.load(asset_path.clone());
tile_image_offsets
.insert((tileset_index, tile_id), tile_images.len() as u32);
tile_images.push(texture.clone());
}
}
TilemapTexture::Vector(tile_images)
}
}
Some(img) => {
let asset_path = load_context
.path()
.resolve_embed(&img.source.to_string_lossy())
.expect("The asset load context was empty.");
info!(?asset_path);
let texture: Handle<Image> = load_context.load(asset_path.clone());
TilemapTexture::Single(texture.clone())
}
};
tilemap_textures.insert(tileset_index, tilemap_texture);
}
let asset_map = TiledMap {
map,
tilemap_textures,
#[cfg(not(feature = "atlas"))]
tile_image_offsets,
};
info!("Loaded map: {}", load_context.path());
Ok(asset_map)
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &["tmx"];
EXTENSIONS
}
}
#[allow(dead_code)]
fn process_loaded_maps(
mut commands: Commands,
mut map_events: MessageReader<AssetEvent<TiledMap>>,
maps: Res<Assets<TiledMap>>,
tile_storage_query: Query<(Entity, &TileStorage)>,
mut map_query: Query<(
&TiledMapHandle,
&mut TiledLayersStorage,
&TilemapRenderSettings,
)>,
new_maps: Query<&TiledMapHandle, Added<TiledMapHandle>>,
) {
let mut changed_maps = Vec::<AssetId<TiledMap>>::default();
for event in map_events.read() {
match event {
AssetEvent::Added { id } => {
info!("Map added!");
changed_maps.push(*id);
}
AssetEvent::Modified { id } => {
info!("Map changed!");
changed_maps.push(*id);
}
AssetEvent::Removed { id } => {
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 (map_handle, mut layer_storage, render_settings) in map_query.iter_mut() {
if map_handle.0.id() != *changed_map {
continue;
}
if let Some(tiled_map) = maps.get(&map_handle.0) {
for layer_entity in layer_storage.storage.values() {
if let Ok((_, layer_tile_storage)) = tile_storage_query.get(*layer_entity) {
for tile in layer_tile_storage.iter().flatten() {
commands.entity(*tile).despawn()
}
}
}
for (tileset_index, tileset) in tiled_map.map.tilesets().iter().enumerate() {
let Some(tilemap_texture) = tiled_map.tilemap_textures.get(&tileset_index)
else {
warn!("Skipped creating layer with missing tilemap textures.");
continue;
};
let tile_size = TilemapTileSize {
x: tileset.tile_width as f32,
y: tileset.tile_height as f32,
};
let tile_spacing = TilemapSpacing {
x: tileset.spacing as f32,
y: tileset.spacing as f32,
};
for (layer_index, layer) in tiled_map.map.layers().enumerate() {
let offset_x = layer.offset_x;
let offset_y = layer.offset_y;
let tiled::LayerType::Tiles(tile_layer) = layer.layer_type() else {
info!(
"Skipping layer {} because only tile layers are supported.",
layer.id()
);
continue;
};
let tiled::TileLayer::Finite(layer_data) = tile_layer else {
info!(
"Skipping layer {} because only finite layers are supported.",
layer.id()
);
continue;
};
let map_size = TilemapSize {
x: tiled_map.map.width,
y: tiled_map.map.height,
};
let grid_size = TilemapGridSize {
x: tiled_map.map.tile_width as f32,
y: tiled_map.map.tile_height as f32,
};
let map_type = match tiled_map.map.orientation {
tiled::Orientation::Hexagonal => {
TilemapType::Hexagon(HexCoordSystem::Row)
}
tiled::Orientation::Isometric => {
TilemapType::Isometric(IsoCoordSystem::Diamond)
}
tiled::Orientation::Staggered => {
TilemapType::Isometric(IsoCoordSystem::Staggered)
}
tiled::Orientation::Orthogonal => TilemapType::Square,
};
let mut tile_storage = TileStorage::empty(map_size);
let layer_entity = commands.spawn_empty().id();
for x in 0..map_size.x {
for y in 0..map_size.y {
let mapped_y = tiled_map.map.height - 1 - y;
let mapped_x = x as i32;
let mapped_y = mapped_y as i32;
let layer_tile = match layer_data.get_tile(mapped_x, mapped_y) {
Some(t) => t,
None => {
continue;
}
};
if tileset_index != layer_tile.tileset_index() {
continue;
}
let layer_tile_data =
match layer_data.get_tile_data(mapped_x, mapped_y) {
Some(d) => d,
None => {
continue;
}
};
let texture_index = match tilemap_texture {
TilemapTexture::Single(_) => layer_tile.id(),
#[cfg(not(feature = "atlas"))]
TilemapTexture::Vector(_) =>
*tiled_map.tile_image_offsets.get(&(tileset_index, layer_tile.id()))
.expect("The offset into to image vector should have been saved during the initial load."),
#[cfg(not(feature = "atlas"))]
_ => unreachable!()
};
let tile_pos = TilePos { x, y };
let tile_entity = commands
.spawn(TileBundle {
position: tile_pos,
tilemap_id: TilemapId(layer_entity),
texture_index: TileTextureIndex(texture_index),
flip: TileFlip {
x: layer_tile_data.flip_h,
y: layer_tile_data.flip_v,
d: layer_tile_data.flip_d,
},
..Default::default()
})
.id();
tile_storage.set(&tile_pos, tile_entity);
}
}
commands.entity(layer_entity).insert(TilemapBundle {
grid_size,
size: map_size,
storage: tile_storage,
texture: tilemap_texture.clone(),
tile_size,
spacing: tile_spacing,
anchor: TilemapAnchor::Center,
transform: Transform::from_xyz(offset_x, -offset_y, layer_index as f32),
map_type,
render_settings: *render_settings,
..Default::default()
});
layer_storage
.storage
.insert(layer_index as u32, layer_entity);
}
}
}
}
}
}