From c3ea9f45138abcd2d3a4c585ffceaa882e5a9e72 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:14:49 +0200 Subject: [PATCH] updated texel and chunk structures to allow easier simulation --- src/game/debug.rs | 6 +- src/game/debug/terrain.rs | 2 +- src/terrain2d.rs | 27 ++++-- src/terrain2d/chunk2d.rs | 141 +++++++++++------------------ src/terrain2d/terrain_gen2d.rs | 4 +- src/terrain2d/texel2d.rs | 41 ++++----- src/terrain2d/texel_behaviour2d.rs | 33 ++++--- 7 files changed, 117 insertions(+), 137 deletions(-) diff --git a/src/game/debug.rs b/src/game/debug.rs index 202c153..2700666 100644 --- a/src/game/debug.rs +++ b/src/game/debug.rs @@ -1,7 +1,5 @@ use bevy::prelude::*; -// use bevy_inspector_egui::*; use bevy_prototype_debug_lines::DebugLinesPlugin; -// use bevy_rapier2d::prelude::*; mod terrain; @@ -12,8 +10,8 @@ pub struct DebugPlugin; impl Plugin for DebugPlugin { fn build(&self, app: &mut App) { app.add_plugin(DebugLinesPlugin::default()) - // .add_plugin(RapierDebugRenderPlugin::default()) - // .add_plugin(WorldInspectorPlugin::new()) + // .add_plugin(bevy_rapier2d::prelude::RapierDebugRenderPlugin::default()) + // .add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new()) .add_plugin(TerrainDebugPlugin); } } diff --git a/src/game/debug/terrain.rs b/src/game/debug/terrain.rs index 00b3234..f5f4ba5 100644 --- a/src/game/debug/terrain.rs +++ b/src/game/debug/terrain.rs @@ -128,7 +128,7 @@ fn debug_painter( ); if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right) { - terrain.set_texel(&pos, id, None) + terrain.set_texel(&pos, Texel2D { id, ..default() }, None) } } } diff --git a/src/terrain2d.rs b/src/terrain2d.rs index a64196e..08d22bc 100644 --- a/src/terrain2d.rs +++ b/src/terrain2d.rs @@ -120,8 +120,8 @@ fn terrain_simulation(mut terrain: ResMut, frame_counter: Res Option { + self.global_to_chunk(global).map_or(None, |chunk| { + chunk.get_latest_simulation(&global_to_local(global)) + }) + } + pub fn get_texel_behaviour( &self, global: &Vector2I, @@ -320,16 +326,22 @@ impl Terrain2D { ) } - pub fn set_texel(&mut self, global: &Vector2I, id: TexelID, simulation_frame: Option) { + pub fn set_texel( + &mut self, + global: &Vector2I, + new_texel: Texel2D, + simulation_frame: Option, + ) { if !self.is_within_boundaries(global) { return; } let index = global_to_chunk_index(global); let changed = match self.index_to_chunk_mut(&index) { - Some(chunk) => chunk.set_texel(&global_to_local(global), id, simulation_frame), + Some(chunk) => chunk.set_texel(&global_to_local(global), new_texel, simulation_frame), None => { let mut chunk = Chunk2D::new(); - let changed = chunk.set_texel(&global_to_local(global), id, simulation_frame); + let changed = + chunk.set_texel(&global_to_local(global), new_texel, simulation_frame); self.add_chunk(index, chunk); changed } @@ -348,9 +360,10 @@ impl Terrain2D { to_global: &Vector2I, simulation_frame: Option, ) { - let from = self.get_texel(from_global).map_or(0, |t| t.id); - let to = self.get_texel(to_global).map_or(0, |t| t.id); + let from = self.get_texel(from_global).unwrap_or(Texel2D::default()); + let to = self.get_texel(to_global).unwrap_or(Texel2D::default()); self.set_texel(to_global, from, simulation_frame); + // REM: The displaced texel is also marked as simulated self.set_texel(from_global, to, simulation_frame); } } diff --git a/src/terrain2d/chunk2d.rs b/src/terrain2d/chunk2d.rs index 63d3373..24559f8 100644 --- a/src/terrain2d/chunk2d.rs +++ b/src/terrain2d/chunk2d.rs @@ -1,18 +1,13 @@ use std::collections::VecDeque; -use super::{ - local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, - TexelBehaviour2D, TexelID, NEIGHBOUR_INDEX_MAP, -}; +use super::*; use crate::util::{CollisionLayers, Segment2I, Vector2I}; -use bevy::{ - prelude::*, - render::{render_resource::Extent3d, texture::ImageSampler}, -}; -use bevy_rapier2d::prelude::*; +use bevy::render::{render_resource::Extent3d, texture::ImageSampler}; use lazy_static::lazy_static; type Island = VecDeque; +pub type Chunk2DIndex = Vector2I; +pub type NeighbourMask = u8; lazy_static! { /// Marching Square case dictionary. @@ -49,6 +44,14 @@ lazy_static! { /* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO }, /* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP }, ]; + + static ref NEIGHBOUR_INDEX_MAP: HashMap = { + let mut map = HashMap::new(); + for i in 0..Chunk2D::NEIGHBOUR_OFFSET_VECTORS.len() { + map.insert(Chunk2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8); + } + map + }; } #[derive(Reflect, Component, Default)] @@ -79,8 +82,6 @@ pub struct ChunkColliderBundle { pub transform: TransformBundle, } -pub type Chunk2DIndex = Vector2I; - #[derive(Clone, Copy)] pub struct ChunkRect { pub min: Vector2I, @@ -97,7 +98,11 @@ impl ChunkRect { } pub struct Chunk2D { - pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize], + pub texels: [Texel2D; Self::SIZE_X * Self::SIZE_Y], + /// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order + pub neighbour_mask: [NeighbourMask; Self::SIZE_X * Self::SIZE_Y], + /// Used in simulation step so that texels won't be updated twice. Value of 0 is always updated. + pub simulation_frames: [u8; Self::SIZE_X * Self::SIZE_Y], // TODO: handle multiple dirty rects? pub dirty_rect: Option, } @@ -109,65 +114,22 @@ impl Chunk2D { x: Self::SIZE_X as i32, y: Self::SIZE_Y as i32, }; + pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [ + Vector2I { x: 0, y: 1 }, + Vector2I { x: 1, y: 0 }, + Vector2I { x: 0, y: -1 }, + Vector2I { x: -1, y: 0 }, + ]; pub fn new() -> Chunk2D { Chunk2D { - texels: Self::new_texel_array(), + texels: [Texel2D::default(); Self::SIZE_X * Self::SIZE_Y], + neighbour_mask: [0; Self::SIZE_X * Self::SIZE_Y], + simulation_frames: [0; Self::SIZE_X * Self::SIZE_Y], 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, None); - } - } - 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, None); - } - } - } - 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, None); - } - } - } - 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 { @@ -208,6 +170,10 @@ impl Chunk2D { local_to_texel_index(position).map(|i| self.texels[i]) } + pub fn get_latest_simulation(&self, position: &Vector2I) -> Option { + local_to_texel_index(position).map(|i| self.simulation_frames[i]) + } + pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> { local_to_texel_index(position).map(|i| &mut self.texels[i]) } @@ -215,46 +181,44 @@ impl Chunk2D { pub fn set_texel( &mut self, position: &Vector2I, - id: TexelID, + new_texel: Texel2D, simulation_frame: Option, ) -> bool { let i = local_to_texel_index(position).expect("Texel index out of range"); - if self.texels[i].id != id { - self.mark_dirty(position); + if self.texels[i] == new_texel { + return false; } - let update_neighbours = TexelBehaviour2D::has_collision(&self.texels[i].id) - != TexelBehaviour2D::has_collision(&id); - let changed = self.texels[i].id != id; - self.texels[i].id = id; + self.mark_dirty(position); + let update_neighbours = self.texels[i].has_collision() != new_texel.has_collision(); + self.texels[i] = new_texel; + // Update simulation frame if let Some(simulation_frame) = simulation_frame { - self.texels[i].last_simulation = simulation_frame; + self.simulation_frames[i] = simulation_frame; } // Update neighbour mask if update_neighbours { - for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS { + for offset in Self::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]; + match local_to_texel_index(&(*position + offset)) { + Some(index) => { + self.neighbour_mask[index] ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset]; } None => (), } } } - changed + true } 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 = + let texel = &self.get_texel(&Vector2I::new(x as i32, y as i32)).unwrap(); + let behaviour = texel.behaviour(); + let mut color = behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.color); + color.set_a(color.a() * ((texel.density as f32) / 256.0)); let color_data = color.as_rgba_u32(); let mut color_data: Vec = vec![ ((color_data >> 0) & 0xff) as u8, @@ -268,6 +232,7 @@ impl Chunk2D { image_data } + // TODO: Don't create collision for falling texels, it's pretty annoying that a stream of small grains blocks movement pub fn create_collision_data(&self) -> Vec> { let mut islands: Vec = Vec::new(); for i in 0..self.texels.len() { @@ -287,7 +252,7 @@ impl Chunk2D { let mut sides: Vec; let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id); if !has_collision { - sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize] + sides = MST_CASE_MAP[self.neighbour_mask[i] as usize] .iter() .clone() .map(|side| Segment2I { @@ -473,9 +438,7 @@ pub fn chunk_spawner( } } -/** - Update the chunk sprite as needed -*/ +/// Update the chunk sprite as needed pub fn chunk_sprite_sync( mut terrain_events: EventReader, mut images: ResMut>, @@ -532,9 +495,7 @@ pub fn chunk_sprite_sync( } } -/** - Create and update colliders for chunk as needed -*/ +/// Create and update colliders for chunk as needed pub fn chunk_collision_sync( mut terrain_events: EventReader, mut commands: Commands, diff --git a/src/terrain2d/terrain_gen2d.rs b/src/terrain2d/terrain_gen2d.rs index 307697d..1fde062 100644 --- a/src/terrain2d/terrain_gen2d.rs +++ b/src/terrain2d/terrain_gen2d.rs @@ -1,6 +1,6 @@ use noise::{NoiseFn, PerlinSurflet}; -use super::{chunk_index_to_global, Chunk2D, Chunk2DIndex}; +use super::*; pub struct TerrainGen2D { pub seed: u32, @@ -39,7 +39,7 @@ impl TerrainGen2D { id = 13; } - chunk.set_texel(&local, id, None); + chunk.set_texel(&local, Texel2D { id, ..default() }, None); } chunk } diff --git a/src/terrain2d/texel2d.rs b/src/terrain2d/texel2d.rs index e823aa6..0ca9c07 100644 --- a/src/terrain2d/texel2d.rs +++ b/src/terrain2d/texel2d.rs @@ -1,35 +1,32 @@ -use lazy_static::lazy_static; -use std::collections::HashMap; - pub use u8 as TexelID; -pub use u8 as NeighbourMask; -use crate::util::Vector2I; +use super::TexelBehaviour2D; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, PartialEq)] pub struct Texel2D { + /// Identifier for a set of properties pub id: TexelID, - /// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order - pub neighbour_mask: NeighbourMask, - pub last_simulation: u8, + /// Used by gas materials + pub density: u8, } -lazy_static! { - pub static ref NEIGHBOUR_INDEX_MAP: HashMap = { - let mut map = HashMap::new(); - for i in 0..Texel2D::NEIGHBOUR_OFFSET_VECTORS.len() { - map.insert(Texel2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8); +impl Default for Texel2D { + fn default() -> Self { + Self { + id: TexelID::default(), + density: u8::MAX, } - map - }; + } } impl Texel2D { pub const EMPTY: TexelID = 0; - pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [ - Vector2I { x: 0, y: 1 }, - Vector2I { x: 1, y: 0 }, - Vector2I { x: 0, y: -1 }, - Vector2I { x: -1, y: 0 }, - ]; + + pub fn has_collision(&self) -> bool { + TexelBehaviour2D::has_collision(&self.id) + } + + pub fn behaviour(&self) -> Option { + TexelBehaviour2D::from_id(&self.id) + } } diff --git a/src/terrain2d/texel_behaviour2d.rs b/src/terrain2d/texel_behaviour2d.rs index 1c3ed6c..8a7be0d 100644 --- a/src/terrain2d/texel_behaviour2d.rs +++ b/src/terrain2d/texel_behaviour2d.rs @@ -14,7 +14,7 @@ lazy_static! { TexelBehaviour2D { name: Cow::Borrowed("loose sand"), color: Color::rgb(0.61, 0.49, 0.38), - gravity: Some(TexelGravity::Down(100)), + gravity: Some(TexelGravity::Down(200)), has_collision: true, ..default() }, @@ -25,7 +25,7 @@ lazy_static! { TexelBehaviour2D { name: Cow::Borrowed("loose stone"), color: Color::rgb(0.21, 0.19, 0.17), - gravity: Some(TexelGravity::Down(100)), + gravity: Some(TexelGravity::Down(200)), has_collision: true, ..default() }, @@ -36,7 +36,7 @@ lazy_static! { TexelBehaviour2D { name: Cow::Borrowed("loose sturdy stone"), color: Color::rgb(0.11, 0.11, 0.11), - gravity: Some(TexelGravity::Down(100)), + gravity: Some(TexelGravity::Down(200)), has_collision: true, ..default() }, @@ -48,7 +48,7 @@ lazy_static! { name: Cow::Borrowed("water"), color: Color::rgba(0.0, 0.0, 1.0, 0.5), form: TexelForm::Liquid, - gravity: Some(TexelGravity::Down(10)), + gravity: Some(TexelGravity::Down(50)), ..default() }, ); @@ -57,9 +57,9 @@ lazy_static! { 5, TexelBehaviour2D { name: Cow::Borrowed("oil"), - color: Color::rgba(0.0, 1.0, 0.0, 0.5), - form: TexelForm::Gas, - gravity: Some(TexelGravity::Up(50)), + color: Color::rgba(0.5, 0.5, 0.25, 0.5), + form: TexelForm::Liquid, + gravity: Some(TexelGravity::Down(20)), ..default() }, ); @@ -67,10 +67,21 @@ lazy_static! { result.insert( 6, TexelBehaviour2D { - name: Cow::Borrowed("gas"), - color: Color::rgba(0.5, 0.5, 0.25, 0.5), - form: TexelForm::Liquid, - gravity: Some(TexelGravity::Down(5)), + name: Cow::Borrowed("light gas"), + color: Color::rgba(0.0, 1.0, 0.0, 0.5), + form: TexelForm::Gas, + gravity: Some(TexelGravity::Up(10)), + ..default() + }, + ); + + result.insert( + 7, + TexelBehaviour2D { + name: Cow::Borrowed("heavy gas"), + color: Color::rgba(1.0, 1.0, 1.0, 0.5), + form: TexelForm::Gas, + gravity: Some(TexelGravity::Down(10)), ..default() }, );