From 5673bcea0e65014b0f7559bcaa88f3c063690ee5 Mon Sep 17 00:00:00 2001 From: James Bell Date: Wed, 29 Oct 2025 21:48:16 -0400 Subject: [PATCH 1/3] Wip --- crates/bevy_sprite/Cargo.toml | 1 + crates/bevy_sprite/src/lib.rs | 2 + crates/bevy_sprite/src/tilemap/commands.rs | 74 ++++++ crates/bevy_sprite/src/tilemap/mod.rs | 87 +++++++ crates/bevy_sprite/src/tilemap/storage.rs | 80 +++++++ .../src/tilemap_chunk/mod.rs | 220 ++++++------------ .../tilemap_chunk/tilemap_chunk_material.rs | 12 +- examples/2d/tilemap_chunk.rs | 19 +- 8 files changed, 329 insertions(+), 166 deletions(-) create mode 100644 crates/bevy_sprite/src/tilemap/commands.rs create mode 100644 crates/bevy_sprite/src/tilemap/mod.rs create mode 100644 crates/bevy_sprite/src/tilemap/storage.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 c14aab0c623a1..3cd123497edab 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..9ada7036eb3dd --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/commands.rs @@ -0,0 +1,74 @@ +use crate::tilemap::{TileData, TileStorage, Tilemap}; +use bevy_ecs::{entity::Entity, hierarchy::ChildOf, system::Commands, world::World}; +use bevy_math::{IVec2, UVec2}; + +pub trait CommandsTilemapExt { + fn set_tile( + &mut self, + tilemap: Entity, + tile_position: IVec2, + maybe_tile: Option, + ); + + fn remove_tile(&mut self, tilemap: 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 |w: &mut World| { + let Ok(mut tilemap_entity) = w.get_entity_mut(tilemap_id) else { + tracing::warn!("Could not find Tilemap Entity {:?}", tilemap_id); + return; + }; + + let Some(tilemap) = tilemap_entity.get::() else { + tracing::warn!("Could not find Tilemap on Entity {:?}", tilemap_id); + return; + }; + + let chunk_position = tilemap.tile_chunk_position(tile_position); + let tile_position = tilemap.tile_chunk_local_position(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; + }; + + let Some(mut tile_storage) = tilestorage_entity.get_mut::>() + else { + tracing::warn!( + "Could not find TileStorage on Entity {:?}", + tile_storage_id + ); + return; + }; + + tile_storage.set(tile_position, maybe_tile); + }); + } else { + let chunk_size = tilemap.chunk_size; + let tile_storage_id = tilemap_entity.world_scope(move |w| { + let mut tile_storage = TileStorage::::new(chunk_size); + tile_storage.set(tile_position, maybe_tile); + w.spawn((ChildOf(tilemap_id), tile_storage)).id() + }); + let Some(mut tilemap) = tilemap_entity.get_mut::() else { + tracing::warn!("Could not find Tilemap on Entity {:?}", tilemap_id); + return; + }; + tilemap.chunks.insert(chunk_position, tile_storage_id); + }; + }); + } + + fn remove_tile(&mut self, tilemap: Entity, tile_position: IVec2) { + todo!() + } +} diff --git a/crates/bevy_sprite/src/tilemap/mod.rs b/crates/bevy_sprite/src/tilemap/mod.rs new file mode 100644 index 0000000000000..79ff0672fbb82 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/mod.rs @@ -0,0 +1,87 @@ +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; + +pub use commands::*; +pub use storage::*; + +/// 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(TilemapChunkPlugin).add_plugins(TilePlugin); + } +} + +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone, Debug)] +#[require(Name::new("TilemapLayer"), Transform)] +pub struct Tilemap { + pub chunks: HashMap, + pub chunk_size: UVec2, + pub tile_display_size: UVec2, +} + +impl Tilemap { + /// 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. + // TODO: NAME THIS BETTER + pub fn tile_chunk_local_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..da0d3894642f6 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap/storage.rs @@ -0,0 +1,80 @@ +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>, + pub 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 } + } + + 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 5139ab704b5a6..23ae406da089e 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -8,11 +8,13 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + hierarchy::ChildOf, lifecycle::HookContext, query::Changed, reflect::{ReflectComponent, ReflectResource}, + relationship::Relationship, resource::Resource, - system::{Query, ResMut}, + system::{Commands, Query, ResMut}, world::DeferredWorld, }; use bevy_image::Image; @@ -20,6 +22,7 @@ use bevy_math::{primitives::Rectangle, UVec2}; use bevy_mesh::{Mesh, Mesh2d}; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::*, Reflect}; +use bevy_sprite::{TileData, TileStorage, Tilemap}; use bevy_transform::components::Transform; use bevy_utils::default; use tracing::warn; @@ -44,53 +47,22 @@ impl Plugin for TilemapChunkPlugin { #[reflect(Resource, Default)] pub struct TilemapChunkMeshCache(HashMap>); -/// A component representing a chunk of a tilemap. -/// Each chunk is a rectangular section of tiles that is rendered as a single mesh. +/// Information for rendering chunks in a tilemap #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Clone, Debug, Default)] -#[component(immutable, on_insert = on_insert_tilemap_chunk)] -pub struct TilemapChunk { - /// 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. - /// The size of the tile in the tileset image is determined by the tileset image's dimensions. - pub tile_display_size: UVec2, +#[component(immutable)] +#[require(Transform)] +pub struct TilemapChunkRenderer { /// 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 TilemapChunk { - pub fn calculate_tile_transform(&self, position: UVec2) -> Transform { - Transform::from_xyz( - // tile position - 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 - position.y as f32 - // times display size for a tile - * (self.tile_display_size.y as f32).neg() - // 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., - ) - } -} - /// Data for a single tile in the tilemap chunk. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Clone, Debug, Default)] -pub struct TileData { +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. @@ -99,7 +71,7 @@ pub struct TileData { pub visible: bool, } -impl TileData { +impl TileRenderData { /// Creates a new `TileData` with the given tileset index and default values. pub fn from_tileset_index(tileset_index: u16) -> Self { Self { @@ -109,7 +81,9 @@ impl TileData { } } -impl Default for TileData { +impl TileData for TileRenderData {} + +impl Default for TileRenderData { fn default() -> Self { Self { tileset_index: 0, @@ -119,135 +93,77 @@ 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); - return; - }; - - let chunk_size = tilemap_chunk.chunk_size; - 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); - return; - }; - - let expected_tile_data_length = chunk_size.element_product() as usize; - if tile_data.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(), - ); - return; - } - - let packed_tile_data: Vec = - tile_data.0.iter().map(|&tile| tile.into()).collect(); - - let tile_data_image = make_chunk_tile_data_image(&chunk_size, &packed_tile_data); - - let tilemap_chunk_mesh_cache = world.resource::(); - - let mesh_size = chunk_size * tilemap_chunk.tile_display_size; - - let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_size) { - mesh.clone() - } else { - let mut meshes = world.resource_mut::>(); - meshes.add(Rectangle::from_size(mesh_size.as_vec2())) - }; - - let mut images = world.resource_mut::>(); - let tile_data = images.add(tile_data_image); - - let mut materials = world.resource_mut::>(); - let material = materials.add(TilemapChunkMaterial { - tileset, - tile_data, - alpha_mode, - }); - - world - .commands() - .entity(entity) - .insert((Mesh2d(mesh), MeshMaterial2d(material))); -} - fn update_tilemap_chunk_indices( query: Query< ( Entity, - &TilemapChunk, - &TilemapChunkTileData, - &MeshMaterial2d, + &ChildOf, + &TileStorage, + Option<&MeshMaterial2d>, ), - Changed, + Changed>, >, + map_query: Query<(&Tilemap, &TilemapChunkRenderer)>, + mut tilemap_chunk_mesh_cache: ResMut, mut materials: ResMut>, mut images: ResMut>, + mut meshes: ResMut>, + mut commands: Commands, ) { - for (chunk_entity, TilemapChunk { 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 { + for (chunk_id, in_map, storage, material) in query { + let Ok((map, map_renderer)) = map_query.get(in_map.get()) else { warn!( - "Invalid TilemapChunkTileData length for tilemap chunk {} of size {}. Expected {}, got {}", - chunk_entity, - chunk_size, - tile_data.len(), - expected_tile_data_length + "Could not find Tilemap {} for chunk {}", + in_map.get(), + chunk_id ); continue; - } + }; let packed_tile_data: Vec = - tile_data.0.iter().map(|&tile| tile.into()).collect(); + storage.tiles.iter().map(|&tile| tile.into()).collect(); // Getting the material mutably to trigger change detection - let Some(material) = materials.get_mut(material.id()) else { - warn!( - "TilemapChunkMaterial not found for tilemap chunk {}", - chunk_entity - ); - continue; - }; - let Some(tile_data_image) = images.get_mut(&material.tile_data) else { - warn!( - "TilemapChunkMaterial tile data image not found for tilemap chunk {}", - chunk_entity - ); - continue; + if let Some(material) = material.and_then(|material| materials.get_mut(material.id())) { + let Some(tile_data_image) = images.get_mut(&material.tile_data) else { + warn!( + "TilemapChunkMaterial tile data image not found for tilemap chunk {}", + chunk_id + ); + continue; + }; + let Some(data) = tile_data_image.data.as_mut() else { + warn!( + "TilemapChunkMaterial tile data image data not found for tilemap chunk {}", + chunk_id + ); + continue; + }; + data.clear(); + data.extend_from_slice(bytemuck::cast_slice(&packed_tile_data)); + } else { + let tile_data_image = make_chunk_tile_data_image(&storage.size, &packed_tile_data); + + let mesh_size = storage.size * map.tile_display_size; + + let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_size) { + mesh.clone() + } else { + let mesh = meshes.add(Rectangle::from_size(mesh_size.as_vec2())); + tilemap_chunk_mesh_cache.insert(mesh_size, mesh.clone()); + mesh + }; + let tile_data = images.add(tile_data_image); + + let material = materials.add(TilemapChunkMaterial { + tileset: map_renderer.tileset.clone(), + tile_data, + alpha_mode: map_renderer.alpha_mode, + }); + + commands + .entity(chunk_id) + .insert((Mesh2d(mesh), MeshMaterial2d(material))); }; - let Some(data) = tile_data_image.data.as_mut() else { - warn!( - "TilemapChunkMaterial tile data image data not found for tilemap chunk {}", - chunk_entity - ); - continue; - }; - data.clear(); - data.extend_from_slice(bytemuck::cast_slice(&packed_tile_data)); - } -} - -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()) } } 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_chunk.rs b/examples/2d/tilemap_chunk.rs index 4bc1855841c04..3c6d339491719 100644 --- a/examples/2d/tilemap_chunk.rs +++ b/examples/2d/tilemap_chunk.rs @@ -4,7 +4,7 @@ use bevy::{ color::palettes::tailwind::RED_400, image::{ImageArrayLayout, ImageLoaderSettings}, prelude::*, - sprite_render::{TileData, TilemapChunk, TilemapChunkTileData}, + sprite_render::{TileRenderData, TilemapChunkRenderer, TilemapChunkTileData}, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -30,19 +30,19 @@ 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(); commands.spawn(( - TilemapChunk { + TilemapChunkRenderer { chunk_size, tile_display_size, tileset: assets.load_with_settings( @@ -71,7 +71,7 @@ fn spawn_fake_player( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, - chunk: Single<&TilemapChunk>, + chunk: Single<&TilemapChunkRenderer>, ) { let mut transform = chunk.calculate_tile_transform(UVec2::new(0, 0)); transform.translation.z = 1.; @@ -97,7 +97,7 @@ fn spawn_fake_player( fn move_player( mut player: Single<&mut Transform, With>, time: Res