feat: first iteration of gas dispersion
parent
c3ea9f4513
commit
76deff3f2f
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
188
src/terrain2d.rs
188
src/terrain2d.rs
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue