diff --git a/Cargo.toml b/Cargo.toml index d039936d83779..5353685ed1a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -984,6 +984,17 @@ category = "2D Rendering" # Loading asset folders is not supported in Wasm, but required to create the atlas. wasm = false +[[example]] +name = "tilemap_entities" +path = "examples/2d/tilemap_entities.rs" +doc-scrape-examples = true + +[package.metadata.example.tilemap_entities] +name = "Tilemap Entities" +description = "Renders a tilemap where each tile is an entity" +category = "2D Rendering" +wasm = true + [[example]] name = "tilemap_chunk" path = "examples/2d/tilemap_chunk.rs" @@ -995,6 +1006,17 @@ description = "Renders a tilemap chunk" category = "2D Rendering" wasm = true +[[example]] +name = "tilemap" +path = "examples/2d/tilemap.rs" +doc-scrape-examples = true + +[package.metadata.example.tilemap] +name = "Tilemap" +description = "Renders a tilemap" +category = "2D Rendering" +wasm = true + [[example]] name = "transparency_2d" path = "examples/2d/transparency_2d.rs" diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index f980c56fda8c6..3eb1289cdfdd8 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -23,6 +23,7 @@ bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev" } bevy_math = { path = "../bevy_math", version = "0.18.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev", optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" } bevy_window = { path = "../bevy_window", version = "0.18.0-dev", optional = true } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index a2a5d594d0b79..42cc3cfac8bb0 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -16,6 +16,7 @@ mod sprite; #[cfg(feature = "bevy_text")] mod text2d; mod texture_slice; +mod tilemap; /// The sprite prelude. /// @@ -50,6 +51,7 @@ pub use sprite::*; #[cfg(feature = "bevy_text")] pub use text2d::*; pub use texture_slice::*; +pub use tilemap::*; use bevy_app::prelude::*; use bevy_ecs::prelude::*; diff --git a/crates/bevy_sprite/src/tilemap/commands.rs b/crates/bevy_sprite/src/tilemap/commands.rs new file mode 100644 index 0000000000000..711453f0a3b14 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/commands.rs @@ -0,0 +1,150 @@ +use std::marker::PhantomData; + +use crate::tilemap::{TileData, TileStorage, Tilemap}; +use bevy_ecs::{entity::Entity, hierarchy::ChildOf, system::{Command, Commands}, world::World}; +use bevy_math::{IVec2, Vec2, Vec3}; +use bevy_transform::components::Transform; + +pub trait CommandsTilemapExt { + fn set_tile( + &mut self, + tilemap_id: Entity, + tile_position: IVec2, + maybe_tile: Option, + ); + + fn remove_tile(&mut self, tilemap_id: Entity, tile_position: IVec2); +} + +impl CommandsTilemapExt for Commands<'_, '_> { + fn set_tile( + &mut self, + tilemap_id: Entity, + tile_position: IVec2, + maybe_tile: Option, + ) { + self.queue(move |world: &mut World| {SetTile {tilemap_id, tile_position, maybe_tile }.apply(world);}); + } + + fn remove_tile(&mut self, tilemap_id: Entity, tile_position: IVec2) { + self.queue(move |world: &mut World| {RemoveTile:: {tilemap_id, tile_position, _t: PhantomData::default() }.apply(world);}); + } +} + +pub struct SetTile { + pub tilemap_id: Entity, + pub tile_position: IVec2, + pub maybe_tile: Option, +} + +pub struct SetTileResult { + pub replaced_tile: Option, + pub chunk_id: Option, +} + +impl Default for SetTileResult { + fn default() -> Self { + Self { replaced_tile: Default::default(), chunk_id: Default::default() } + } +} + +impl Command> for SetTile { + fn apply(self, world: &mut World) -> SetTileResult { + let Ok(mut tilemap_entity) = world.get_entity_mut(self.tilemap_id) else { + tracing::warn!("Could not find Tilemap Entity {:?}", self.tilemap_id); + return Default::default(); + }; + + let Some(tilemap) = tilemap_entity.get::() else { + tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id); + return Default::default(); + }; + + let chunk_position = tilemap.tile_chunk_position(self.tile_position); + let tile_relative_position = tilemap.tile_relative_position(self.tile_position); + + if let Some(tile_storage_id) = tilemap.chunks.get(&chunk_position).cloned() { + let replaced_tile = tilemap_entity.world_scope(move |w| { + let Ok(mut tilestorage_entity) = w.get_entity_mut(tile_storage_id) else { + tracing::warn!("Could not find TileStorage Entity {:?}", tile_storage_id); + return None; + }; + + let Some(mut tile_storage) = tilestorage_entity.get_mut::>() + else { + tracing::warn!( + "Could not find TileStorage on Entity {:?}", + tile_storage_id + ); + return None; + }; + + tile_storage.set(tile_relative_position, self.maybe_tile) + }); + SetTileResult { chunk_id: Some(tile_storage_id), replaced_tile } + } else { + let chunk_size = tilemap.chunk_size; + let tile_size = tilemap.tile_display_size; + let tile_storage_id = tilemap_entity.world_scope(move |w| { + let mut tile_storage = TileStorage::::new(chunk_size); + tile_storage.set(tile_relative_position, self.maybe_tile); + let translation = Vec2::new(chunk_size.x as f32, chunk_size.y as f32) * Vec2::new(tile_size.x as f32, tile_size.y as f32) * Vec2::new(chunk_position.x as f32, chunk_position.y as f32); + let translation = Vec3::new(translation.x, translation.y, 0.0); + let transform = Transform::from_translation(translation); + w.spawn((ChildOf(self.tilemap_id), tile_storage, transform)).id() + }); + let Some(mut tilemap) = tilemap_entity.get_mut::() else { + tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id); + return Default::default(); + }; + tilemap.chunks.insert(chunk_position, tile_storage_id); + SetTileResult { chunk_id: Some(tile_storage_id), replaced_tile: None } + } + } +} + +pub struct RemoveTile { + pub tilemap_id: Entity, + pub tile_position: IVec2, + pub _t: PhantomData +} + +impl Command> for RemoveTile { + fn apply(self, world: &mut World) -> Option { + let Ok(mut tilemap_entity) = world.get_entity_mut(self.tilemap_id) else { + tracing::warn!("Could not find Tilemap Entity {:?}", self.tilemap_id); + return Default::default(); + }; + + let Some(tilemap) = tilemap_entity.get::() else { + tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id); + return Default::default(); + }; + + let chunk_position = tilemap.tile_chunk_position(self.tile_position); + let tile_relative_position = tilemap.tile_relative_position(self.tile_position); + + if let Some(tile_storage_id) = tilemap.chunks.get(&chunk_position).cloned() { + tilemap_entity.world_scope(move |w| { + let Ok(mut tilestorage_entity) = w.get_entity_mut(tile_storage_id) else { + tracing::warn!("Could not find TileStorage Entity {:?}", tile_storage_id); + return None; + }; + + let Some(mut tile_storage) = tilestorage_entity.get_mut::>() + else { + tracing::warn!( + "Could not find TileStorage on Entity {:?}", + tile_storage_id + ); + return None; + }; + + tile_storage.remove(tile_relative_position) + }) + } + else { + None + } + } +} \ No newline at end of file diff --git a/crates/bevy_sprite/src/tilemap/entity_tiles.rs b/crates/bevy_sprite/src/tilemap/entity_tiles.rs new file mode 100644 index 0000000000000..879a1673bcd8e --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/entity_tiles.rs @@ -0,0 +1,112 @@ +use std::marker::PhantomData; + +use bevy_app::{App, Plugin}; +use bevy_derive::Deref; +use bevy_ecs::{component::Component, entity::Entity, hierarchy::ChildOf, lifecycle::HookContext, system::Command, world::{DeferredWorld, World}}; +use bevy_math::IVec2; +use tracing::warn; + +use crate::{RemoveTile, SetTile, SetTileResult, TileData}; + +/// Plugin that handles the initialization and updating of tilemap chunks. +/// Adds systems for processing newly added tilemap chunks. +pub struct EntityTilePlugin; + +impl Plugin for EntityTilePlugin { + fn build(&self, app: &mut App) { + app.world_mut().register_component_hooks::().on_insert(on_insert_entity_tile).on_remove(on_remove_entity_tile); + app.world_mut().register_component_hooks::().on_remove(on_remove_entity_tile); + } +} + +/// An Entity in the tilemap +pub struct EntityTile(pub Entity); + +impl TileData for EntityTile { + +} + +#[derive(Component, Clone, Debug, Deref)] +#[component(immutable)] +pub struct InMap(pub Entity); + +#[derive(Component, Clone, Debug, Deref)] +#[component(immutable)] +pub struct TileCoord(pub IVec2); + +#[derive(Component, Clone, Debug)] +pub struct DespawnOnRemove; + +fn on_insert_entity_tile(mut world: DeferredWorld, HookContext { entity, .. }: HookContext){ + let Ok(tile) = world.get_entity(entity) else { + warn!("Tile {} not found", entity); + return; + }; + let Some(in_map) = tile.get::().cloned() else { + warn!("Tile {} is not in a TileMap", entity); + return; + }; + let Some(tile_position) = tile.get::().cloned() else { + warn!("Tile {} has no tile coord.", entity); + return; + }; + + world + .commands() + .queue(move |world: &mut World| { + let SetTileResult { chunk_id: Some(chunk_id), replaced_tile} = SetTile { + tilemap_id: in_map.0, + tile_position: tile_position.0, + maybe_tile: Some(EntityTile(entity)), + }.apply(world) else { + warn!("Could not create chunk to place Tile {} entity.", entity); + return; + }; + + world.entity_mut(entity).insert(ChildOf(chunk_id)); + + if let Some(replaced_tile) = replaced_tile { + let mut replaced_tile = world.entity_mut(replaced_tile.0); + if replaced_tile.contains::() { + replaced_tile.despawn(); + } else { + replaced_tile.remove::<(InMap, TileCoord)>(); + } + } + }); +} + +fn on_remove_entity_tile(mut world: DeferredWorld, HookContext { entity, .. }: HookContext){ + let Ok(tile) = world.get_entity(entity) else { + warn!("Tile {} not found", entity); + return; + }; + let Some(in_map) = tile.get::().cloned() else { + warn!("Tile {} is not in a TileMap", entity); + return; + }; + let Some(tile_position) = tile.get::().cloned() else { + warn!("Tile {} has no tile coord.", entity); + return; + }; + + world + .commands() + .queue(move |world: &mut World| { + let Some(removed) = RemoveTile:: { + tilemap_id: in_map.0, + tile_position: tile_position.0, + _t: PhantomData, + }.apply(world) else { + warn!("Tile {} could not be removed from map or was already removed.", entity); + return; + }; + + let mut removed = world.entity_mut(removed.0); + if removed.contains::() { + removed.despawn(); + } else { + removed.remove::(); + } + }); +} \ No newline at end of file diff --git a/crates/bevy_sprite/src/tilemap/mod.rs b/crates/bevy_sprite/src/tilemap/mod.rs new file mode 100644 index 0000000000000..b8083504c5691 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/mod.rs @@ -0,0 +1,96 @@ +use bevy_app::{App, Plugin}; +use bevy_ecs::{component::Component, entity::Entity, name::Name, reflect::ReflectComponent}; +use bevy_math::{IVec2, UVec2}; +use bevy_platform::collections::HashMap; +use bevy_reflect::Reflect; +use bevy_transform::components::Transform; + +mod commands; +mod storage; +mod entity_tiles; + +pub use commands::*; +pub use storage::*; +pub use entity_tiles::*; + +/// Plugin that handles the initialization and updating of tilemap chunks. +/// Adds systems for processing newly added tilemap chunks. +pub struct TilemapPlugin; + +impl Plugin for TilemapPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(EntityTilePlugin); + } +} + +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone, Debug)] +#[require(Name::new("Tilemap"), Transform)] +pub struct Tilemap { + pub chunks: HashMap, + pub chunk_size: UVec2, + pub tile_display_size: UVec2, +} + +impl Tilemap { + pub fn new(chunk_size: UVec2, tile_display_size: UVec2) -> Self { + Self { + chunks: HashMap::new(), + chunk_size, + tile_display_size, + } + } + + /// Get the coordinates of the chunk a given tile is in. + // TODO: NAME THIS BETTER + pub fn tile_chunk_position(&self, tile_position: IVec2) -> IVec2 { + tile_position.div_euclid( + self.chunk_size + .try_into() + .expect("Could not convert chunk size into IVec2"), + ) + } + + /// Get the coordinates with in a chunk from a tiles global coordinates. + pub fn tile_relative_position(&self, tile_position: IVec2) -> UVec2 { + let chunk_size = self + .chunk_size + .try_into() + .expect("Could not convert chunk size into IVec2"); + let mut res = tile_position.rem_euclid(chunk_size); + if res.x < 0 { + res.x = chunk_size.x - res.x.abs() - 1; + } + if res.y < 0 { + res.y = chunk_size.y - res.y.abs() - 1; + } + res.try_into() + .expect("Could not convert chunk local position into UVec2") + } + + pub fn calculate_tile_transform(&self, tile_position: IVec2) -> Transform { + Transform::from_xyz( + // tile position + tile_position.x as f32 + // times display size for a tile + * self.tile_display_size.x as f32 + // plus 1/2 the tile_display_size to correct the center + + self.tile_display_size.x as f32 / 2. + // minus 1/2 the tilechunk size, in terms of the tile_display_size, + // to place the 0 at left of tilemapchunk + - self.tile_display_size.x as f32 * self.chunk_size.x as f32 / 2., + // tile position + tile_position.y as f32 + // times display size for a tile + * self.tile_display_size.y as f32 + // minus 1/2 the tile_display_size to correct the center + + self.tile_display_size.y as f32 / 2. + // plus 1/2 the tilechunk size, in terms of the tile_display_size, + // to place the 0 at top of tilemapchunk + - self.tile_display_size.y as f32 * self.chunk_size.y as f32 / 2., + 0., + ) + } +} + +pub trait TileData: Send + Sync + 'static {} diff --git a/crates/bevy_sprite/src/tilemap/storage.rs b/crates/bevy_sprite/src/tilemap/storage.rs new file mode 100644 index 0000000000000..910200ad0e444 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/storage.rs @@ -0,0 +1,82 @@ +use std::ops::Deref; + +use bevy_ecs::{component::Component, entity::Entity, name::Name, reflect::ReflectComponent}; +use bevy_math::{URect, UVec2}; +use bevy_reflect::Reflect; +use bevy_transform::components::Transform; + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component)] +#[require(Name::new("TileStorage"), Transform)] +pub struct TileStorage { + pub tiles: Vec>, + size: UVec2, +} + +impl TileStorage { + pub fn new(size: UVec2) -> Self { + let mut tiles = Vec::new(); + tiles.resize_with(size.element_product() as usize, Default::default); + Self { tiles, size } + } + + pub fn index(&self, tile_coord: UVec2) -> usize { + (tile_coord.y * self.size.x + tile_coord.x) as usize + } + + pub fn get(&self, tile_coord: UVec2) -> Option<&T> { + let index = self.index(tile_coord); + self.tiles.get(index).map(Option::as_ref).flatten() + } + + pub fn get_mut(&mut self, tile_coord: UVec2) -> Option<&mut T> { + let index = self.index(tile_coord); + self.tiles.get_mut(index).map(Option::as_mut).flatten() + } + + pub fn set(&mut self, tile_position: UVec2, maybe_tile: Option) -> Option { + let index = self.index(tile_position); + let tile = self.tiles.get_mut(index)?; + core::mem::replace(tile, maybe_tile) + } + + pub fn remove(&mut self, tile_position: UVec2) -> Option { + self.set(tile_position, None) + } + + // pub fn iter(&self) -> impl Iterator> { + // self.tiles.iter().cloned() + // } + + // pub fn iter_sub_rect(&self, rect: URect) -> impl Iterator> { + // let URect { min, max } = rect; + + // (min.y..max.y).flat_map(move |y| { + // (min.x..max.x).map(move |x| { + // if x >= self.size.x || y >= self.size.y { + // return None; + // } + + // let index = (y * self.size.x + x) as usize; + // self.tiles.get(index).cloned().flatten() + // }) + // }) + // } + + // pub fn iter_chunk_tiles( + // &self, + // chunk_position: UVec2, + // chunk_size: UVec2, + // ) -> impl Iterator> { + // let chunk_rect = URect::from_corners( + // chunk_position * chunk_size, + // (chunk_position + UVec2::splat(1)) * chunk_size, + // ); + + // self.iter_sub_rect(chunk_rect) + // } + + pub fn size(&self) -> UVec2 { + self.size + } +} diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index 8b4f062873262..1f6abbccac3b9 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -1,26 +1,23 @@ +use std::{any::type_name, marker::PhantomData}; + use crate::{AlphaMode2d, MeshMaterial2d}; use bevy_app::{App, Plugin, Update}; use bevy_asset::{Assets, Handle}; +use bevy_camera::visibility::Visibility; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, - entity::Entity, - lifecycle::HookContext, - query::Changed, - reflect::{ReflectComponent, ReflectResource}, - resource::Resource, - system::{Query, ResMut}, - world::DeferredWorld, + component::Component, entity::Entity, hierarchy::ChildOf, lifecycle::HookContext, query::Changed, reflect::{ReflectComponent, ReflectResource}, resource::Resource, system::{Command, Commands, Query, ResMut}, world::{DeferredWorld, World} }; use bevy_image::Image; use bevy_math::{primitives::Rectangle, UVec2}; use bevy_mesh::{Mesh, Mesh2d}; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::*, Reflect}; +use bevy_sprite::{InMap, RemoveTile, SetTile, TileCoord, TileData, TileStorage, Tilemap}; use bevy_transform::components::Transform; use bevy_utils::default; -use tracing::warn; +use tracing::{trace, warn}; mod tilemap_chunk_material; @@ -34,6 +31,8 @@ impl Plugin for TilemapChunkPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_systems(Update, update_tilemap_chunk_indices); + app.world_mut().register_component_hooks::>().on_insert(on_insert_chunk_tile_render_data); + app.world_mut().register_component_hooks::().on_insert(on_insert_tile_render_data).on_remove(on_remove_tile_render_data); } } @@ -47,7 +46,7 @@ pub struct TilemapChunkMeshCache(HashMap>); #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Clone, Debug, Default)] #[component(immutable, on_insert = on_insert_tilemap_chunk)] -pub struct TilemapChunk { +pub struct TilemapChunkRenderData { /// The size of the chunk in tiles. pub chunk_size: UVec2, /// The size to use for each tile, not to be confused with the size of a tile in the tileset image. @@ -59,7 +58,20 @@ pub struct TilemapChunk { pub alpha_mode: AlphaMode2d, } -impl TilemapChunk { +/// A component representing a chunk of a tilemap. +/// Each chunk is a rectangular section of tiles that is rendered as a single mesh. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Clone, Debug, Default)] +#[require(Transform, Visibility)] +#[component(immutable)] +pub struct TilemapRenderData { + /// Handle to the tileset image containing all tile textures. + pub tileset: Handle, + /// The alpha mode to use for the tilemap chunk. + pub alpha_mode: AlphaMode2d, +} + +impl TilemapChunkRenderData { pub fn calculate_tile_transform(&self, position: UVec2) -> Transform { Transform::from_xyz( // tile position @@ -85,10 +97,50 @@ impl TilemapChunk { } } +fn on_insert_chunk_tile_render_data(mut world: DeferredWorld, HookContext { entity, .. }: HookContext){ + let Ok(chunk) = world.get_entity(entity) else { + warn!("Chunk {} not found", entity); + return; + }; + if chunk.contains::() { + trace!("Chunk {} already contains TilemapChunkRenderData", entity); + return; + } + let Some(child_of) = chunk.get::() else { + warn!("Chunk {} is not a child of an entity", entity); + return; + }; + let Ok(tilemap) = world.get_entity(child_of.parent()) else { + warn!("Could not find chunk {}'s parent {}", entity, child_of.parent()); + return; + }; + let Some(tilemap_render_data) = tilemap.get::() else { + warn!("Could not find TilemapRenderData on chunk {}'s parent {}", entity, child_of.parent()); + return; + }; + let Some(tilemap) = tilemap.get::() else { + warn!("Could not find Tilemap on chunk {}'s parent {}", entity, child_of.parent()); + return; + }; + + let data = TilemapChunkRenderData { + chunk_size: tilemap.chunk_size, + tile_display_size: tilemap.tile_display_size, + tileset: tilemap_render_data.tileset.clone(), + alpha_mode: tilemap_render_data.alpha_mode, + }; + + world + .commands() + .entity(entity) + .insert(data); +} + /// Data for a single tile in the tilemap chunk. -#[derive(Clone, Copy, Debug, Reflect)] +#[derive(Component, Clone, Copy, Debug, Reflect)] #[reflect(Clone, Debug, Default)] -pub struct TileData { +#[component(immutable)] +pub struct TileRenderData { /// The index of the tile in the corresponding tileset array texture. pub tileset_index: u16, /// The color tint of the tile. White leaves the sampled texture color unchanged. @@ -97,7 +149,9 @@ pub struct TileData { pub visible: bool, } -impl TileData { +impl TileData for TileRenderData {} + +impl TileRenderData { /// Creates a new `TileData` with the given tileset index and default values. pub fn from_tileset_index(tileset_index: u16) -> Self { Self { @@ -107,7 +161,7 @@ impl TileData { } } -impl Default for TileData { +impl Default for TileRenderData { fn default() -> Self { Self { tileset_index: 0, @@ -117,15 +171,13 @@ impl Default for TileData { } } -/// Component storing the data of tiles within a chunk. -/// Each index corresponds to a specific tile in the tileset. `None` indicates an empty tile. -#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] -#[reflect(Component, Clone, Debug)] -pub struct TilemapChunkTileData(pub Vec>); - fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { - let Some(tilemap_chunk) = world.get::(entity) else { - warn!("TilemapChunk not found for tilemap chunk {}", entity); + let Some(tilemap_chunk) = world.get::(entity) else { + warn!( + "{} not found for tilemap chunk {}", + type_name::(), + entity + ); return; }; @@ -133,25 +185,29 @@ fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: let alpha_mode = tilemap_chunk.alpha_mode; let tileset = tilemap_chunk.tileset.clone(); - let Some(tile_data) = world.get::(entity) else { - warn!("TilemapChunkIndices not found for tilemap chunk {}", entity); + let Some(tile_data) = world.get::>(entity) else { + warn!( + "{} not found for tilemap chunk {}", + type_name::>(), + entity + ); return; }; let expected_tile_data_length = chunk_size.element_product() as usize; - if tile_data.len() != expected_tile_data_length { + if tile_data.tiles.len() != expected_tile_data_length { warn!( "Invalid tile data length for tilemap chunk {} of size {}. Expected {}, got {}", entity, chunk_size, expected_tile_data_length, - tile_data.len(), + tile_data.tiles.len(), ); return; } let packed_tile_data: Vec = - tile_data.0.iter().map(|&tile| tile.into()).collect(); + tile_data.tiles.iter().map(|&tile| tile.into()).collect(); let tile_data_image = make_chunk_tile_data_image(&chunk_size, &packed_tile_data); @@ -186,30 +242,30 @@ fn update_tilemap_chunk_indices( query: Query< ( Entity, - &TilemapChunk, - &TilemapChunkTileData, + &TilemapChunkRenderData, + &TileStorage, &MeshMaterial2d, ), - Changed, + Changed>, >, mut materials: ResMut>, mut images: ResMut>, ) { - for (chunk_entity, TilemapChunk { chunk_size, .. }, tile_data, material) in query { + for (chunk_entity, TilemapChunkRenderData { chunk_size, .. }, tile_data, material) in query { let expected_tile_data_length = chunk_size.element_product() as usize; - if tile_data.len() != expected_tile_data_length { + if tile_data.tiles.len() != expected_tile_data_length { warn!( "Invalid TilemapChunkTileData length for tilemap chunk {} of size {}. Expected {}, got {}", chunk_entity, chunk_size, - tile_data.len(), + tile_data.tiles.len(), expected_tile_data_length ); continue; } let packed_tile_data: Vec = - tile_data.0.iter().map(|&tile| tile.into()).collect(); + tile_data.tiles.iter().map(|&tile| tile.into()).collect(); // Getting the material mutably to trigger change detection let Some(material) = materials.get_mut(material.id()) else { @@ -238,14 +294,60 @@ fn update_tilemap_chunk_indices( } } -impl TilemapChunkTileData { - pub fn tile_data_from_tile_pos( - &self, - tilemap_size: UVec2, - position: UVec2, - ) -> Option<&TileData> { - self.0 - .get(tilemap_size.x as usize * position.y as usize + position.x as usize) - .and_then(|opt| opt.as_ref()) - } +fn on_insert_tile_render_data(mut world: DeferredWorld, HookContext { entity, .. }: HookContext){ + let Ok(tile) = world.get_entity(entity) else { + warn!("Tile {} not found", entity); + return; + }; + let Some(in_map) = tile.get::().cloned() else { + warn!("Tile {} is not in a TileMap", entity); + return; + }; + let Some(tile_position) = tile.get::().cloned() else { + warn!("Tile {} has no tile coord.", entity); + return; + }; + let Some(tile_render_data) = tile.get::().cloned() else { + warn!("Tile {} does not have TileRenderData", entity); + return; + }; + + world + .commands() + .queue(move |world: &mut World| { + SetTile { + tilemap_id: in_map.0, + tile_position: tile_position.0, + maybe_tile: Some(tile_render_data), + }.apply(world); + }); } + +fn on_remove_tile_render_data(mut world: DeferredWorld, HookContext { entity, .. }: HookContext){ + let Ok(tile) = world.get_entity(entity) else { + warn!("Tile {} not found", entity); + return; + }; + let Some(in_map) = tile.get::().cloned() else { + warn!("Tile {} is not in a TileMap", entity); + return; + }; + let Some(tile_position) = tile.get::().cloned() else { + warn!("Tile {} has no tile coord.", entity); + return; + }; + let Some(tile_render_data) = tile.get::().cloned() else { + warn!("Tile {} does not have TileRenderData", entity); + return; + }; + + world + .commands() + .queue(move |world: &mut World| { + RemoveTile:: { + tilemap_id: in_map.0, + tile_position: tile_position.0, + _t: PhantomData::default(), + }.apply(world); + }); +} \ No newline at end of file diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs index 53039c3458de3..5372445cc35c3 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs @@ -1,4 +1,4 @@ -use crate::{AlphaMode2d, Material2d, Material2dPlugin, TileData}; +use crate::{AlphaMode2d, Material2d, Material2dPlugin, TileRenderData}; use bevy_app::{App, Plugin}; use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle, RenderAssetUsages}; use bevy_color::ColorToPacked; @@ -68,13 +68,13 @@ impl PackedTileData { } } -impl From for PackedTileData { +impl From for PackedTileData { fn from( - TileData { + TileRenderData { tileset_index, color, visible, - }: TileData, + }: TileRenderData, ) -> Self { Self { tileset_index, @@ -84,8 +84,8 @@ impl From for PackedTileData { } } -impl From> for PackedTileData { - fn from(maybe_tile_data: Option) -> Self { +impl From> for PackedTileData { + fn from(maybe_tile_data: Option) -> Self { maybe_tile_data .map(Into::into) .unwrap_or(PackedTileData::empty()) diff --git a/examples/2d/tilemap.rs b/examples/2d/tilemap.rs new file mode 100644 index 0000000000000..6fd89c5435167 --- /dev/null +++ b/examples/2d/tilemap.rs @@ -0,0 +1,65 @@ +//! Shows a tilemap chunk rendered with a single draw call. + +use bevy::{ + color::palettes::tailwind::RED_400, image::{ImageArrayLayout, ImageLoaderSettings}, prelude::*, sprite::{TileStorage, Tilemap, CommandsTilemapExt}, sprite_render::{TileRenderData, TilemapChunkRenderData, TilemapRenderData} +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_systems(Startup, setup) + .add_systems(Update, update_tilemap) + .run(); +} + +#[derive(Component, Deref, DerefMut)] +struct UpdateTimer(Timer); + +#[derive(Resource, Deref, DerefMut)] +struct SeededRng(ChaCha8Rng); + +fn setup(mut commands: Commands, assets: Res) { + // We're seeding the PRNG here to make this example deterministic for testing purposes. + // This isn't strictly required in practical use unless you need your app to be deterministic. + let mut rng = ChaCha8Rng::seed_from_u64(42); + + let chunk_size = UVec2::splat(16); + let tile_display_size = UVec2::splat(8); + + commands.spawn(( + Transform::default(), + Visibility::default(), + Tilemap::new(chunk_size, tile_display_size), + TilemapRenderData { + tileset: assets.load_with_settings( + "textures/array_texture.png", + |settings: &mut ImageLoaderSettings| { + // The tileset texture is expected to be an array of tile textures, so we tell the + // `ImageLoader` that our texture is composed of 4 stacked tile images. + settings.array_layout = Some(ImageArrayLayout::RowCount { rows: 4 }); + }, + ), + ..default() + } + )); + + commands.spawn(Camera2d); + + commands.insert_resource(SeededRng(rng)); +} + +fn update_tilemap( + map: Single>, + mut commands: Commands, + mut rng: ResMut, +) { + let map = *map; + for _ in 0..rng.random_range(1..=20) { + let x = rng.random_range(-64..=64); + let y = rng.random_range(-64..=64); + + commands.set_tile(map, IVec2::new(x, y), Some(TileRenderData { tileset_index: rng.random_range(0..4), ..Default::default()})); + } +} \ No newline at end of file diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs index 4bc1855841c04..4a24c3212af79 100644 --- a/examples/2d/tilemap_chunk.rs +++ b/examples/2d/tilemap_chunk.rs @@ -4,7 +4,8 @@ use bevy::{ color::palettes::tailwind::RED_400, image::{ImageArrayLayout, ImageLoaderSettings}, prelude::*, - sprite_render::{TileData, TilemapChunk, TilemapChunkTileData}, + sprite_render::{TilemapChunkRenderData, TileRenderData}, + sprite::TileStorage }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -30,19 +31,21 @@ fn setup(mut commands: Commands, assets: Res) { let chunk_size = UVec2::splat(64); let tile_display_size = UVec2::splat(8); - let tile_data: Vec> = (0..chunk_size.element_product()) + let tile_data: Vec> = (0..chunk_size.element_product()) .map(|_| rng.random_range(0..5)) .map(|i| { if i == 0 { None } else { - Some(TileData::from_tileset_index(i - 1)) + Some(TileRenderData::from_tileset_index(i - 1)) } }) .collect(); + let mut storage = TileStorage::::new(chunk_size); + storage.tiles = tile_data; commands.spawn(( - TilemapChunk { + TilemapChunkRenderData { chunk_size, tile_display_size, tileset: assets.load_with_settings( @@ -55,7 +58,7 @@ fn setup(mut commands: Commands, assets: Res) { ), ..default() }, - TilemapChunkTileData(tile_data), + storage, UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), )); @@ -71,7 +74,7 @@ fn spawn_fake_player( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, - chunk: Single<&TilemapChunk>, + chunk: Single<&TilemapChunkRenderData>, ) { let mut transform = chunk.calculate_tile_transform(UVec2::new(0, 0)); transform.translation.z = 1.; @@ -97,7 +100,7 @@ fn spawn_fake_player( fn move_player( mut player: Single<&mut Transform, With>, time: Res