Path: blob/main/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs
6849 views
use core::ops::Neg;12use crate::{AlphaMode2d, MeshMaterial2d};3use bevy_app::{App, Plugin, Update};4use bevy_asset::{Assets, Handle};5use bevy_color::Color;6use bevy_derive::{Deref, DerefMut};7use bevy_ecs::{8component::Component,9entity::Entity,10lifecycle::HookContext,11query::Changed,12reflect::{ReflectComponent, ReflectResource},13resource::Resource,14system::{Query, ResMut},15world::DeferredWorld,16};17use bevy_image::Image;18use bevy_math::{primitives::Rectangle, UVec2};19use bevy_mesh::{Mesh, Mesh2d};20use bevy_platform::collections::HashMap;21use bevy_reflect::{prelude::*, Reflect};22use bevy_transform::components::Transform;23use bevy_utils::default;24use tracing::warn;2526mod tilemap_chunk_material;2728pub use tilemap_chunk_material::*;2930/// Plugin that handles the initialization and updating of tilemap chunks.31/// Adds systems for processing newly added tilemap chunks and updating their indices.32pub struct TilemapChunkPlugin;3334impl Plugin for TilemapChunkPlugin {35fn build(&self, app: &mut App) {36app.init_resource::<TilemapChunkMeshCache>()37.add_systems(Update, update_tilemap_chunk_indices);38}39}4041/// A resource storing the meshes for each tilemap chunk size.42#[derive(Resource, Default, Deref, DerefMut, Reflect)]43#[reflect(Resource, Default)]44pub struct TilemapChunkMeshCache(HashMap<UVec2, Handle<Mesh>>);4546/// A component representing a chunk of a tilemap.47/// Each chunk is a rectangular section of tiles that is rendered as a single mesh.48#[derive(Component, Clone, Debug, Default, Reflect)]49#[reflect(Component, Clone, Debug, Default)]50#[component(immutable, on_insert = on_insert_tilemap_chunk)]51pub struct TilemapChunk {52/// The size of the chunk in tiles.53pub chunk_size: UVec2,54/// The size to use for each tile, not to be confused with the size of a tile in the tileset image.55/// The size of the tile in the tileset image is determined by the tileset image's dimensions.56pub tile_display_size: UVec2,57/// Handle to the tileset image containing all tile textures.58pub tileset: Handle<Image>,59/// The alpha mode to use for the tilemap chunk.60pub alpha_mode: AlphaMode2d,61}6263impl TilemapChunk {64pub fn calculate_tile_transform(&self, position: UVec2) -> Transform {65Transform::from_xyz(66// tile position67position.x as f3268// times display size for a tile69* self.tile_display_size.x as f3270// plus 1/2 the tile_display_size to correct the center71+ self.tile_display_size.x as f32 / 2.72// minus 1/2 the tilechunk size, in terms of the tile_display_size,73// to place the 0 at left of tilemapchunk74- self.tile_display_size.x as f32 * self.chunk_size.x as f32 / 2.,75// tile position76position.y as f3277// times display size for a tile78* (self.tile_display_size.y as f32).neg()79// minus 1/2 the tile_display_size to correct the center80- self.tile_display_size.y as f32 / 2.81// plus 1/2 the tilechunk size, in terms of the tile_display_size,82// to place the 0 at top of tilemapchunk83+ self.tile_display_size.y as f32 * self.chunk_size.y as f32 / 2.,840.,85)86}87}8889/// Data for a single tile in the tilemap chunk.90#[derive(Clone, Copy, Debug, Reflect)]91#[reflect(Clone, Debug, Default)]92pub struct TileData {93/// The index of the tile in the corresponding tileset array texture.94pub tileset_index: u16,95/// The color tint of the tile. White leaves the sampled texture color unchanged.96pub color: Color,97/// The visibility of the tile.98pub visible: bool,99}100101impl TileData {102/// Creates a new `TileData` with the given tileset index and default values.103pub fn from_tileset_index(tileset_index: u16) -> Self {104Self {105tileset_index,106..default()107}108}109}110111impl Default for TileData {112fn default() -> Self {113Self {114tileset_index: 0,115color: Color::WHITE,116visible: true,117}118}119}120121/// Component storing the data of tiles within a chunk.122/// Each index corresponds to a specific tile in the tileset. `None` indicates an empty tile.123#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)]124#[reflect(Component, Clone, Debug)]125pub struct TilemapChunkTileData(pub Vec<Option<TileData>>);126127fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {128let Some(tilemap_chunk) = world.get::<TilemapChunk>(entity) else {129warn!("TilemapChunk not found for tilemap chunk {}", entity);130return;131};132133let chunk_size = tilemap_chunk.chunk_size;134let alpha_mode = tilemap_chunk.alpha_mode;135let tileset = tilemap_chunk.tileset.clone();136137let Some(tile_data) = world.get::<TilemapChunkTileData>(entity) else {138warn!("TilemapChunkIndices not found for tilemap chunk {}", entity);139return;140};141142let expected_tile_data_length = chunk_size.element_product() as usize;143if tile_data.len() != expected_tile_data_length {144warn!(145"Invalid tile data length for tilemap chunk {} of size {}. Expected {}, got {}",146entity,147chunk_size,148expected_tile_data_length,149tile_data.len(),150);151return;152}153154let packed_tile_data: Vec<PackedTileData> =155tile_data.0.iter().map(|&tile| tile.into()).collect();156157let tile_data_image = make_chunk_tile_data_image(&chunk_size, &packed_tile_data);158159let tilemap_chunk_mesh_cache = world.resource::<TilemapChunkMeshCache>();160161let mesh_size = chunk_size * tilemap_chunk.tile_display_size;162163let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_size) {164mesh.clone()165} else {166let mut meshes = world.resource_mut::<Assets<Mesh>>();167meshes.add(Rectangle::from_size(mesh_size.as_vec2()))168};169170let mut images = world.resource_mut::<Assets<Image>>();171let tile_data = images.add(tile_data_image);172173let mut materials = world.resource_mut::<Assets<TilemapChunkMaterial>>();174let material = materials.add(TilemapChunkMaterial {175tileset,176tile_data,177alpha_mode,178});179180world181.commands()182.entity(entity)183.insert((Mesh2d(mesh), MeshMaterial2d(material)));184}185186fn update_tilemap_chunk_indices(187query: Query<188(189Entity,190&TilemapChunk,191&TilemapChunkTileData,192&MeshMaterial2d<TilemapChunkMaterial>,193),194Changed<TilemapChunkTileData>,195>,196mut materials: ResMut<Assets<TilemapChunkMaterial>>,197mut images: ResMut<Assets<Image>>,198) {199for (chunk_entity, TilemapChunk { chunk_size, .. }, tile_data, material) in query {200let expected_tile_data_length = chunk_size.element_product() as usize;201if tile_data.len() != expected_tile_data_length {202warn!(203"Invalid TilemapChunkTileData length for tilemap chunk {} of size {}. Expected {}, got {}",204chunk_entity,205chunk_size,206tile_data.len(),207expected_tile_data_length208);209continue;210}211212let packed_tile_data: Vec<PackedTileData> =213tile_data.0.iter().map(|&tile| tile.into()).collect();214215// Getting the material mutably to trigger change detection216let Some(material) = materials.get_mut(material.id()) else {217warn!(218"TilemapChunkMaterial not found for tilemap chunk {}",219chunk_entity220);221continue;222};223let Some(tile_data_image) = images.get_mut(&material.tile_data) else {224warn!(225"TilemapChunkMaterial tile data image not found for tilemap chunk {}",226chunk_entity227);228continue;229};230let Some(data) = tile_data_image.data.as_mut() else {231warn!(232"TilemapChunkMaterial tile data image data not found for tilemap chunk {}",233chunk_entity234);235continue;236};237data.clear();238data.extend_from_slice(bytemuck::cast_slice(&packed_tile_data));239}240}241242impl TilemapChunkTileData {243pub fn tile_data_from_tile_pos(244&self,245tilemap_size: UVec2,246position: UVec2,247) -> Option<&TileData> {248self.0249.get(tilemap_size.x as usize * position.y as usize + position.x as usize)250.and_then(|opt| opt.as_ref())251}252}253254255