From 21255120c5592ea9b46ce0f98e9555464634f966 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Mon, 12 Dec 2022 04:22:25 +0200 Subject: [PATCH] feat: reactive collision/sprite updates --- Cargo.lock | 10 + Cargo.toml | 1 + src/game.rs | 19 +- src/game/chunk.rs | 17 -- src/terrain2d.rs | 26 +- src/terrain2d/chunk2d.rs | 502 +++++++++++++++++++++++++-------------- 6 files changed, 368 insertions(+), 207 deletions(-) delete mode 100644 src/game/chunk.rs diff --git a/Cargo.lock b/Cargo.lock index 564875d..351e7fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -627,6 +627,15 @@ dependencies = [ "radsort", ] +[[package]] +name = "bevy_prototype_debug_lines" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e933f5bf971fbc7e83b447db6758ed3b091faf4cd8e524a9406ae7acca096" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_ptr" version = "0.9.0" @@ -2071,6 +2080,7 @@ version = "0.1.0" dependencies = [ "bevy", "bevy-inspector-egui", + "bevy_prototype_debug_lines", "bevy_rapier2d", "lazy_static", "noise", diff --git a/Cargo.toml b/Cargo.toml index 56856fa..4a75326 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] bevy = { version = "0.9.0", features = ["dynamic"] } bevy-inspector-egui = "0.14.0" +bevy_prototype_debug_lines = "0.9.0" bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" } lazy_static = "1.4.0" noise = "0.8.2" diff --git a/src/game.rs b/src/game.rs index 9db1231..3c7fb37 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,5 +1,6 @@ use bevy::{input::mouse::MouseWheel, prelude::*}; use bevy_inspector_egui::*; +use bevy_prototype_debug_lines::DebugLinesPlugin; use bevy_rapier2d::prelude::*; use crate::{ @@ -14,13 +15,13 @@ use self::{ }; pub mod camera; -pub mod chunk; pub mod kinematic; pub mod player; pub fn init() { App::new() .add_plugins(DefaultPlugins) + .add_plugin(DebugLinesPlugin::default()) .add_plugin(RapierPhysicsPlugin::::default()) .add_plugin(RapierDebugRenderPlugin::default()) .add_plugin(WorldInspectorPlugin::new()) @@ -53,11 +54,17 @@ fn setup_debug_terrain(mut commands: Commands, mut terrain: ResMut) { } } - commands.spawn(Name::new("Left wall")) + commands + .spawn(Name::new("Left wall")) .insert(Collider::halfspace(Vec2::X).unwrap()) - .insert(TransformBundle::from_transform(Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)))); - - commands.spawn(Name::new("Right wall")) + .insert(TransformBundle::from_transform( + Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + )); + + commands + .spawn(Name::new("Right wall")) .insert(Collider::halfspace(Vec2::NEG_X).unwrap()) - .insert(TransformBundle::from_transform(Transform::from_translation(Vec3::new(WORLD_WIDTH as f32, 0.0, 0.0)))); + .insert(TransformBundle::from_transform( + Transform::from_translation(Vec3::new(WORLD_WIDTH as f32, 0.0, 0.0)), + )); } diff --git a/src/game/chunk.rs b/src/game/chunk.rs deleted file mode 100644 index 26bd244..0000000 --- a/src/game/chunk.rs +++ /dev/null @@ -1,17 +0,0 @@ -use bevy::prelude::*; - -use crate::terrain2d::Chunk2DIndex; - -pub struct ChunkPlugin {} - -#[derive(Reflect, Component, Default)] -#[reflect(Component)] -pub struct Chunk { - pub index: Chunk2DIndex, -} - -#[derive(Bundle)] -pub struct ChunkBundle { - pub chunk: Chunk, - pub sprite_bundle: SpriteBundle, -} diff --git a/src/terrain2d.rs b/src/terrain2d.rs index 006e7d3..563f860 100644 --- a/src/terrain2d.rs +++ b/src/terrain2d.rs @@ -19,30 +19,32 @@ pub struct Terrain2DPlugin; impl Plugin for Terrain2DPlugin { fn build(&self, app: &mut App) { - app.register_type::() + app.register_type::() .insert_resource(Terrain2D::new()) - .add_event::() - .add_system(emit_terrain_events) - .add_system(chunk_spawner); + .add_event::() + .add_system_to_stage(CoreStage::PostUpdate, emit_terrain_events) + .add_system(chunk_spawner) + .add_system(chunk_sprite_sync) + .add_system(chunk_collision_sync); } } fn emit_terrain_events( mut terrain: ResMut, - mut terrain_events: EventWriter, + mut terrain_events: EventWriter, ) { for event in terrain.events.drain(..) { terrain_events.send(event) } - for (chunk_index, mut chunk) in terrain.chunk_iter_mut() { + for (chunk_index, chunk) in terrain.chunk_iter_mut() { if let Some(rect) = &chunk.dirty_rect { - terrain_events.send(TerrainEvent::TexelsUpdated(*chunk_index, *rect)); - chunk.dirty_rect = None; + terrain_events.send(TerrainEvent2D::TexelsUpdated(*chunk_index, *rect)); + chunk.mark_clean(); } } } -pub enum TerrainEvent { +pub enum TerrainEvent2D { ChunkAdded(Chunk2DIndex), ChunkRemoved(Chunk2DIndex), TexelsUpdated(Chunk2DIndex, ChunkRect), @@ -51,7 +53,7 @@ pub enum TerrainEvent { #[derive(Default, Resource)] pub struct Terrain2D { chunk_map: HashMap, - events: Vec, + events: Vec, } impl Terrain2D { @@ -64,11 +66,11 @@ impl Terrain2D { pub fn add_chunk(&mut self, index: Chunk2DIndex, chunk: Chunk2D) { self.chunk_map.insert(index, chunk); - self.events.push(TerrainEvent::ChunkAdded(index)) + self.events.push(TerrainEvent2D::ChunkAdded(index)) } pub fn remove_chunk(&mut self, index: Chunk2DIndex) { - self.events.push(TerrainEvent::ChunkRemoved(index)); + self.events.push(TerrainEvent2D::ChunkRemoved(index)); self.chunk_map.remove(&index); } diff --git a/src/terrain2d/chunk2d.rs b/src/terrain2d/chunk2d.rs index e26bda6..d157497 100644 --- a/src/terrain2d/chunk2d.rs +++ b/src/terrain2d/chunk2d.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, VecDeque}; use super::{ - local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent, Texel2D, TexelID, + local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, TexelID, NEIGHBOUR_INDEX_MAP, }; use crate::util::{Segment2I, Vector2I}; @@ -64,14 +64,30 @@ lazy_static! { #[derive(Reflect, Component, Default)] #[reflect(Component)] -pub struct Chunk2DHandler { +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 ChunkBundle { - pub chunk: Chunk2DHandler, - pub sprite_bundle: SpriteBundle, +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; @@ -191,6 +207,10 @@ impl Chunk2D { } } + 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]) } @@ -224,40 +244,171 @@ impl Chunk2D { } } } + + pub fn create_texture_data(&self) -> Vec { + let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); + let fallback: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; + for y in (0..Chunk2D::SIZE_Y).rev() { + for x in 0..Chunk2D::SIZE_X { + image_data.append( + &mut COLOR_MAP + .get( + &self + .get_texel(&Vector2I::new(x as i32, y as i32)) + .unwrap() + .id, + ) + .unwrap_or(&fallback) + .to_vec() + .clone(), + ); + } + } + 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 terrain_events: EventReader, mut images: ResMut>, - terrain: Res, - chunk_query: Query<(Entity, &Chunk2DHandler)>, + chunk_query: Query<(Entity, &TerrainChunk2D)>, ) { for terrain_event in terrain_events.iter() { match terrain_event { - TerrainEvent::ChunkAdded(chunk_index) => { - let chunk = terrain.index_to_chunk(chunk_index).unwrap(); - - // Chunk sprite - // TODO: Move to separate function - let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); - let fallback: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; - for y in (0..Chunk2D::SIZE_Y).rev() { - for x in 0..Chunk2D::SIZE_X { - image_data.append( - &mut COLOR_MAP - .get( - &chunk - .get_texel(&Vector2I::new(x as i32, y as i32)) - .unwrap() - .id, - ) - .unwrap_or(&fallback) - .to_vec() - .clone(), - ); - } - } + TerrainEvent2D::ChunkAdded(chunk_index) => { + // Create unique handle for the image + // TODO: recycling image data would be nice let mut image = Image::new( Extent3d { width: Chunk2D::SIZE_X as u32, @@ -265,24 +416,19 @@ pub fn chunk_spawner( depth_or_array_layers: 1, }, bevy::render::render_resource::TextureDimension::D2, - image_data, + 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); - // Chunk collision - // TODO: Move to separate function - let collision_islands = generate_collision(chunk); - let pos = Vec2::from(*chunk_index * Chunk2D::SIZE); commands - .spawn(ChunkBundle { - chunk: Chunk2DHandler { + .spawn(ChunkSpriteBundle { + chunk: TerrainChunk2D { index: *chunk_index, }, - sprite_bundle: SpriteBundle { + sprite: SpriteBundle { sprite: Sprite { custom_size: Some(Vec2::from(Chunk2D::SIZE)), anchor: bevy::sprite::Anchor::BottomLeft, @@ -292,160 +438,172 @@ pub fn chunk_spawner( transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)), ..default() }, + ..default() }) .insert(Name::new(format!( - "Chunk {},{}", + "Chunk Sprite {},{}", chunk_index.x, chunk_index.y - ))) - .with_children(|builder| { - let mut index = 1; - for island in collision_islands.iter() { - builder - .spawn(Collider::polyline(island.clone(), None)) - .insert(TransformBundle::default()) - .insert(Name::new(format!("Collision #{index}"))); - index += 1; - } - }); + ))); + + 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 + ))); } - TerrainEvent::ChunkRemoved(chunk_index) => { + TerrainEvent2D::ChunkRemoved(chunk_index) => { for (entity, chunk) in chunk_query.iter() { if chunk.index == *chunk_index { commands.entity(entity).despawn_recursive(); } } } - TerrainEvent::TexelsUpdated(chunk_index, rect) => {} + _ => (), } } } -pub fn generate_collision(chunk: &Chunk2D) -> Vec> { - let mut islands: Vec = Vec::new(); - for i in 0..chunk.texels.len() { - let local = texel_index_to_local(i); +/** + Update the chunk sprite as needed +*/ +pub fn chunk_sprite_sync( + mut terrain_events: EventReader, + mut commands: Commands, + mut images: ResMut>, + mut sprite_query: Query<&mut Sprite>, + 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![]; - 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 }; + // Check for added components + for (added_entity, added_chunk) in added_chunk_query.iter() { + updated_chunks.push((added_entity, added_chunk, None)); + } - let mut sides: Vec; - if chunk.texels[i].is_empty() { - sides = MST_CASE_MAP[chunk.texels[i].neighbour_mask as usize] - .iter() - .clone() - .map(|side| Segment2I { - from: side.from + local, - to: side.to + local, - }) - .collect(); - } else if !chunk.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, - }) + // 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 + 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) } - } - } else { - continue; - } + TerrainEvent2D::TexelsUpdated(chunk_index, rect) => (chunk_index, Some(*rect)), + _ => 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. + if *chunk_index != chunk.index { + continue; + }; - // 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, - } - } + updated_chunks.push((entity, chunk, rect)); } } - 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)); + // Update sprite + for (entity, chunk, rect) in updated_chunks { + let chunk = terrain.index_to_chunk(&chunk.index).unwrap(); + let rect = rect.unwrap_or(ChunkRect { + min: Vector2I::ZERO, + max: Chunk2D::SIZE - Vector2I::ONE, + }); + + let mut sprite = match sprite_query.get_mut(entity) { + Ok(sprite) => sprite, + Err(err) => { + println!("[chunk_sprite_sync] Sprite component not found for entity:"); + commands.entity(entity).log_components(); + println!("{err:?}"); + continue; } - } - result.push(points); + }; + + 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_collider_query: Query<&Children, With>, +) { + let mut updated_chunks = 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 + 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)); + } + } + + for (entity, chunk) in updated_chunks.iter() { + // Remove old colliders + for children in child_collider_query.get(*entity) { + for child in children { + commands.entity(*child).despawn_recursive() + } + } + + let chunk = terrain.index_to_chunk(&chunk.index).unwrap(); + + // Add new colliders + let collision_islands = chunk.create_collision_data(); + commands.entity(*entity).with_children(|builder| { + let mut index = 1; + for island in collision_islands.iter() { + builder + .spawn(Collider::polyline(island.clone(), None)) + .insert(TransformBundle::default()) + .insert(Name::new(format!("Island #{index}"))); + index += 1; + } + }); } - result }