diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6cd240..0ab4b3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
## [unreleased]
+### Features
+
+- Add dedicated marker for Tiled objects (#22) and for objects layer, group layer and images layer
+- Regroup tilemaps of the same tile layer using different tilesets under a common parent entity `TiledMapTileLayerForTileset`
+
+### Bugfixes
+
+- Prevent duplicating objects when there are multiple tilesets (#28)
+
## v0.3.6
### Bugfixes
diff --git a/Cargo.toml b/Cargo.toml
index ab742b1..6a82c0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -115,3 +115,6 @@ required-features = ["user_properties", "avian"]
[[example]]
name = "isometric_map"
+
+[[example]]
+name = "multiple_tilesets"
diff --git a/assets/Tileset2.tsx b/assets/Tileset2.tsx
new file mode 100644
index 0000000..6618b06
--- /dev/null
+++ b/assets/Tileset2.tsx
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/multiple_tilesets.tmx b/assets/multiple_tilesets.tmx
new file mode 100644
index 0000000..89e2e9f
--- /dev/null
+++ b/assets/multiple_tilesets.tmx
@@ -0,0 +1,24 @@
+
+
diff --git a/examples/README.md b/examples/README.md
index 7fd3bda..27749b9 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -28,3 +28,4 @@ In most of the examples, you can use the following action keys:
| `infinite_avian` | `avian` | This example shows an infinite orthogonal map with an external tileset and Avian2D physics. |
| `controller_avian` | `avian` | This example shows a simple player-controlled object using Avian2D physics. You can move the object using arrow keys. |
| `user_properties_avian` | `user_properties`, `avian` | This example shows how to map custom tiles / objects properties from Tiled to Bevy Components and manually spawn Avian colliders from them. |
+| `multiple_tilesets` | None | This example shows a finite orthogonal map with multiple external tilesets. |
diff --git a/examples/multiple_tilesets.rs b/examples/multiple_tilesets.rs
new file mode 100644
index 0000000..bfe3377
--- /dev/null
+++ b/examples/multiple_tilesets.rs
@@ -0,0 +1,30 @@
+//! This example shows a finite orthogonal map with multiple external tilesets.
+
+use bevy::prelude::*;
+use bevy_ecs_tiled::prelude::*;
+use bevy_ecs_tilemap::prelude::*;
+
+mod helper;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .add_plugins(TilemapPlugin)
+ .add_plugins(TiledMapPlugin)
+ .add_plugins(helper::HelperPlugin)
+ .add_systems(Startup, startup)
+ .run();
+}
+
+fn startup(mut commands: Commands, asset_server: Res) {
+ commands.spawn(Camera2dBundle::default());
+
+ // For simplicity sake, we use two tilesets which actually use the same images
+ // However, we can verify with the inspector that the map actually use tiles
+ // from both tilesets
+ let map_handle: Handle = asset_server.load("multiple_tilesets.tmx");
+ commands.spawn(TiledMapBundle {
+ tiled_map: map_handle,
+ ..Default::default()
+ });
+}
diff --git a/src/components.rs b/src/components.rs
index 13fb407..f98a514 100644
--- a/src/components.rs
+++ b/src/components.rs
@@ -25,14 +25,30 @@ pub struct TiledMapLayer {
#[derive(Component)]
pub struct TiledMapTileLayer;
+/// Marker component for a Tiled map tile layer for a given tileset.
+#[derive(Component)]
+pub struct TiledMapTileLayerForTileset;
+
/// Marker component for a Tiled map object layer.
#[derive(Component)]
pub struct TiledMapObjectLayer;
+/// Marker component for a Tiled map group layer.
+#[derive(Component)]
+pub struct TiledMapGroupLayer;
+
+/// Marker component for a Tiled map image layer.
+#[derive(Component)]
+pub struct TiledMapImageLayer;
+
/// Marker component for a Tiled map tile.
#[derive(Component)]
pub struct TiledMapTile;
+/// Marker component for a Tiled map object.
+#[derive(Component)]
+pub struct TiledMapObject;
+
#[derive(Default, Clone)]
pub enum MapPositioning {
#[default]
diff --git a/src/loader.rs b/src/loader.rs
index f7ff4f6..5b6923e 100644
--- a/src/loader.rs
+++ b/src/loader.rs
@@ -44,7 +44,9 @@ use crate::physics::{insert_object_colliders, insert_tile_colliders};
use crate::prelude::*;
use bevy_ecs_tilemap::prelude::*;
-use tiled::{ChunkData, FiniteTileLayer, InfiniteTileLayer, LayerType, Tile};
+use tiled::{
+ ChunkData, FiniteTileLayer, InfiniteTileLayer, Layer, LayerType, ObjectLayer, Tile, TileLayer,
+};
#[derive(Default)]
pub struct TiledMapPlugin;
@@ -159,7 +161,7 @@ impl AssetLoader for TiledLoader {
for (tile_id, tile) in tileset.tiles() {
if let Some(img) = &tile.image {
let asset_path = AssetPath::from(img.source.clone());
- log::info!("Loading tile image from {asset_path:?} as image ({tileset_index}, {tile_id})");
+ log::debug!("Loading tile image from {asset_path:?} as image ({tileset_index}, {tile_id})");
let texture: Handle = load_context.load(asset_path.clone());
tile_image_offsets
.insert((tileset_index, tile_id), tile_images.len() as u32);
@@ -380,10 +382,148 @@ fn load_map(
)))
.insert(TiledMapMarker);
- #[cfg(feature = "physics")]
- let collision_layer_names =
- crate::prelude::ObjectNameFilter::from(&tiled_settings.collision_layer_names);
+ let map_type = match tiled_map.map.orientation {
+ tiled::Orientation::Hexagonal => match tiled_map.map.stagger_axis {
+ tiled::StaggerAxis::X if tiled_map.map.stagger_index == tiled::StaggerIndex::Even => {
+ TilemapType::Hexagon(HexCoordSystem::ColumnOdd)
+ }
+ tiled::StaggerAxis::X if tiled_map.map.stagger_index == tiled::StaggerIndex::Odd => {
+ TilemapType::Hexagon(HexCoordSystem::ColumnEven)
+ }
+ tiled::StaggerAxis::Y if tiled_map.map.stagger_index == tiled::StaggerIndex::Even => {
+ TilemapType::Hexagon(HexCoordSystem::RowOdd)
+ }
+ tiled::StaggerAxis::Y if tiled_map.map.stagger_index == tiled::StaggerIndex::Odd => {
+ TilemapType::Hexagon(HexCoordSystem::RowEven)
+ }
+ _ => unreachable!(),
+ },
+ tiled::Orientation::Isometric => TilemapType::Isometric(IsoCoordSystem::Diamond),
+ tiled::Orientation::Staggered => TilemapType::Isometric(IsoCoordSystem::Staggered),
+ tiled::Orientation::Orthogonal => TilemapType::Square,
+ };
+
+ 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,
+ };
+
+ // Order of the differents layers in the .TMX file is important:
+ // a layer appearing last in the .TMX should appear "on top" of previous layers
+ let mut offset_z = 0.;
+
+ // Once materials have been created/added we need to then create the layers.
+ for (layer_index, layer) in tiled_map.map.layers().enumerate() {
+ // Spawn layer entity and attach it to the map entity
+ let layer_entity = commands
+ .spawn((
+ TiledMapLayer {
+ map_handle_id: map_handle.id(),
+ },
+ TransformBundle::from_transform(Transform::from_xyz(0., 0., 0.)),
+ ))
+ .set_parent(map_entity)
+ .id();
+
+ // Increment Z offset
+ offset_z += 100.;
+
+ // Apply layer offset and MapPositioning setting
+ let offset_transform = Transform::from_xyz(layer.offset_x, -layer.offset_y, offset_z);
+ commands.entity(layer_entity).insert(SpatialBundle {
+ transform: match &tiled_settings.map_positioning {
+ MapPositioning::LayerOffset => offset_transform,
+ MapPositioning::Centered => {
+ get_tilemap_center_transform(
+ &map_size,
+ &grid_size,
+ &map_type,
+ layer_index as f32,
+ ) * offset_transform
+ }
+ },
+ ..default()
+ });
+ match layer.layer_type() {
+ LayerType::Tiles(tile_layer) => {
+ commands
+ .entity(layer_entity)
+ .insert(Name::new(format!("TiledMapTileLayer({})", layer.name)))
+ .insert(TiledMapTileLayer);
+ load_tiles_layer(
+ commands,
+ layer_entity,
+ layer,
+ tile_layer,
+ tiled_map,
+ &map_type,
+ &map_size,
+ &grid_size,
+ render_settings,
+ tiled_settings,
+ #[cfg(feature = "user_properties")]
+ custom_tiles_registry,
+ );
+ }
+ LayerType::Objects(object_layer) => {
+ commands
+ .entity(layer_entity)
+ .insert(Name::new(format!("TiledMapObjectLayer({})", layer.name)))
+ .insert(TiledMapObjectLayer);
+ load_objects_layer(
+ commands,
+ layer_entity,
+ layer,
+ object_layer,
+ &map_size,
+ &grid_size,
+ tiled_settings,
+ #[cfg(feature = "user_properties")]
+ objects_registry,
+ );
+ }
+ LayerType::Group(_group_layer) => {
+ commands
+ .entity(layer_entity)
+ .insert(Name::new(format!("TiledMapGroupLayer({})", layer.name)))
+ .insert(TiledMapGroupLayer);
+ // TODO: not implemented yet.
+ }
+ LayerType::Image(_image_layer) => {
+ commands
+ .entity(layer_entity)
+ .insert(Name::new(format!("TiledMapImageLayer({})", layer.name)))
+ .insert(TiledMapImageLayer);
+ // TODO: not implemented yet.
+ }
+ }
+
+ layer_storage
+ .storage
+ .insert(layer_index as u32, layer_entity);
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn load_tiles_layer(
+ commands: &mut Commands,
+ layer_entity: Entity,
+ layer: Layer,
+ tile_layer: TileLayer,
+ tiled_map: &TiledMap,
+ map_type: &TilemapType,
+ map_size: &TilemapSize,
+ grid_size: &TilemapGridSize,
+ render_settings: &TilemapRenderSettings,
+ tiled_settings: &TiledMapSettings,
+ #[cfg(feature = "user_properties")] custom_tiles_registry: &TiledCustomTileRegistry,
+) {
// The TilemapBundle requires that all tile images come exclusively from a single
// tiled texture or from a Vec of independent per-tile images. Furthermore, all of
// the per-tile images must be the same size. Since Tiled allows tiles of mixed
@@ -395,6 +535,8 @@ fn load_map(
continue;
};
+ let mut map_size = *map_size;
+
let tile_size = TilemapTileSize {
x: tileset.tile_width as f32,
y: tileset.tile_height as f32,
@@ -405,213 +547,75 @@ fn load_map(
y: tileset.spacing as f32,
};
- // Order of the differents layers in the .TMX file is important:
- // a layer appearing last in the .TMX should appear "on top" of previous layers
- let mut offset_z = 0.;
-
- // Once materials have been created/added we need to then create the layers.
- for (layer_index, layer) in tiled_map.map.layers().enumerate() {
- let mut offset_x = layer.offset_x;
- let mut offset_y = layer.offset_y;
- offset_z += 100.;
-
- let mut 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 => match tiled_map.map.stagger_axis {
- tiled::StaggerAxis::X
- if tiled_map.map.stagger_index == tiled::StaggerIndex::Even =>
- {
- TilemapType::Hexagon(HexCoordSystem::ColumnOdd)
- }
- tiled::StaggerAxis::X
- if tiled_map.map.stagger_index == tiled::StaggerIndex::Odd =>
- {
- TilemapType::Hexagon(HexCoordSystem::ColumnEven)
- }
- tiled::StaggerAxis::Y
- if tiled_map.map.stagger_index == tiled::StaggerIndex::Even =>
- {
- TilemapType::Hexagon(HexCoordSystem::RowOdd)
- }
- tiled::StaggerAxis::Y
- if tiled_map.map.stagger_index == tiled::StaggerIndex::Odd =>
- {
- TilemapType::Hexagon(HexCoordSystem::RowEven)
- }
- _ => unreachable!(),
- },
- tiled::Orientation::Isometric => TilemapType::Isometric(IsoCoordSystem::Diamond),
- tiled::Orientation::Staggered => TilemapType::Isometric(IsoCoordSystem::Staggered),
- tiled::Orientation::Orthogonal => TilemapType::Square,
- };
-
- let layer_entity = commands
- .spawn((
- TiledMapLayer {
- map_handle_id: map_handle.id(),
- },
- TransformBundle::from_transform(Transform::from_xyz(0., 0., 0.)),
- ))
- .set_parent(map_entity)
- .id();
-
- match layer.layer_type() {
- LayerType::Tiles(tile_layer) => {
- commands.entity(layer_entity).insert(TiledMapTileLayer);
- let tile_storage = match tile_layer {
- tiled::TileLayer::Finite(layer_data) => load_finite_tiles_layer(
- commands,
- layer_entity,
- &map_size,
- &grid_size,
- tiled_map,
- &layer_data,
- tileset_index,
- tilemap_texture,
- tiled_settings,
- #[cfg(feature = "user_properties")]
- custom_tiles_registry,
- ),
- tiled::TileLayer::Infinite(layer_data) => {
- let (storage, new_map_size, origin) = load_infinite_tiles_layer(
- commands,
- layer_entity,
- tiled_map,
- &grid_size,
- &layer_data,
- tileset_index,
- tilemap_texture,
- tiled_settings,
- #[cfg(feature = "user_properties")]
- custom_tiles_registry,
- );
- map_size = new_map_size;
- // log::info!("Infinite layer origin: {:?}", origin);
- offset_x += origin.0 * grid_size.x;
- offset_y -= origin.1 * grid_size.y;
- storage
- }
- };
-
- let offset_transform = Transform::from_xyz(
- offset_x + grid_size.x / 2.,
- -offset_y + grid_size.y / 2.,
- offset_z,
- );
- commands
- .entity(layer_entity)
- .insert(Name::new(format!("TiledMapTileLayer({})", layer.name)))
- .insert(TilemapBundle {
- grid_size,
- size: map_size,
- storage: tile_storage,
- texture: tilemap_texture.clone(),
- tile_size,
- spacing: tile_spacing,
- transform: match &tiled_settings.map_positioning {
- MapPositioning::LayerOffset => offset_transform,
- MapPositioning::Centered => {
- get_tilemap_center_transform(
- &map_size,
- &grid_size,
- &map_type,
- layer_index as f32,
- ) * offset_transform
- }
- },
- map_type,
- render_settings: *render_settings,
- ..Default::default()
- });
- }
- LayerType::Objects(object_layer) => {
- let offset_transform = Transform::from_xyz(offset_x, -offset_y, offset_z);
- commands
- .entity(layer_entity)
- .insert(Name::new(format!("TiledMapObjectLayer({})", layer.name)))
- .insert(match &tiled_settings.map_positioning {
- MapPositioning::LayerOffset => offset_transform,
- MapPositioning::Centered => {
- get_tilemap_center_transform(
- &map_size,
- &grid_size,
- &map_type,
- layer_index as f32,
- ) * offset_transform
- }
- });
-
- for object_data in object_layer.objects() {
- let _object_entity = commands
- .spawn(TransformBundle::from_transform(Transform::from_xyz(
- object_data.x,
- map_size.y as f32 * grid_size.y - object_data.y,
- 0.,
- )))
- .insert(Name::new(format!("Object({})", object_data.name)))
- .set_parent(layer_entity)
- .id();
-
- #[cfg(feature = "physics")]
- {
- if collision_layer_names.contains(&layer.name.trim().to_lowercase()) {
- insert_object_colliders(
- commands,
- _object_entity,
- &object_data,
- tiled_settings.collider_callback,
- );
- }
- }
-
- #[cfg(feature = "user_properties")]
- {
- if let Some(phantom) = objects_registry.get(&object_data.user_type) {
- phantom.initialize(
- commands,
- &TiledObjectCreated {
- entity: _object_entity,
- object_data: object_data.deref().clone(),
- map_size,
- },
- );
- } else {
- log::warn!(
- "Skipping unregistered object (name='{}' type='{}')",
- object_data.name,
- object_data.user_type
- );
- }
- }
- }
- }
- LayerType::Group(_group_layer) => {
- commands
- .entity(layer_entity)
- .insert(Name::new(format!("TiledMapGroupLayer({})", layer.name)));
- // TODO: not implemented yet.
- }
- LayerType::Image(_image_layer) => {
- commands
- .entity(layer_entity)
- .insert(Name::new(format!("TiledMapImageLayer({})", layer.name)));
- // TODO: not implemented yet.
- }
+ let mut offset_x = 0.;
+ let mut offset_y = 0.;
+
+ let layer_for_tileset_entity = commands
+ .spawn((
+ Name::new(format!(
+ "TiledMapTileLayerForTileset({}, {})",
+ layer.name, tileset.name
+ )),
+ TiledMapTileLayerForTileset,
+ ))
+ .set_parent(layer_entity)
+ .id();
+
+ let tile_storage = match tile_layer {
+ tiled::TileLayer::Finite(layer_data) => load_finite_tiles_layer(
+ commands,
+ layer_for_tileset_entity,
+ &map_size,
+ grid_size,
+ tiled_map,
+ &layer_data,
+ tileset_index,
+ tilemap_texture,
+ tiled_settings,
+ #[cfg(feature = "user_properties")]
+ custom_tiles_registry,
+ ),
+ tiled::TileLayer::Infinite(layer_data) => {
+ let (storage, new_map_size, origin) = load_infinite_tiles_layer(
+ commands,
+ layer_for_tileset_entity,
+ tiled_map,
+ grid_size,
+ &layer_data,
+ tileset_index,
+ tilemap_texture,
+ tiled_settings,
+ #[cfg(feature = "user_properties")]
+ custom_tiles_registry,
+ );
+ map_size = new_map_size;
+ // log::info!("Infinite layer origin: {:?}", origin);
+ offset_x += origin.0 * grid_size.x;
+ offset_y -= origin.1 * grid_size.y;
+ storage
}
+ };
- layer_storage
- .storage
- .insert(layer_index as u32, layer_entity);
- }
+ let offset_transform = Transform::from_xyz(
+ offset_x + grid_size.x / 2.,
+ -offset_y + grid_size.y / 2.,
+ 0.,
+ );
+
+ commands
+ .entity(layer_for_tileset_entity)
+ .insert(TilemapBundle {
+ grid_size: *grid_size,
+ size: map_size,
+ storage: tile_storage,
+ texture: tilemap_texture.clone(),
+ tile_size,
+ spacing: tile_spacing,
+ transform: offset_transform,
+ map_type: *map_type,
+ render_settings: *render_settings,
+ ..Default::default()
+ });
}
}
@@ -837,6 +841,67 @@ fn load_infinite_tiles_layer(
(tile_storage, map_size, origin)
}
+#[allow(clippy::too_many_arguments, unused)]
+fn load_objects_layer(
+ commands: &mut Commands,
+ layer_entity: Entity,
+ layer: Layer,
+ object_layer: ObjectLayer,
+ map_size: &TilemapSize,
+ grid_size: &TilemapGridSize,
+ tiled_settings: &TiledMapSettings,
+ #[cfg(feature = "user_properties")] objects_registry: &TiledObjectRegistry,
+) {
+ #[cfg(feature = "physics")]
+ let collision_layer_names =
+ crate::prelude::ObjectNameFilter::from(&tiled_settings.collision_layer_names);
+
+ for object_data in object_layer.objects() {
+ let _object_entity = commands
+ .spawn(TransformBundle::from_transform(Transform::from_xyz(
+ object_data.x,
+ map_size.y as f32 * grid_size.y - object_data.y,
+ 0.,
+ )))
+ .insert(Name::new(format!("Object({})", object_data.name)))
+ .insert(TiledMapObject)
+ .set_parent(layer_entity)
+ .id();
+
+ #[cfg(feature = "physics")]
+ {
+ if collision_layer_names.contains(&layer.name.trim().to_lowercase()) {
+ insert_object_colliders(
+ commands,
+ _object_entity,
+ &object_data,
+ tiled_settings.collider_callback,
+ );
+ }
+ }
+
+ #[cfg(feature = "user_properties")]
+ {
+ if let Some(phantom) = objects_registry.get(&object_data.user_type) {
+ phantom.initialize(
+ commands,
+ &TiledObjectCreated {
+ entity: _object_entity,
+ object_data: object_data.deref().clone(),
+ map_size: *map_size,
+ },
+ );
+ } else {
+ log::warn!(
+ "Skipping unregistered object (name='{}' type='{}')",
+ object_data.name,
+ object_data.user_type
+ );
+ }
+ }
+ }
+}
+
fn get_animated_tile(tile: &Tile) -> Option {
let Some(animation_data) = &tile.animation else {
return None;