use std::collections::VecDeque; use super::{ local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, TexelBehaviour2D, TexelID, NEIGHBOUR_INDEX_MAP, }; use crate::util::{CollisionLayers, Segment2I, Vector2I}; use bevy::{ prelude::*, render::{render_resource::Extent3d, texture::ImageSampler}, }; use bevy_rapier2d::prelude::*; use lazy_static::lazy_static; type Island = VecDeque; lazy_static! { /// Marching Square case dictionary. /// /// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first). /// Bit set to 1 means that the neighbour has collision. Only the 4 least significant bits are currently used. /// /// Value is an array of segments that the tile should have. The segments are configured to go clockwise. /// /// Note: This dictionary should only be used for empty tiles. static ref MST_CASE_MAP: [Vec; 16] = [ /* 0b0000 */ vec![], /* 0b0001 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::UP } ], /* 0b0010 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ONE } ], /* 0b0011 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::UP } ], /* 0b0100 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::RIGHT } ], /* 0b0101 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::UP }, Segment2I { from: Vector2I::ZERO, to: Vector2I::RIGHT } ], /* 0b0110 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::ONE } ], /* 0b0111 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP } ], /* 0b1000 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::ZERO } ], /* 0b1001 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::ZERO } ], /* 0b1010 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ONE }, Segment2I { from: Vector2I::UP, to: Vector2I::ZERO } ], /* 0b1011 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO } ], /* 0b1100 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::RIGHT } ], /* 0b1101 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::RIGHT } ], /* 0b1110 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::ONE } ], /* 0b1111 */ vec![], ]; /// Version of the MS case dictionary that is used by the solid tiles at the edge of the chunk static ref MST_EDGE_CASE_MAP: [Segment2I; 4] = [ /* up */ Segment2I { from: Vector2I::UP, to: Vector2I::ONE }, /* right */ Segment2I { from: Vector2I::ONE, to: Vector2I::RIGHT }, /* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO }, /* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP }, ]; } #[derive(Reflect, Component, Default)] #[reflect(Component)] pub struct TerrainChunk2D { pub index: Chunk2DIndex, } #[derive(Reflect, Component, Default)] #[reflect(Component)] pub struct TerrainChunkSpriteSync2D; #[derive(Reflect, Component, Default)] #[reflect(Component)] pub struct TerrainChunkCollisionSync2D; #[derive(Bundle, Default)] pub struct ChunkSpriteBundle { pub chunk: TerrainChunk2D, pub sync_flag: TerrainChunkSpriteSync2D, pub sprite: SpriteBundle, } #[derive(Bundle, Default)] pub struct ChunkColliderBundle { pub chunk: TerrainChunk2D, pub sync_flag: TerrainChunkCollisionSync2D, pub transform: TransformBundle, } pub type Chunk2DIndex = Vector2I; #[derive(Clone, Copy)] pub struct ChunkRect { pub min: Vector2I, pub max: Vector2I, } pub struct Chunk2D { pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize], // TODO: handle multiple dirty rects pub dirty_rect: Option, } impl Chunk2D { pub const SIZE_X: usize = 32; pub const SIZE_Y: usize = 32; pub const SIZE: Vector2I = Vector2I { x: Self::SIZE_X as i32, y: Self::SIZE_Y as i32, }; pub fn new() -> Chunk2D { Chunk2D { texels: Self::new_texel_array(), dirty_rect: None, } } pub fn new_full() -> Chunk2D { let mut chunk = Chunk2D { texels: Self::new_texel_array(), dirty_rect: None, }; for y in 0..Self::SIZE_Y { for x in 0..Self::SIZE_X { chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1); } } chunk } pub fn new_half() -> Chunk2D { let mut chunk = Chunk2D { texels: Self::new_texel_array(), dirty_rect: None, }; for y in 0..Self::SIZE_Y { for x in 0..Self::SIZE_X { if x <= Self::SIZE_Y - y { chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1); } } } chunk } pub fn new_circle() -> Chunk2D { let mut chunk = Chunk2D { texels: Self::new_texel_array(), dirty_rect: None, }; let origin = Self::SIZE / 2; let radius = Self::SIZE_X as i32 / 2; for y in 0..Self::SIZE_Y { for x in 0..Self::SIZE_X { let dx = (x as i32 - origin.x).abs(); let dy = (y as i32 - origin.y).abs(); if dx * dx + dy * dy <= (radius - 1) * (radius - 1) { chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1); } } } chunk } pub fn new_texel_array() -> [Texel2D; Self::SIZE_X * Self::SIZE_Y] { [Texel2D::default(); Self::SIZE_X * Self::SIZE_Y] } pub fn xy_vec() -> Vec { let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y); for y in 0..Self::SIZE_Y { for x in 0..Self::SIZE_X { result.push(Vector2I { x: x as i32, y: y as i32, }); } } result } pub fn mark_all_dirty(&mut self) { self.dirty_rect = Some(ChunkRect { min: Vector2I::ZERO, max: Self::SIZE, }); } pub fn mark_dirty(&mut self, position: &Vector2I) { match &self.dirty_rect { Some(rect) => { self.dirty_rect = Some(ChunkRect { min: Vector2I::min(&rect.min, position), max: Vector2I::max(&rect.max, position), }) } None => { self.dirty_rect = Some(ChunkRect { min: *position, max: *position, }) } } } pub fn mark_clean(&mut self) { self.dirty_rect = None; } pub fn get_texel(&self, position: &Vector2I) -> Option { local_to_texel_index(position).map(|i| self.texels[i]) } pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> { local_to_texel_index(position).map(|i| &mut self.texels[i]) } pub fn set_texel(&mut self, position: &Vector2I, id: TexelID) { let i = local_to_texel_index(position).expect("Texel index out of range"); if self.texels[i].id != id { self.mark_dirty(position); } let update_neighbours = self.texels[i].is_empty() != (Texel2D { id, ..self.texels[i] }) .is_empty(); self.texels[i].id = id; // Update neighbour mask if update_neighbours { for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS { // Flip neighbour's bit match self.get_texel_mut(&(*position + offset)) { Some(mut neighbour) => { neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset]; } None => (), } } } } pub fn create_texture_data(&self) -> Vec { let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); for y in (0..Chunk2D::SIZE_Y).rev() { for x in 0..Chunk2D::SIZE_X { let id = &self .get_texel(&Vector2I::new(x as i32, y as i32)) .unwrap() .id; let behaviour = TexelBehaviour2D::from_id(id); let color = behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.color); let color_data = color.as_rgba_u32(); let mut color_data: Vec = vec![ ((color_data >> 0) & 0xff) as u8, ((color_data >> 8) & 0xff) as u8, ((color_data >> 16) & 0xff) as u8, ((color_data >> 24) & 0xff) as u8, ]; image_data.append(&mut color_data); } } image_data } pub fn create_collision_data(&self) -> Vec> { let mut islands: Vec = Vec::new(); for i in 0..self.texels.len() { let local = texel_index_to_local(i); let edge_mask: u8 = if local.y == Chunk2D::SIZE.y - 1 { 1 << 0 } else { 0 } | if local.x == Chunk2D::SIZE.x - 1 { 1 << 1 } else { 0 } | if local.y == 0 { 1 << 2 } else { 0 } | if local.x == 0 { 1 << 3 } else { 0 }; let mut sides: Vec; if self.texels[i].is_empty() { sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize] .iter() .clone() .map(|side| Segment2I { from: side.from + local, to: side.to + local, }) .collect(); } else if !self.texels[i].is_empty() && edge_mask != 0 { sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2); for i in 0..MST_EDGE_CASE_MAP.len() { if edge_mask & (1 << i) != 0 { let edge = MST_EDGE_CASE_MAP[i]; sides.push(Segment2I { from: edge.from + local, to: edge.to + local, }) } } } else { continue; } for side in sides { // Check if the side can be attached to any island // The naming of front and back are kind of misleading, and come from the VecDeque type. // You can think of the front as the beginning of the island loop, and back the end. // Connect to an island if possible, otherwise create a new island { let mut connected_to: Option<&mut Island> = None; for island in islands.iter_mut() { if island.back().is_some() && island.back().unwrap().to == side.from { connected_to = Some(island); } } match connected_to { Some(back) => { back.push_back(side); } None => { let mut island: Island = Island::new(); island.push_back(side); islands.push(island); } } } // Find connected islands loop { let mut merge_index: Option = None; 'outer: for i in 0..islands.len() { for j in 0..islands.len() { if i == j { continue; } if islands[i].back().is_some() && islands[j].front().is_some() && islands[i].back().unwrap().to == islands[j].front().unwrap().from { merge_index = Some(i); break 'outer; } } } // Merge connected islands match merge_index { Some(index) => { let mut merge_from = islands.swap_remove(index); match islands.iter_mut().find(|island| match island.front() { Some(front) => front.from == merge_from.back().unwrap().to, None => false, }) { Some(merge_to) => loop { match merge_from.pop_back() { Some(segment) => merge_to.push_front(segment), None => break, } }, None => (), }; } None => break, } } } } let mut result: Vec> = Vec::with_capacity(islands.len()); for island in islands { if island.len() < 4 { continue; } let mut points: Vec = Vec::with_capacity(island.len() + 1); points.push(Vec2::from(island.front().unwrap().from)); let mut current_angle: Option = None; for side in island { if current_angle.is_some() && (current_angle.unwrap() - side.angle()).abs() < 0.1 { let len = points.len(); points[len - 1] = Vec2::from(side.to) } else { current_angle = Some(side.angle()); points.push(Vec2::from(side.to)); } } result.push(points); } result } } pub fn chunk_spawner( mut commands: Commands, mut terrain_events: EventReader, mut images: ResMut>, chunk_query: Query<(Entity, &TerrainChunk2D)>, ) { for terrain_event in terrain_events.iter() { match terrain_event { TerrainEvent2D::ChunkAdded(chunk_index) => { // Create unique handle for the image let mut image = Image::new( Extent3d { width: Chunk2D::SIZE_X as u32, height: Chunk2D::SIZE_Y as u32, depth_or_array_layers: 1, }, bevy::render::render_resource::TextureDimension::D2, vec![0x00; Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4], bevy::render::render_resource::TextureFormat::Rgba8Unorm, ); image.sampler_descriptor = ImageSampler::nearest(); let texture = images.add(image); let pos = Vec2::from(*chunk_index * Chunk2D::SIZE); commands .spawn(ChunkSpriteBundle { chunk: TerrainChunk2D { index: *chunk_index, }, sprite: SpriteBundle { sprite: Sprite { custom_size: Some(Vec2::from(Chunk2D::SIZE)), anchor: bevy::sprite::Anchor::BottomLeft, ..default() }, texture, transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)), ..default() }, ..default() }) .insert(Name::new(format!( "Chunk Sprite {},{}", chunk_index.x, chunk_index.y ))); commands .spawn(ChunkColliderBundle { chunk: TerrainChunk2D { index: *chunk_index, }, transform: TransformBundle::from_transform(Transform::from_translation( Vec3::new(pos.x, pos.y, 0.0), )), ..default() }) .insert(Name::new(format!( "Chunk Collider {},{}", chunk_index.x, chunk_index.y ))); } TerrainEvent2D::ChunkRemoved(chunk_index) => { for (entity, chunk) in chunk_query.iter() { if chunk.index == *chunk_index { commands.entity(entity).despawn_recursive(); } } } _ => (), } } } /** Update the chunk sprite as needed */ pub fn chunk_sprite_sync( mut terrain_events: EventReader, mut images: ResMut>, terrain: Res, added_chunk_query: Query< (Entity, &TerrainChunk2D), (With, Changed), >, chunk_query: Query<(Entity, &TerrainChunk2D), (With, With)>, texture_query: Query<&Handle>, ) { let mut updated_chunks: Vec<(Entity, &TerrainChunk2D, Option)> = vec![]; // Check for added components for (added_entity, added_chunk) in added_chunk_query.iter() { updated_chunks.push((added_entity, added_chunk, None)); } // Check for terrain events for event in terrain_events.iter() { for (entity, chunk) in chunk_query.iter() { let (chunk_index, rect) = match event { TerrainEvent2D::ChunkAdded(chunk_index) => { // The entity should not have the time to react to the event since it was just made // REM: This gets called when new chunk is instantiated with brush // println!("[chunk_sprite_sync -> TerrainEvent2D::ChunkAdded] This probably shouldn't be firing, maybe the chunk was destroyed and immediately created? chunk: {chunk_index:?}"); (chunk_index, None) } TerrainEvent2D::TexelsUpdated(chunk_index, rect) => (chunk_index, Some(*rect)), _ => continue, }; if *chunk_index != chunk.index { continue; }; updated_chunks.push((entity, chunk, rect)); } } // Update sprite for (entity, chunk, rect) in updated_chunks { let chunk = terrain.index_to_chunk(&chunk.index).unwrap(); // TODO: Update only the rect let _rect = rect.unwrap_or(ChunkRect { min: Vector2I::ZERO, max: Chunk2D::SIZE - Vector2I::ONE, }); let handle = texture_query.get(entity).unwrap(); let mut image = images.get_mut(handle).unwrap(); let image_data = chunk.create_texture_data(); image.data = image_data; } } /** Create and update colliders for chunk as needed */ pub fn chunk_collision_sync( mut terrain_events: EventReader, mut commands: Commands, terrain: Res, added_chunk_query: Query< (Entity, &TerrainChunk2D), (With, Changed), >, chunk_query: Query<(Entity, &TerrainChunk2D), With>, child_query: Query<&Children>, collider_query: Query<&Collider>, ) { let mut updated_chunks: Vec<(Entity, &TerrainChunk2D)> = vec![]; // Check for added components for (added_entity, added_chunk) in added_chunk_query.iter() { updated_chunks.push((added_entity, added_chunk)); } // Check for terrain events for event in terrain_events.iter() { for (entity, chunk) in chunk_query.iter() { let chunk_index = match event { TerrainEvent2D::ChunkAdded(chunk_index) => { // The entity should not have the time to react to the event since it was just made // REM: This gets called when new chunk is instantiated with brush // println!("[chunk_collision_sync -> TerrainEvent2D::ChunkAdded] This probably shouldn't be firing, maybe the chunk was destroyed and immediately created? chunk: {chunk_index:?}"); chunk_index } TerrainEvent2D::TexelsUpdated(chunk_index, _) => chunk_index, _ => continue, }; if *chunk_index != chunk.index { continue; }; updated_chunks.push((entity, chunk)); } } // let layer_membership = CollisionLayers::WORLD; // REM: Kinda messy, partly due do how entity creation is timed for (entity, chunk_component) in updated_chunks.iter() { let chunk = terrain.index_to_chunk(&chunk_component.index).unwrap(); let new_islands = chunk.create_collision_data(); // Create new colliders if let Ok(children) = child_query.get(*entity) { // Chunk has children, new ones will be created and old ones components will be removed for (index, island) in new_islands.iter().enumerate() { if let Some(child) = children.get(index) { // Replace collider commands .entity(*child) .insert(Collider::polyline(island.clone(), None)); } else { // Create new child commands.entity(*entity).with_children(|builder| { builder .spawn(Collider::polyline(island.clone(), None)) .insert(TransformBundle::default()) .insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL)) .insert(Name::new(format!("Island #{}", index))); }); } } } else { // Chunk doesn't have a Children component yet for (index, island) in new_islands.iter().enumerate() { commands.entity(*entity).with_children(|builder| { builder .spawn(Collider::polyline(island.clone(), None)) .insert(TransformBundle::default()) .insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL)) .insert(Name::new(format!("Island #{}", index))); }); } }; // Remove extra children. // Leaving them seems to cause weird problems with rapier when re-adding the collider. The collider is ignored until something else is updated. for children in child_query.get(*entity) { for (index, child) in children.iter().enumerate() { if let Ok(_) = collider_query.get(*child) { if index >= new_islands.len() { commands.entity(*child).despawn_recursive(); } } } } } }