From 76deff3f2f1b281ebef393af8bbdfcc8f981ca79 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Sat, 31 Dec 2022 20:14:25 +0200 Subject: [PATCH] feat: first iteration of gas dispersion --- src/game/debug/terrain.rs | 82 ++++++++++--- src/game/player.rs | 2 +- src/terrain2d.rs | 188 ++++++++++++++++++++++++++++- src/terrain2d/texel2d.rs | 2 +- src/terrain2d/texel_behaviour2d.rs | 18 ++- 5 files changed, 265 insertions(+), 27 deletions(-) diff --git a/src/game/debug/terrain.rs b/src/game/debug/terrain.rs index f5f4ba5..7e00b34 100644 --- a/src/game/debug/terrain.rs +++ b/src/game/debug/terrain.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I}; use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget}; use bevy_prototype_debug_lines::DebugLines; @@ -7,7 +9,8 @@ pub struct TerrainDebugPlugin; impl Plugin for TerrainDebugPlugin { fn build(&self, app: &mut App) { app.insert_resource(TerrainBrush2D::default()) - .add_system_to_stage(TerrainStages::EventHandler, dirty_rect_visualizer) + // .add_system_to_stage(TerrainStages::EventHandler, dirty_rect_visualizer) + // .add_system_to_stage(CoreStage::Last, chunk_debugger) .add_system(debug_painter); } } @@ -20,7 +23,7 @@ struct TerrainBrush2D { impl Default for TerrainBrush2D { fn default() -> Self { - TerrainBrush2D { radius: 5, tile: 4 } + TerrainBrush2D { radius: 3, tile: 7 } } } @@ -146,22 +149,69 @@ fn dirty_rect_visualizer(terrain: Res, mut debug_draw: ResMut, mut debug_draw: ResMut) { + for (chunk_index, chunk) in terrain.chunk_iter() { + println!("chunk contents: {chunk_index:?}"); + let offset = Vec3::from(chunk_index_to_global(chunk_index)); + let min = offset + Vec3::ZERO; + let max = offset + Vec3::from(Chunk2D::SIZE); + draw_box( + &mut debug_draw, + min, + max, + Color::rgba(0.5, 0.0, 0.5, 0.5), + 0.0, + ); + + let mut tile_counter: HashMap = HashMap::new(); + for y in 0..Chunk2D::SIZE_Y as i32 { + for x in 0..Chunk2D::SIZE_X as i32 { + let local = Vector2I::new(x, y); + let global = local_to_global(&local, chunk_index); + if let (Some(texel), _) = terrain.get_texel_behaviour(&global) { + if !tile_counter.contains_key(&texel.id) { + tile_counter.insert(texel.id, (0, 0)); + } + let (old_count, old_density) = tile_counter[&texel.id].clone(); + tile_counter.insert( + texel.id, + (old_count + 1, old_density + texel.density as u32), + ); + } + } + } + + let mut counts: Vec<(u8, String, u32, u32)> = vec![]; + + for (id, (count, total_density)) in tile_counter.iter() { + let name = + TexelBehaviour2D::from_id(id).map_or("unknown".to_string(), |b| b.name.to_string()); + counts.push((*id, name, *count, *total_density)); + } + counts.sort_unstable_by_key(|c| c.0); + for (id, name, count, total_density) in counts { + println!( + "\tmaterial: {name:<24}id: {id:<8}count: {count:<8}total_density: {total_density:<8}" ); } } } + +fn draw_box(debug_draw: &mut DebugLines, min: Vec3, max: Vec3, color: Color, duration: f32) { + let points = vec![ + Vec3::new(min.x, min.y, min.z), + Vec3::new(max.x, min.y, min.z), + Vec3::new(max.x, max.y, min.z), + Vec3::new(min.x, max.y, min.z), + ]; + for i in 0..points.len() { + debug_draw.line_colored(points[i], points[(i + 1) % points.len()], duration, color); + } +} diff --git a/src/game/player.rs b/src/game/player.rs index 676bd9e..67540fb 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -94,6 +94,6 @@ pub fn player_spawn(mut commands: Commands) { .insert(Sleeping::disabled()) .insert(CameraFollow { priority: 1, - movement: FollowMovement::Smooth(18.0), + movement: FollowMovement::Instant, }); } diff --git a/src/terrain2d.rs b/src/terrain2d.rs index 08d22bc..c5be6ec 100644 --- a/src/terrain2d.rs +++ b/src/terrain2d.rs @@ -104,9 +104,10 @@ fn terrain_simulation(mut terrain: ResMut, frame_counter: Res = (rect.min.y..rect.max.y + 1).collect(); let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect(); - if frame_counter.frame % 2 == 0 { y_range.reverse(); } @@ -129,6 +130,125 @@ fn terrain_simulation(mut terrain: ResMut, frame_counter: Res>(); + let x_range = + ((rect.min.x - alternate)..rect.max.x + 1 + alternate).collect::>(); + const DISPERSION_WIDTH: usize = 2; + const DISPERSION_HEIGHT: usize = 2; + for y_arr in y_range.chunks(DISPERSION_HEIGHT) { + for x_arr in x_range.chunks(DISPERSION_WIDTH) { + let mut global_positions = vec![]; + for y in y_arr.iter() { + for x in x_arr.iter() { + let local = Vector2I::new(*x, *y); + let global = local_to_global(&local, &chunk_index); + global_positions.push(global); + } + } + + // Distribute gas + disperse_gas(global_positions, &mut terrain, &frame_counter) + } + } + } + } +} + +fn disperse_gas( + global_positions: Vec, + terrain: &mut Terrain2D, + frame_counter: &FrameCounter, +) { + use u32 as Capacity; + let mut total_densities: HashMap = HashMap::new(); + // let mut total_densities: Vec<(TexelID, Capacity)> = vec![]; + let mut valid_globals = vec![]; + for global in global_positions.iter() { + let (texel, behaviour) = terrain.get_texel_behaviour(global); + if behaviour.clone().map_or(true, |b| b.form == TexelForm::Gas) { + valid_globals.push(*global); + } + match (texel, behaviour) { + (Some(texel), Some(behaviour)) => { + if behaviour.form == TexelForm::Gas { + total_densities.insert( + texel.id, + texel.density as u32 + + total_densities.get(&texel.id).map_or(0, |density| *density), + ); + } + } + (_, _) => (), + } + } + + let mut total_densities: Vec<(TexelID, Capacity)> = + total_densities.iter().map(|(t, d)| (*t, *d)).collect(); + + if total_densities.len() == 0 { + return; + } + + total_densities.sort_unstable_by_key(|(_, density)| *density); + total_densities.reverse(); + + const TILE_CAPACITY: u32 = u8::MAX as u32; + let free_slots = valid_globals.len() as u32 + - total_densities + .iter() + .map(|(_, v)| (*v / (TILE_CAPACITY + 1)) + 1) + .sum::(); + + // Allocate slots + let mut slots: Vec<(TexelID, u32)> = vec![]; + for (id, density) in total_densities.iter() { + let min_slots = (density / (TILE_CAPACITY + 1)) + 1; + slots.push((*id, min_slots)); + } + for i in 0..free_slots as usize { + let len = slots.len(); + slots[i % len].1 += 1; + } + + // Disperse into given slots + let mut texels: Vec = vec![]; + for (id, total_density) in total_densities.iter() { + let slots = slots.iter().find(|s| s.0 == *id).unwrap().1; + let mut density_left = *total_density; + for i in 0..slots { + let density = if i < (slots - 1) { + (total_density / slots).min(density_left) + } else { + density_left + } + .min(u8::MAX as u32); + if density > 0 { + texels.push(Texel2D { + id: *id, + density: density as u8, + }); + density_left -= density; + } + } + } + + // Apply changes + if texels.len() > valid_globals.len() { + panic!("disperse_gas() - valid_globals is shorter than texels"); + } + + for i in 0..valid_globals.len() { + let global = valid_globals[i]; + if i < texels.len() { + let texel = texels[i]; + terrain.set_texel(&global, texel, None); + } else { + terrain.set_texel(&global, Texel2D::default(), None) } } } @@ -153,6 +273,9 @@ fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &Fra terrain.swap_texels(&global, &grav_pos, Some(simulation_frame)); return; } + if terrain.can_transfer_density(&global, &grav_pos) { + terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame)) + } } // Try "sliding" @@ -170,6 +293,9 @@ fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &Fra terrain.swap_texels(&global, &slide_pos, Some(simulation_frame)); return; } + if terrain.can_transfer_density(&global, &grav_pos) { + terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame)) + } } } } @@ -278,7 +404,7 @@ impl Terrain2D { pub fn is_within_boundaries(&self, global: &Vector2I) -> bool { if let Some(top) = self.top_boundary { - if global.y > top { + if global.y >= top { return false; } } @@ -293,7 +419,7 @@ impl Terrain2D { } } if let Some(right) = self.right_boundary { - if global.x > right { + if global.x >= right { return false; } } @@ -360,12 +486,64 @@ impl Terrain2D { to_global: &Vector2I, simulation_frame: Option, ) { - let from = self.get_texel(from_global).unwrap_or(Texel2D::default()); - let to = self.get_texel(to_global).unwrap_or(Texel2D::default()); + let from = self.get_texel(from_global).unwrap_or_default(); + let to = self.get_texel(to_global).unwrap_or_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); } + + fn can_transfer_density(&self, from_global: &Vector2I, to_global: &Vector2I) -> bool { + let from = self.get_texel(from_global).unwrap_or_default(); + let to = self.get_texel(to_global).unwrap_or_default(); + if from.id != to.id { + return false; + } + + let behaviour = if let Some(behaviour) = from.behaviour() { + behaviour + } else { + return false; + }; + + behaviour.form == TexelForm::Gas + } + + fn transfer_density( + &mut self, + from_global: &Vector2I, + to_global: &Vector2I, + gravity: TexelGravity, + simulation_frame: Option, + ) { + let from = self.get_texel(from_global).unwrap_or_default(); + let to = self.get_texel(to_global).unwrap_or_default(); + let max_transfer = match gravity { + TexelGravity::Down(grav) => grav, + TexelGravity::Up(grav) => grav, + }; + let transfer = (u8::MAX - to.density).min(max_transfer).min(from.density); + if from.density - transfer == 0 { + self.set_texel(&from_global, Texel2D::default(), simulation_frame); + } else { + self.set_texel( + &from_global, + Texel2D { + density: from.density - transfer, + ..from + }, + simulation_frame, + ); + } + self.set_texel( + &to_global, + Texel2D { + density: to.density + transfer, + ..to + }, + simulation_frame, + ); + } } pub fn local_to_texel_index(position: &Vector2I) -> Option { diff --git a/src/terrain2d/texel2d.rs b/src/terrain2d/texel2d.rs index 0ca9c07..2273968 100644 --- a/src/terrain2d/texel2d.rs +++ b/src/terrain2d/texel2d.rs @@ -2,7 +2,7 @@ pub use u8 as TexelID; use super::TexelBehaviour2D; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Texel2D { /// Identifier for a set of properties pub id: TexelID, diff --git a/src/terrain2d/texel_behaviour2d.rs b/src/terrain2d/texel_behaviour2d.rs index 8a7be0d..b85182b 100644 --- a/src/terrain2d/texel_behaviour2d.rs +++ b/src/terrain2d/texel_behaviour2d.rs @@ -79,13 +79,23 @@ lazy_static! { 7, TexelBehaviour2D { name: Cow::Borrowed("heavy gas"), - color: Color::rgba(1.0, 1.0, 1.0, 0.5), + color: Color::rgba(1.0, 0.5, 0.5, 0.5), form: TexelForm::Gas, gravity: Some(TexelGravity::Down(10)), ..default() }, ); + result.insert( + 8, + TexelBehaviour2D { + name: Cow::Borrowed("oxygen"), + color: Color::rgba(0.5, 0.5, 0.5, 0.5), + form: TexelForm::Gas, + ..default() + }, + ); + result.insert( 11, TexelBehaviour2D { @@ -120,7 +130,7 @@ lazy_static! { }; } -#[derive(Clone, Copy, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum TexelForm { #[default] // Solid materials, when affected by gravity, create pyramid-like piles @@ -131,7 +141,7 @@ pub enum TexelForm { Gas, } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum TexelGravity { Down(u8), Up(u8), @@ -146,7 +156,7 @@ impl From for Vector2I { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct TexelBehaviour2D { pub name: Cow<'static, str>, pub color: Color,