wip: tweaking disperse system (random disperse, stabilisation)

feat/gas-dispersion
hheik 2022-12-31 22:47:38 +02:00
parent 76deff3f2f
commit 183475c60f
10 changed files with 154 additions and 75 deletions

1
Cargo.lock generated
View File

@ -2084,6 +2084,7 @@ dependencies = [
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy_prototype_debug_lines", "bevy_prototype_debug_lines",
"bevy_rapier2d", "bevy_rapier2d",
"fastrand",
"lazy_static", "lazy_static",
"noise", "noise",
] ]

View File

@ -10,6 +10,7 @@ bevy = { version = "0.9.0", features = ["dynamic"] }
bevy-inspector-egui = "0.14.0" bevy-inspector-egui = "0.14.0"
bevy_prototype_debug_lines = "0.9.0" bevy_prototype_debug_lines = "0.9.0"
bevy_rapier2d = "0.19.0" bevy_rapier2d = "0.19.0"
fastrand = "1.8.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
noise = "0.8.2" noise = "0.8.2"

View File

@ -7,10 +7,7 @@ use crate::{
}; };
use self::{ use self::{
camera::{GameCameraPlugin, WORLD_WIDTH}, camera::GameCameraPlugin, debug::DebugPlugin, kinematic::KinematicPlugin, player::PlayerPlugin,
debug::DebugPlugin,
kinematic::KinematicPlugin,
player::PlayerPlugin,
}; };
pub mod camera; pub mod camera;
@ -42,8 +39,8 @@ fn setup_window(mut windows: ResMut<Windows>) {
fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) { fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
let terrain_gen = TerrainGen2D::new(432678); let terrain_gen = TerrainGen2D::new(432678);
for y in 0..(WORLD_WIDTH / Chunk2D::SIZE_Y as i32) { for y in 0..(Terrain2D::WORLD_HEIGHT / Chunk2D::SIZE_Y as i32) {
for x in 0..(WORLD_WIDTH / Chunk2D::SIZE_X as i32) { for x in 0..(Terrain2D::WORLD_WIDTH / Chunk2D::SIZE_X as i32) {
let position = Vector2I { x, y }; let position = Vector2I { x, y };
terrain.add_chunk(position, terrain_gen.gen_chunk(&position)); terrain.add_chunk(position, terrain_gen.gen_chunk(&position));
} }
@ -60,6 +57,6 @@ fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
.spawn(Name::new("Right wall")) .spawn(Name::new("Right wall"))
.insert(Collider::halfspace(Vec2::NEG_X).unwrap()) .insert(Collider::halfspace(Vec2::NEG_X).unwrap())
.insert(TransformBundle::from_transform( .insert(TransformBundle::from_transform(
Transform::from_translation(Vec3::new(WORLD_WIDTH as f32, 0.0, 0.0)), Transform::from_translation(Vec3::new(Terrain2D::WORLD_WIDTH as f32, 0.0, 0.0)),
)); ));
} }

View File

@ -4,9 +4,10 @@ use bevy::{
}; };
use bevy_inspector_egui::{Inspectable, RegisterInspectable}; use bevy_inspector_egui::{Inspectable, RegisterInspectable};
use crate::util::{move_towards_vec3, vec3_lerp}; use crate::{
terrain2d::Terrain2D,
pub const WORLD_WIDTH: i32 = 512; util::{move_towards_vec3, vec3_lerp},
};
pub struct GameCameraPlugin; pub struct GameCameraPlugin;
@ -48,7 +49,7 @@ fn camera_setup(mut commands: Commands) {
Name::new("Camera"), Name::new("Camera"),
Camera2dBundle { Camera2dBundle {
projection: OrthographicProjection { projection: OrthographicProjection {
scaling_mode: ScalingMode::FixedHorizontal(WORLD_WIDTH as f32), scaling_mode: ScalingMode::FixedHorizontal(Terrain2D::WORLD_WIDTH as f32),
window_origin: WindowOrigin::Center, window_origin: WindowOrigin::Center,
scale: 1.0 / 2.0, scale: 1.0 / 2.0,
..default() ..default()
@ -79,7 +80,7 @@ fn camera_system(
for (mut camera_transform, projection) in camera_query.iter_mut() { for (mut camera_transform, projection) in camera_query.iter_mut() {
let left_limit = 0.0; let left_limit = 0.0;
let right_limit = WORLD_WIDTH as f32; let right_limit = Terrain2D::WORLD_WIDTH as f32;
let offset = Vec3::new(0.0, 0.0, 999.9); let offset = Vec3::new(0.0, 0.0, 999.9);
match follow.movement { match follow.movement {
FollowMovement::Instant => { FollowMovement::Instant => {

View File

@ -1,7 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_prototype_debug_lines::DebugLinesPlugin; use bevy_prototype_debug_lines::DebugLinesPlugin;
mod terrain; pub mod terrain;
use terrain::TerrainDebugPlugin; use terrain::TerrainDebugPlugin;

View File

@ -23,7 +23,10 @@ struct TerrainBrush2D {
impl Default for TerrainBrush2D { impl Default for TerrainBrush2D {
fn default() -> Self { fn default() -> Self {
TerrainBrush2D { radius: 3, tile: 7 } TerrainBrush2D {
radius: 40,
tile: 8,
}
} }
} }
@ -204,7 +207,7 @@ fn chunk_debugger(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
} }
} }
fn draw_box(debug_draw: &mut DebugLines, min: Vec3, max: Vec3, color: Color, duration: f32) { pub fn draw_box(debug_draw: &mut DebugLines, min: Vec3, max: Vec3, color: Color, duration: f32) {
let points = vec![ let points = vec![
Vec3::new(min.x, min.y, min.z), Vec3::new(min.x, min.y, min.z),
Vec3::new(max.x, min.y, min.z), Vec3::new(max.x, min.y, min.z),

View File

@ -81,9 +81,6 @@ pub fn player_spawn(mut commands: Commands) {
}, },
..default() ..default()
}) })
.insert(TransformBundle::from_transform(Transform::from_xyz(
256.0, 128.0, 0.0,
)))
.insert(Collider::cuboid(3.0, 6.0)) .insert(Collider::cuboid(3.0, 6.0))
.insert(PlayerBundle { .insert(PlayerBundle {
kinematic, kinematic,

View File

@ -17,10 +17,7 @@ pub use terrain_gen2d::*;
pub use texel2d::*; pub use texel2d::*;
pub use texel_behaviour2d::*; pub use texel_behaviour2d::*;
use crate::{ use crate::util::{frame_counter::FrameCounter, math::*, Vector2I};
game::camera::WORLD_WIDTH,
util::{frame_counter::FrameCounter, math::*, Vector2I},
};
pub struct Terrain2DPlugin; pub struct Terrain2DPlugin;
@ -46,10 +43,10 @@ impl Plugin for Terrain2DPlugin {
app.register_type::<TerrainChunk2D>() app.register_type::<TerrainChunk2D>()
.insert_resource(Terrain2D::new( .insert_resource(Terrain2D::new(
Some(WORLD_WIDTH * 2), Some(Terrain2D::WORLD_HEIGHT),
Some(0), Some(0),
Some(0), Some(0),
Some(WORLD_WIDTH), Some(Terrain2D::WORLD_WIDTH),
)) ))
.add_event::<TerrainEvent2D>() .add_event::<TerrainEvent2D>()
.add_system_to_stage(TerrainStages::Simulation, terrain_simulation) .add_system_to_stage(TerrainStages::Simulation, terrain_simulation)
@ -74,7 +71,11 @@ pub enum TerrainStages {
ChunkSync, ChunkSync,
} }
fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCounter>) { fn terrain_simulation(
mut terrain: ResMut<Terrain2D>,
frame_counter: Res<FrameCounter>,
mut debug_draw: ResMut<bevy_prototype_debug_lines::DebugLines>,
) {
let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1; let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
let indices = terrain let indices = terrain
@ -152,21 +153,29 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
} }
// Distribute gas // Distribute gas
disperse_gas(global_positions, &mut terrain, &frame_counter) disperse_gas(
global_positions,
&mut terrain,
&frame_counter,
&mut debug_draw,
)
} }
} }
} }
} }
} }
// TODO: Don't update if the result of dispersion is similar to before
fn disperse_gas( fn disperse_gas(
global_positions: Vec<Vector2I>, global_positions: Vec<Vector2I>,
terrain: &mut Terrain2D, terrain: &mut Terrain2D,
frame_counter: &FrameCounter, frame_counter: &FrameCounter,
debug_draw: &mut bevy_prototype_debug_lines::DebugLines,
) { ) {
use u32 as Capacity; use u32 as Capacity;
let mut total_densities: HashMap<TexelID, Capacity> = HashMap::new(); use u8 as Min;
// let mut total_densities: Vec<(TexelID, Capacity)> = vec![]; use u8 as Max;
let mut total_densities: HashMap<TexelID, (Capacity, Min, Max)> = HashMap::new();
let mut valid_globals = vec![]; let mut valid_globals = vec![];
for global in global_positions.iter() { for global in global_positions.iter() {
let (texel, behaviour) = terrain.get_texel_behaviour(global); let (texel, behaviour) = terrain.get_texel_behaviour(global);
@ -176,37 +185,75 @@ fn disperse_gas(
match (texel, behaviour) { match (texel, behaviour) {
(Some(texel), Some(behaviour)) => { (Some(texel), Some(behaviour)) => {
if behaviour.form == TexelForm::Gas { if behaviour.form == TexelForm::Gas {
if let Some((old_density, old_min, old_max)) = total_densities.get(&texel.id) {
total_densities.insert( total_densities.insert(
texel.id, texel.id,
texel.density as u32 (
+ total_densities.get(&texel.id).map_or(0, |density| *density), texel.density as u32 + *old_density,
texel.density.min(*old_min),
texel.density.max(*old_max),
),
); );
} else {
total_densities.insert(
texel.id,
(texel.density as u32, texel.density, texel.density),
);
}
} }
} }
(_, _) => (), (_, _) => (),
} }
} }
let mut total_densities: Vec<(TexelID, Capacity)> = let mut total_densities: Vec<(TexelID, Capacity, Min, Max)> = total_densities
total_densities.iter().map(|(t, d)| (*t, *d)).collect(); .iter()
.map(|(t, (d, min, max))| (*t, *d, *min, *max))
.collect();
if total_densities.len() == 0 { if total_densities.len() == 0 {
return; return;
} }
total_densities.sort_unstable_by_key(|(_, density)| *density); total_densities.sort_unstable_by_key(|(_, density, _, _)| *density);
total_densities.reverse(); total_densities.reverse();
const TILE_CAPACITY: u32 = u8::MAX as u32; const TILE_CAPACITY: u32 = u8::MAX as u32;
let free_slots = valid_globals.len() as u32 let free_slots = valid_globals.len() as u32
- total_densities - total_densities
.iter() .iter()
.map(|(_, v)| (*v / (TILE_CAPACITY + 1)) + 1) .map(|(_, v, _, _)| (*v / (TILE_CAPACITY + 1)) + 1)
.sum::<u32>(); .sum::<u32>();
// Stop if the gas is already close to a stable state
const STABLE_TRESHOLD: u8 = 10;
if total_densities.iter().all(|(_, _, min, max)| {
if u8::abs_diff(*min, *max) > STABLE_TRESHOLD {
return false;
}
free_slots > 0 && *max <= STABLE_TRESHOLD
}) {
// // DEBUG: draw box for stabilized area
// let mut min = valid_globals.first().unwrap().clone();
// let mut max = valid_globals.first().unwrap().clone();
// for global in valid_globals.iter() {
// min = Vector2I::min(&min, global);
// max = Vector2I::max(&max, global);
// }
// max = max + Vector2I::ONE;
// crate::game::debug::terrain::draw_box(
// debug_draw,
// Vec3::from(min),
// Vec3::from(max),
// Color::CYAN,
// 0.0,
// );
return;
}
// Allocate slots // Allocate slots
let mut slots: Vec<(TexelID, u32)> = vec![]; let mut slots: Vec<(TexelID, u32)> = vec![];
for (id, density) in total_densities.iter() { for (id, density, _, _) in total_densities.iter() {
let min_slots = (density / (TILE_CAPACITY + 1)) + 1; let min_slots = (density / (TILE_CAPACITY + 1)) + 1;
slots.push((*id, min_slots)); slots.push((*id, min_slots));
} }
@ -217,7 +264,7 @@ fn disperse_gas(
// Disperse into given slots // Disperse into given slots
let mut texels: Vec<Texel2D> = vec![]; let mut texels: Vec<Texel2D> = vec![];
for (id, total_density) in total_densities.iter() { for (id, total_density, _, _) in total_densities.iter() {
let slots = slots.iter().find(|s| s.0 == *id).unwrap().1; let slots = slots.iter().find(|s| s.0 == *id).unwrap().1;
let mut density_left = *total_density; let mut density_left = *total_density;
for i in 0..slots { for i in 0..slots {
@ -242,6 +289,7 @@ fn disperse_gas(
panic!("disperse_gas() - valid_globals is shorter than texels"); panic!("disperse_gas() - valid_globals is shorter than texels");
} }
fastrand::shuffle(&mut valid_globals);
for i in 0..valid_globals.len() { for i in 0..valid_globals.len() {
let global = valid_globals[i]; let global = valid_globals[i];
if i < texels.len() { if i < texels.len() {
@ -266,6 +314,7 @@ fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &Fra
let grav_offset = Vector2I::from(gravity); let grav_offset = Vector2I::from(gravity);
let grav_pos = global + grav_offset; let grav_pos = global + grav_offset;
if behaviour.form != TexelForm::Gas || gravity.abs() > fastrand::u8(0..u8::MAX) {
// Try falling // Try falling
{ {
let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos); let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
@ -299,6 +348,7 @@ fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &Fra
} }
} }
} }
}
fn emit_terrain_events( fn emit_terrain_events(
mut terrain: ResMut<Terrain2D>, mut terrain: ResMut<Terrain2D>,
@ -331,13 +381,16 @@ pub struct Terrain2D {
} }
impl Terrain2D { impl Terrain2D {
pub const WORLD_WIDTH: i32 = 512;
pub const WORLD_HEIGHT: i32 = Self::WORLD_WIDTH * 2;
pub fn new( pub fn new(
top_boundary: Option<i32>, top_boundary: Option<i32>,
bottom_boundary: Option<i32>, bottom_boundary: Option<i32>,
left_boundary: Option<i32>, left_boundary: Option<i32>,
right_boundary: Option<i32>, right_boundary: Option<i32>,
) -> Terrain2D { ) -> Self {
Terrain2D { Self {
chunk_map: HashMap::new(), chunk_map: HashMap::new(),
events: Vec::new(), events: Vec::new(),
top_boundary, top_boundary,
@ -518,10 +571,7 @@ impl Terrain2D {
) { ) {
let from = self.get_texel(from_global).unwrap_or_default(); let from = self.get_texel(from_global).unwrap_or_default();
let to = self.get_texel(to_global).unwrap_or_default(); let to = self.get_texel(to_global).unwrap_or_default();
let max_transfer = match gravity { let max_transfer = gravity.abs();
TexelGravity::Down(grav) => grav,
TexelGravity::Up(grav) => grav,
};
let transfer = (u8::MAX - to.density).min(max_transfer).min(from.density); let transfer = (u8::MAX - to.density).min(max_transfer).min(from.density);
if from.density - transfer == 0 { if from.density - transfer == 0 {
self.set_texel(&from_global, Texel2D::default(), simulation_frame); self.set_texel(&from_global, Texel2D::default(), simulation_frame);

View File

@ -1,6 +1,7 @@
use noise::{NoiseFn, PerlinSurflet}; use noise::{NoiseFn, PerlinSurflet};
use super::*; use super::*;
use crate::util::{inverse_lerp, lerp};
pub struct TerrainGen2D { pub struct TerrainGen2D {
pub seed: u32, pub seed: u32,

View File

@ -70,7 +70,7 @@ lazy_static! {
name: Cow::Borrowed("light gas"), name: Cow::Borrowed("light gas"),
color: Color::rgba(0.0, 1.0, 0.0, 0.5), color: Color::rgba(0.0, 1.0, 0.0, 0.5),
form: TexelForm::Gas, form: TexelForm::Gas,
gravity: Some(TexelGravity::Up(10)), gravity: Some(TexelGravity::Up(160)),
..default() ..default()
}, },
); );
@ -81,7 +81,7 @@ lazy_static! {
name: Cow::Borrowed("heavy gas"), name: Cow::Borrowed("heavy gas"),
color: Color::rgba(1.0, 0.5, 0.5, 0.5), color: Color::rgba(1.0, 0.5, 0.5, 0.5),
form: TexelForm::Gas, form: TexelForm::Gas,
gravity: Some(TexelGravity::Down(10)), gravity: Some(TexelGravity::Down(60)),
..default() ..default()
}, },
); );
@ -128,9 +128,16 @@ lazy_static! {
result result
}; };
static ref FORM_DISPLACEMENT_PRIORITY: HashMap<TexelForm, u8> = {
let mut result = HashMap::new();
result.insert(TexelForm::Gas, 0);
result.insert(TexelForm::Liquid, 1);
result.insert(TexelForm::Solid, 2);
result
};
} }
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TexelForm { pub enum TexelForm {
#[default] #[default]
// Solid materials, when affected by gravity, create pyramid-like piles // Solid materials, when affected by gravity, create pyramid-like piles
@ -141,6 +148,15 @@ pub enum TexelForm {
Gas, Gas,
} }
impl TexelForm {
fn priority(&self) -> u8 {
FORM_DISPLACEMENT_PRIORITY
.get(self)
.cloned()
.unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum TexelGravity { pub enum TexelGravity {
Down(u8), Down(u8),
@ -156,6 +172,15 @@ impl From<TexelGravity> for Vector2I {
} }
} }
impl TexelGravity {
pub fn abs(&self) -> u8 {
match self {
TexelGravity::Down(grav) => *grav,
TexelGravity::Up(grav) => *grav,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TexelBehaviour2D { pub struct TexelBehaviour2D {
pub name: Cow<'static, str>, pub name: Cow<'static, str>,
@ -206,7 +231,10 @@ impl TexelBehaviour2D {
let to = if let Some(to) = to { to } else { return true }; let to = if let Some(to) = to { to } else { return true };
match (from.form, to.form) { match (from.form, to.form) {
(_, to_form) => { (from_form, to_form) => {
if from_form.priority() != to_form.priority() {
return from_form.priority() > to_form.priority();
}
if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) { if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) {
match (from_grav, to_grav) { match (from_grav, to_grav) {
(TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => { (TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => {