bevy_ecs_tiled 0.11.1

A Bevy plugin for loading Tiled maps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! Asset types and asset loader for Tiled maps.
//!
//! This module defines asset structures, asset events, and the asset loader implementation for importing Tiled maps
//! into Bevy's asset system.

use crate::{prelude::*, tiled::helpers::iso_projection};
use bevy::{platform::collections::HashMap, prelude::*};
use std::fmt;

#[derive(Default, Debug)]
pub(crate) struct TiledMapTileset {
    /// Does this tileset can be used for tiles layer ?
    ///
    /// A tileset can be used for tiles layer only if all the images it contains have the
    /// same dimensions (restriction from bevy_ecs_tilemap).
    pub(crate) usable_for_tiles_layer: bool,
    /// Tileset texture (ie. a single image or an images collection)
    pub(crate) tilemap_texture: TilemapTexture,
    /// The [`TextureAtlasLayout`] handle associated to each tileset, if any.
    pub(crate) texture_atlas_layout_handle: Option<Handle<TextureAtlasLayout>>,
    /// The offset into the tileset_images for each tile id within each tileset.
    #[cfg(not(feature = "atlas"))]
    pub(crate) tile_image_offsets: HashMap<tiled::TileId, u32>,
}

/// Tiled map [`Asset`].
///
/// [`Asset`] holding Tiled map informations.
#[derive(TypePath, Asset)]
pub struct TiledMapAsset {
    /// The raw Tiled map data
    pub map: tiled::Map,
    /// Map size in tiles
    pub tilemap_size: TilemapSize,
    /// The largest tile size, in pixels, found among all tiles from this map
    ///
    /// You should only rely on this value for "map-level" concerns.
    /// If you want to get the actual size of a given tile, you should instead
    /// use the [`tile_size`] function.
    pub largest_tile_size: TilemapTileSize,
    /// Map bounding box, unanchored, in pixels.
    ///
    /// Origin it map bottom-left.
    /// Minimum is `(0., 0.)`
    /// Maximum is `(map_size.x, map_size.y)`
    pub rect: Rect,
    /// Offset to apply on Tiled coordinates when converting to Bevy coordinates
    ///
    /// Our computations for converting coordinates assume that Tiled origin (ie. point [0, 0])
    /// is always in the top-left corner of the map. This is not the case for infinite maps where
    /// map origin is at the top-left corner of chunk (0, 0) and we can have chunk with negative indexes
    pub(crate) tiled_offset: Vec2,
    /// Top-left chunk index
    ///
    /// For finite maps, always (0, 0)
    pub(crate) topleft_chunk: (i32, i32),
    /// Bottom-right chunk index
    ///
    /// For finite maps, always (0, 0)
    pub(crate) bottomright_chunk: (i32, i32),
    /// HashMap of the map tilesets
    ///
    /// Key is a unique label to identify the Tiled tileset within the map.
    /// See [`tileset_label`](crate::tiled::map::loader::tileset_label) function.
    pub(crate) tilesets: HashMap<String, TiledMapTileset>,
    /// HashMap of the label to tilesets
    ///
    /// Key is the Tiled tileset index
    pub(crate) tilesets_label_by_index: HashMap<u32, String>,
    /// HashMap of the images used in the map
    ///
    /// Key is the layer id of the image layer using this image
    pub(crate) images: HashMap<u32, Handle<Image>>,
    /// Map properties
    #[cfg(feature = "user_properties")]
    pub(crate) properties: crate::tiled::properties::load::DeserializedMapProperties,
}

impl TiledMapAsset {
    /// Raw conversion between a Tiled position and Bevy world space
    ///
    /// # Arguments
    /// * `anchor` - The [`TilemapAnchor`] used for the map.
    /// * `tiled_position` - Tiled position.
    ///
    /// # Returns
    /// * `Vec2` - The corresponding world position.
    pub fn world_space_from_tiled_position(
        &self,
        anchor: &TilemapAnchor,
        tiled_position: Vec2,
    ) -> Vec2 {
        let map_size = self.tilemap_size;
        let tile_size = self.largest_tile_size;
        let map_height = self.rect.height();
        let grid_size = grid_size_from_map(&self.map);
        let map_type = tilemap_type_from_map(&self.map);
        let mut offset = anchor.as_offset(&map_size, &grid_size, &tile_size, &map_type);
        offset.x -= grid_size.x / 2.0;
        offset.y -= grid_size.y / 2.0;
        offset
            + match map_type {
                TilemapType::Square => {
                    self.tiled_offset
                        + Vec2 {
                            x: tiled_position.x,
                            y: map_height - tiled_position.y,
                        }
                }
                TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => {
                    self.tiled_offset
                        + Vec2 {
                            x: tiled_position.x,
                            y: map_height + grid_size.y / 2. - tiled_position.y,
                        }
                }
                TilemapType::Hexagon(HexCoordSystem::ColumnEven) => {
                    self.tiled_offset
                        + Vec2 {
                            x: tiled_position.x,
                            y: map_height - tiled_position.y,
                        }
                }
                TilemapType::Hexagon(HexCoordSystem::RowOdd) => {
                    self.tiled_offset
                        + Vec2 {
                            x: tiled_position.x,
                            y: map_height + grid_size.y / 4. - tiled_position.y,
                        }
                }
                TilemapType::Hexagon(HexCoordSystem::RowEven) => {
                    self.tiled_offset
                        + Vec2 {
                            x: tiled_position.x - grid_size.x / 2.,
                            y: map_height + grid_size.y / 4. - tiled_position.y,
                        }
                }
                TilemapType::Isometric(IsoCoordSystem::Diamond) => {
                    let position =
                        iso_projection(tiled_position + self.tiled_offset, &map_size, &grid_size);
                    Vec2 {
                        x: position.x,
                        y: map_height / 2. - grid_size.y / 2. - position.y,
                    }
                }
                TilemapType::Isometric(IsoCoordSystem::Staggered) => {
                    panic!("Isometric (Staggered) map is not supported");
                }
                _ => unreachable!(),
            }
    }

    /// Iterates over all tiles in the given [`tiled::TileLayer`], invoking a callback for each tile.
    ///
    /// This function abstracts over both finite and infinite Tiled map layers, providing a unified
    /// way to visit every tile in a layer. For each tile, the provided closure is called with:
    /// - the [`tiled::LayerTile`] (tile instance)
    /// - a reference to the [`tiled::LayerTileData`] (tile metadata)
    /// - the [`TilePos`] (tile position in Bevy coordinates)
    /// - the [`IVec2`] (tile position in Tiled chunk coordinates)
    ///
    /// The coordinate conversion ensures that the tile positions are consistent with Bevy's coordinate system,
    /// including Y-axis inversion and chunk offset handling for infinite maps.
    ///
    /// # Arguments
    /// * `tiles_layer` - The Tiled tile layer to iterate over (finite or infinite).
    /// * `f` - A closure to call for each tile, with signature:
    ///   `(LayerTile, &LayerTileData, TilePos, IVec2)`
    ///
    /// # Example
    /// ```rust,no_run
    /// use bevy_ecs_tiled::prelude::*;
    ///
    /// fn print_tile_positions(asset: &TiledMapAsset, layer: &tiled::TileLayer) {
    ///     asset.for_each_tile(layer, |tile, data, tile_pos, chunk_pos| {
    ///         println!("Tile at Bevy pos: {:?}, chunk pos: {:?}", tile_pos, chunk_pos);
    ///     });
    /// }
    /// ```
    ///
    /// # Notes
    /// - For infinite maps, chunk positions are shifted so that the top-left chunk is at (0, 0),
    ///   and negative tile coordinates are avoided.
    /// - The Y coordinate is inverted to match Bevy's coordinate system (origin at bottom-left).
    pub fn for_each_tile<'a, F>(&'a self, tiles_layer: &tiled::TileLayer<'a>, mut f: F)
    where
        F: FnMut(tiled::LayerTile<'a>, &tiled::LayerTileData, TilePos, IVec2),
    {
        let tilemap_size = self.tilemap_size;
        match tiles_layer {
            tiled::TileLayer::Finite(layer) => {
                for x in 0..tilemap_size.x {
                    for y in 0..tilemap_size.y {
                        // Transform TMX coords into bevy coords.
                        let mapped_y = tilemap_size.y - 1 - y;
                        let mapped_x = x as i32;
                        let mapped_y = mapped_y as i32;

                        let Some(layer_tile) = layer.get_tile(mapped_x, mapped_y) else {
                            continue;
                        };
                        let Some(layer_tile_data) = layer.get_tile_data(mapped_x, mapped_y) else {
                            continue;
                        };

                        f(
                            layer_tile,
                            layer_tile_data,
                            TilePos::new(x, y),
                            IVec2::new(mapped_x, mapped_y),
                        );
                    }
                }
            }
            tiled::TileLayer::Infinite(layer) => {
                for (chunk_pos, chunk) in layer.chunks() {
                    // bevy_ecs_tilemap doesn't support negative tile coordinates, so shift all chunks
                    // such that the top-left chunk is at (0, 0).
                    let chunk_pos_mapped = (
                        chunk_pos.0 - self.topleft_chunk.0,
                        chunk_pos.1 - self.topleft_chunk.1,
                    );

                    for x in 0..tiled::ChunkData::WIDTH {
                        for y in 0..tiled::ChunkData::HEIGHT {
                            // Invert y to match bevy coordinates.
                            let Some(layer_tile) = chunk.get_tile(x as i32, y as i32) else {
                                continue;
                            };
                            let Some(layer_tile_data) = chunk.get_tile_data(x as i32, y as i32)
                            else {
                                continue;
                            };

                            let index = IVec2 {
                                x: chunk_pos_mapped.0 * tiled::ChunkData::WIDTH as i32 + x as i32,
                                y: chunk_pos_mapped.1 * tiled::ChunkData::HEIGHT as i32 + y as i32,
                            };

                            f(
                                layer_tile,
                                layer_tile_data,
                                TilePos {
                                    x: index.x as u32,
                                    y: tilemap_size.y - 1 - index.y as u32,
                                },
                                index,
                            );
                        }
                    }
                }
            }
        }
    }

    /// Returns the world position of a Tiled [`tiled::Object`] relative to its parent [`TiledLayer::Objects`] entity.
    ///
    /// The returned position corresponds to the object's top-left anchor in world coordinates, taking into account
    /// the map's anchor, grid size, and coordinate system. This is equivalent to using the object's [`Transform`] component
    /// to get its position, but is provided for convenience and consistency with other Tiled coordinate conversions.
    ///
    /// # Arguments
    /// * `object` - The Tiled object whose position to compute.
    /// * `anchor` - The [`TilemapAnchor`] used for the map.
    ///
    /// # Returns
    /// * `Vec2` - The object's world position relative to its parent layer entity.
    pub fn object_relative_position(&self, object: &tiled::Object, anchor: &TilemapAnchor) -> Vec2 {
        self.world_space_from_tiled_position(anchor, Vec2::new(object.x, object.y))
    }

    /// Returns the center of a Tiled object in world coordinates, taking into account map type and transform.
    ///
    /// # Arguments
    /// * `tiled_object` - The Tiled object to compute the center for.
    /// * `transform` - The global transform to apply to the object.
    ///
    /// # Returns
    /// * `Option<geo::Coord<f32>>` - The world-space center of the object, or `None` if not applicable.
    pub fn object_center(
        &self,
        tiled_object: &TiledObject,
        transform: &GlobalTransform,
    ) -> Option<geo::Coord<f32>> {
        tiled_object.center(
            transform,
            matches!(tilemap_type_from_map(&self.map), TilemapType::Isometric(..)),
            &self.tilemap_size,
            &grid_size_from_map(&self.map),
            self.tiled_offset,
        )
    }

    /// Returns the vertices of a Tiled object in world coordinates, taking into account map type and transform.
    ///
    /// # Arguments
    /// * `tiled_object` - The Tiled object to compute the vertices for.
    /// * `transform` - The global transform to apply to the object.
    ///
    /// # Returns
    /// * `Vec<geo::Coord<f32>>` - The world-space vertices of the object.
    pub fn object_vertices(
        &self,
        tiled_object: &TiledObject,
        transform: &GlobalTransform,
    ) -> Vec<geo::Coord<f32>> {
        tiled_object.vertices(
            transform,
            matches!(tilemap_type_from_map(&self.map), TilemapType::Isometric(..)),
            &self.tilemap_size,
            &grid_size_from_map(&self.map),
            self.tiled_offset,
        )
    }

    /// Returns the object's geometry as a [`geo::LineString`] in world coordinates, if applicable.
    ///
    /// # Arguments
    /// * `tiled_object` - The Tiled object to compute the line string for.
    /// * `transform` - The global transform to apply to the object.
    ///
    /// # Returns
    /// * `Option<geo::LineString<f32>>` - The object's geometry as a line string, or `None` if not applicable.
    pub fn object_line_string(
        &self,
        tiled_object: &TiledObject,
        transform: &GlobalTransform,
    ) -> Option<geo::LineString<f32>> {
        tiled_object.line_string(
            transform,
            matches!(tilemap_type_from_map(&self.map), TilemapType::Isometric(..)),
            &self.tilemap_size,
            &grid_size_from_map(&self.map),
            self.tiled_offset,
        )
    }

    /// Returns the object's geometry as a [`geo::Polygon`] in world coordinates, if applicable.
    ///
    /// # Arguments
    /// * `tiled_object` - The Tiled object to compute the polygon for.
    /// * `transform` - The global transform to apply to the object.
    ///
    /// # Returns
    /// * `Option<geo::Polygon<f32>>` - The object's geometry as a polygon, or `None` if not applicable.
    pub fn object_polygon(
        &self,
        tiled_object: &TiledObject,
        transform: &GlobalTransform,
    ) -> Option<geo::Polygon<f32>> {
        tiled_object.polygon(
            transform,
            matches!(tilemap_type_from_map(&self.map), TilemapType::Isometric(..)),
            &self.tilemap_size,
            &grid_size_from_map(&self.map),
            self.tiled_offset,
        )
    }

    /// Returns the world position (center) of a tile relative to its parent [`TiledTilemap`] [`Entity`].
    ///
    /// This function computes the world-space position of a tile, given its [`TilePos`], tile size, and map anchor.
    /// It is especially useful because tiles do not have their own [`Transform`] component, so their world position must be calculated manually.
    ///
    /// The returned position is the center of the tile in world coordinates, taking into account the map's size,
    /// grid size, tile size, map type (orthogonal, isometric, hex), and anchor. This ensures correct placement
    /// regardless of map orientation or coordinate system.
    ///
    /// # Arguments
    /// * `tile_pos` - The tile's position in Bevy tile coordinates (origin at bottom-left).
    /// * `tile_size` - The size of the tile in pixels.
    /// * `anchor` - The [`TilemapAnchor`] used for the map.
    ///
    /// # Returns
    /// * `Vec2` - The world-space position of the tile's center, relative to its parent tilemap entity.
    ///
    /// # Example
    /// ```rust,no_run
    /// use bevy::prelude::*;
    /// use bevy_ecs_tiled::prelude::*;
    ///
    /// fn tile_position_system(
    ///     assets: Res<Assets<TiledMapAsset>>,
    ///     map_query: Query<(&TiledMap, &TiledMapStorage, &TilemapAnchor)>,
    ///     tile_query: Query<(Entity, &TilePos, &ChildOf), With<TiledTile>>,
    ///     tilemap_query: Query<&GlobalTransform, With<TiledTilemap>>,
    /// ) {
    ///     for (tiled_map, storage, anchor) in map_query.iter() {
    ///         let Some(map_asset) = assets.get(&tiled_map.0) else { continue; };
    ///         for (_, entities) in storage.tiles() {
    ///             for entity in entities {
    ///                 let Ok((_, tile_pos, child_of)) = tile_query.get(*entity) else { continue; };
    ///                 let Some(tile) = storage.get_tile(&map_asset.map, *entity) else { continue; };
    ///                 // Compute the tile's world position (center)
    ///                 let tile_rel_pos = map_asset.tile_relative_position(
    ///                     tile_pos,
    ///                     &tile_size(&tile),
    ///                     anchor
    ///                 );
    ///                 // To get the tile's global transform, combine with the parent tilemap's transform
    ///                 let Ok(parent_transform) = tilemap_query.get(child_of.parent()) else { continue; };
    ///                 let tile_transform = *parent_transform * Transform::from_translation(tile_rel_pos.extend(0.));
    ///             }
    ///         }
    ///     }
    /// }
    /// ```
    ///
    /// # Notes
    /// - The returned position is relative to the parent tilemap entity, not global coordinates.
    /// - For global/world coordinates, combine with the parent tilemap's [`GlobalTransform`].
    pub fn tile_relative_position(
        &self,
        tile_pos: &TilePos,
        tile_size: &TilemapTileSize,
        anchor: &TilemapAnchor,
    ) -> Vec2 {
        tile_pos.center_in_world(
            &self.tilemap_size,
            &grid_size_from_map(&self.map),
            tile_size,
            &tilemap_type_from_map(&self.map),
            anchor,
        )
    }
}

impl fmt::Debug for TiledMapAsset {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("TiledMapAsset")
            .field("map", &self.map)
            .field("tilemap_size", &self.tilemap_size)
            .field("largest_tile_size", &self.largest_tile_size)
            .field("rect", &self.rect)
            .field("tiled_offset", &self.tiled_offset)
            .field("topleft_chunk", &self.topleft_chunk)
            .field("bottomright_chunk", &self.bottomright_chunk)
            .finish()
    }
}

pub(crate) fn plugin(app: &mut App) {
    app.init_asset::<TiledMapAsset>();
}