feat: first iteration of gas dispersion

master
hheik 2022-12-31 20:14:25 +02:00
parent c3ea9f4513
commit 76deff3f2f
5 changed files with 265 additions and 27 deletions

View File

@ -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<Terrain2D>, mut debug_draw: ResMut<DebugLi
continue;
};
let color = Color::RED;
let points = vec![
Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0),
Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0),
Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0),
Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0),
];
for i in 0..points.len() {
let offset = Vec3::from(chunk_index_to_global(chunk_index));
debug_draw.line_colored(
offset + points[i],
offset + points[(i + 1) % points.len()],
let min = offset + Vec3::from(rect.min);
let max = offset + Vec3::from(rect.max + Vector2I::ONE);
draw_box(&mut debug_draw, min, max, Color::RED, 0.0);
}
}
fn chunk_debugger(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
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,
color,
);
let mut tile_counter: HashMap<TexelID, (u32, u32)> = 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);
}
}

View File

@ -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,
});
}

View File

@ -104,9 +104,10 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
} else {
continue;
};
// Texel simulation
let mut y_range: Vec<_> = (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<Terrain2D>, frame_counter: Res<FrameCo
simulate_texel(global, &mut terrain, &frame_counter);
}
}
// Gas dispersion
let alternate_dispersion = frame_counter.frame % 2 == 0;
let alternate = if alternate_dispersion { 1 } else { 0 };
let y_range =
((rect.min.y - alternate)..rect.max.y + 1 + alternate).collect::<Vec<_>>();
let x_range =
((rect.min.x - alternate)..rect.max.x + 1 + alternate).collect::<Vec<_>>();
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<Vector2I>,
terrain: &mut Terrain2D,
frame_counter: &FrameCounter,
) {
use u32 as Capacity;
let mut total_densities: HashMap<TexelID, Capacity> = 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::<u32>();
// 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<Texel2D> = 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<u8>,
) {
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<u8>,
) {
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<usize> {

View File

@ -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,

View File

@ -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<TexelGravity> for Vector2I {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct TexelBehaviour2D {
pub name: Cow<'static, str>,
pub color: Color,