Compare commits

...

5 Commits

12 changed files with 515 additions and 208 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;
@ -35,15 +32,15 @@ pub fn init() {
fn setup_window(mut windows: ResMut<Windows>) { fn setup_window(mut windows: ResMut<Windows>) {
if let Some(window) = windows.get_primary_mut() { if let Some(window) = windows.get_primary_mut() {
window.set_resolution(900.0, 450.0); window.set_resolution(1280.0, 720.0);
window.set_title("Kuilu".to_string()); window.set_title("Kuilu".to_string());
} }
} }
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,9 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
// use bevy_inspector_egui::*;
use bevy_prototype_debug_lines::DebugLinesPlugin; use bevy_prototype_debug_lines::DebugLinesPlugin;
// use bevy_rapier2d::prelude::*;
mod terrain; pub mod terrain;
use terrain::TerrainDebugPlugin; use terrain::TerrainDebugPlugin;
@ -12,8 +10,8 @@ pub struct DebugPlugin;
impl Plugin for DebugPlugin { impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugin(DebugLinesPlugin::default()) app.add_plugin(DebugLinesPlugin::default())
// .add_plugin(RapierDebugRenderPlugin::default()) // .add_plugin(bevy_rapier2d::prelude::RapierDebugRenderPlugin::default())
// .add_plugin(WorldInspectorPlugin::new()) // .add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
.add_plugin(TerrainDebugPlugin); .add_plugin(TerrainDebugPlugin);
} }
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I}; use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I};
use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget}; use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget};
use bevy_prototype_debug_lines::DebugLines; use bevy_prototype_debug_lines::DebugLines;
@ -7,7 +9,8 @@ pub struct TerrainDebugPlugin;
impl Plugin for TerrainDebugPlugin { impl Plugin for TerrainDebugPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.insert_resource(TerrainBrush2D::default()) 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); .add_system(debug_painter);
} }
} }
@ -20,7 +23,10 @@ struct TerrainBrush2D {
impl Default for TerrainBrush2D { impl Default for TerrainBrush2D {
fn default() -> Self { fn default() -> Self {
TerrainBrush2D { radius: 5, tile: 4 } TerrainBrush2D {
radius: 40,
tile: 8,
}
} }
} }
@ -128,7 +134,7 @@ fn debug_painter(
); );
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right) 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)
} }
} }
} }
@ -146,22 +152,69 @@ fn dirty_rect_visualizer(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLi
continue; continue;
}; };
let color = Color::RED; let offset = Vec3::from(chunk_index_to_global(chunk_index));
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);
}
}
let points = vec![ fn chunk_debugger(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0), for (chunk_index, chunk) in terrain.chunk_iter() {
Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0), println!("chunk contents: {chunk_index:?}");
Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0), let offset = Vec3::from(chunk_index_to_global(chunk_index));
Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0), let min = offset + Vec3::ZERO;
]; let max = offset + Vec3::from(Chunk2D::SIZE);
for i in 0..points.len() { draw_box(
let offset = Vec3::from(chunk_index_to_global(chunk_index)); &mut debug_draw,
debug_draw.line_colored( min,
offset + points[i], max,
offset + points[(i + 1) % points.len()], Color::rgba(0.5, 0.0, 0.5, 0.5),
0.0, 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}"
); );
} }
} }
} }
pub 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

@ -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,
@ -94,6 +91,6 @@ pub fn player_spawn(mut commands: Commands) {
.insert(Sleeping::disabled()) .insert(Sleeping::disabled())
.insert(CameraFollow { .insert(CameraFollow {
priority: 1, priority: 1,
movement: FollowMovement::Smooth(18.0), movement: FollowMovement::Instant,
}); });
} }

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
@ -104,9 +105,10 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
} else { } else {
continue; continue;
}; };
// Texel simulation
let mut y_range: Vec<_> = (rect.min.y..rect.max.y + 1).collect(); 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(); let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect();
if frame_counter.frame % 2 == 0 { if frame_counter.frame % 2 == 0 {
y_range.reverse(); y_range.reverse();
} }
@ -120,8 +122,8 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
let global = local_to_global(&local, &chunk_index); let global = local_to_global(&local, &chunk_index);
if terrain if terrain
.get_texel(&global) .get_latest_simulation(&global)
.map_or(true, |t| t.last_simulation == simulation_frame) .map_or(true, |frame| frame == simulation_frame)
{ {
continue; continue;
}; };
@ -129,6 +131,172 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
simulate_texel(global, &mut terrain, &frame_counter); 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,
&mut debug_draw,
)
}
}
}
}
}
// TODO: Don't update if the result of dispersion is similar to before
fn disperse_gas(
global_positions: Vec<Vector2I>,
terrain: &mut Terrain2D,
frame_counter: &FrameCounter,
debug_draw: &mut bevy_prototype_debug_lines::DebugLines,
) {
use u32 as Capacity;
use u8 as Min;
use u8 as Max;
let mut total_densities: HashMap<TexelID, (Capacity, Min, Max)> = HashMap::new();
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 {
if let Some((old_density, old_min, old_max)) = total_densities.get(&texel.id) {
total_densities.insert(
texel.id,
(
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, Min, Max)> = total_densities
.iter()
.map(|(t, (d, min, max))| (*t, *d, *min, *max))
.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>();
// Stop if the gas is already close to a stable state
const STABLE_TRESHOLD: u8 = 3;
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
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");
}
fastrand::shuffle(&mut valid_globals);
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)
} }
} }
} }
@ -146,29 +314,37 @@ 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;
// Try falling if behaviour.form != TexelForm::Gas || gravity.abs() > fastrand::u8(0..u8::MAX) {
{ // Try falling
let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos); {
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) { let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
terrain.swap_texels(&global, &grav_pos, Some(simulation_frame)); if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
return; 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" // Try "sliding"
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT]; let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 { if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
dirs.reverse(); dirs.reverse();
} }
for dir in dirs.iter() { for dir in dirs.iter() {
let slide_pos = match behaviour.form { let slide_pos = match behaviour.form {
TexelForm::Solid => grav_pos + *dir, TexelForm::Solid => grav_pos + *dir,
TexelForm::Liquid | TexelForm::Gas => global + *dir, TexelForm::Liquid | TexelForm::Gas => global + *dir,
}; };
let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos); let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos);
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) { if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
terrain.swap_texels(&global, &slide_pos, Some(simulation_frame)); terrain.swap_texels(&global, &slide_pos, Some(simulation_frame));
return; return;
}
if terrain.can_transfer_density(&global, &grav_pos) {
terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame))
}
} }
} }
} }
@ -205,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,
@ -278,7 +457,7 @@ impl Terrain2D {
pub fn is_within_boundaries(&self, global: &Vector2I) -> bool { pub fn is_within_boundaries(&self, global: &Vector2I) -> bool {
if let Some(top) = self.top_boundary { if let Some(top) = self.top_boundary {
if global.y > top { if global.y >= top {
return false; return false;
} }
} }
@ -293,7 +472,7 @@ impl Terrain2D {
} }
} }
if let Some(right) = self.right_boundary { if let Some(right) = self.right_boundary {
if global.x > right { if global.x >= right {
return false; return false;
} }
} }
@ -305,6 +484,12 @@ impl Terrain2D {
.map_or(None, |chunk| chunk.get_texel(&global_to_local(global))) .map_or(None, |chunk| chunk.get_texel(&global_to_local(global)))
} }
pub fn get_latest_simulation(&self, global: &Vector2I) -> Option<u8> {
self.global_to_chunk(global).map_or(None, |chunk| {
chunk.get_latest_simulation(&global_to_local(global))
})
}
pub fn get_texel_behaviour( pub fn get_texel_behaviour(
&self, &self,
global: &Vector2I, global: &Vector2I,
@ -320,16 +505,22 @@ impl Terrain2D {
) )
} }
pub fn set_texel(&mut self, global: &Vector2I, id: TexelID, simulation_frame: Option<u8>) { pub fn set_texel(
&mut self,
global: &Vector2I,
new_texel: Texel2D,
simulation_frame: Option<u8>,
) {
if !self.is_within_boundaries(global) { if !self.is_within_boundaries(global) {
return; return;
} }
let index = global_to_chunk_index(global); let index = global_to_chunk_index(global);
let changed = match self.index_to_chunk_mut(&index) { 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 => { None => {
let mut chunk = Chunk2D::new(); 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); self.add_chunk(index, chunk);
changed changed
} }
@ -348,11 +539,71 @@ impl Terrain2D {
to_global: &Vector2I, to_global: &Vector2I,
simulation_frame: Option<u8>, simulation_frame: Option<u8>,
) { ) {
let from = self.get_texel(from_global).map_or(0, |t| t.id); let from = self.get_texel(from_global).unwrap_or_default();
let to = self.get_texel(to_global).map_or(0, |t| t.id); let to = self.get_texel(to_global).unwrap_or_default();
self.set_texel(to_global, from, simulation_frame); 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); 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 = gravity.abs();
// DEBUG: Test this out, another property?
const MAX_TARGET_DENSITY: u8 = 25;
let transfer = (u8::MAX - to.density)
.min(max_transfer)
.min(from.density)
.min(MAX_TARGET_DENSITY.max(to.density) - to.density);
if transfer == 0 {
return;
}
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> { pub fn local_to_texel_index(position: &Vector2I) -> Option<usize> {

View File

@ -1,18 +1,13 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use super::{ use super::*;
local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D,
TexelBehaviour2D, TexelID, NEIGHBOUR_INDEX_MAP,
};
use crate::util::{CollisionLayers, Segment2I, Vector2I}; use crate::util::{CollisionLayers, Segment2I, Vector2I};
use bevy::{ use bevy::render::{render_resource::Extent3d, texture::ImageSampler};
prelude::*,
render::{render_resource::Extent3d, texture::ImageSampler},
};
use bevy_rapier2d::prelude::*;
use lazy_static::lazy_static; use lazy_static::lazy_static;
type Island = VecDeque<Segment2I>; type Island = VecDeque<Segment2I>;
pub type Chunk2DIndex = Vector2I;
pub type NeighbourMask = u8;
lazy_static! { lazy_static! {
/// Marching Square case dictionary. /// Marching Square case dictionary.
@ -49,6 +44,14 @@ lazy_static! {
/* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO }, /* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO },
/* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP }, /* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP },
]; ];
static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = {
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)] #[derive(Reflect, Component, Default)]
@ -79,8 +82,6 @@ pub struct ChunkColliderBundle {
pub transform: TransformBundle, pub transform: TransformBundle,
} }
pub type Chunk2DIndex = Vector2I;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ChunkRect { pub struct ChunkRect {
pub min: Vector2I, pub min: Vector2I,
@ -97,7 +98,11 @@ impl ChunkRect {
} }
pub struct Chunk2D { 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? // TODO: handle multiple dirty rects?
pub dirty_rect: Option<ChunkRect>, pub dirty_rect: Option<ChunkRect>,
} }
@ -109,65 +114,22 @@ impl Chunk2D {
x: Self::SIZE_X as i32, x: Self::SIZE_X as i32,
y: Self::SIZE_Y 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 { pub fn new() -> Chunk2D {
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, 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<Vector2I> { pub fn xy_vec() -> Vec<Vector2I> {
let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y); let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y);
for y in 0..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]) local_to_texel_index(position).map(|i| self.texels[i])
} }
pub fn get_latest_simulation(&self, position: &Vector2I) -> Option<u8> {
local_to_texel_index(position).map(|i| self.simulation_frames[i])
}
pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> { pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> {
local_to_texel_index(position).map(|i| &mut self.texels[i]) local_to_texel_index(position).map(|i| &mut self.texels[i])
} }
@ -215,46 +181,44 @@ impl Chunk2D {
pub fn set_texel( pub fn set_texel(
&mut self, &mut self,
position: &Vector2I, position: &Vector2I,
id: TexelID, new_texel: Texel2D,
simulation_frame: Option<u8>, simulation_frame: Option<u8>,
) -> bool { ) -> bool {
let i = local_to_texel_index(position).expect("Texel index out of range"); let i = local_to_texel_index(position).expect("Texel index out of range");
if self.texels[i].id != id { if self.texels[i] == new_texel {
self.mark_dirty(position); return false;
} }
let update_neighbours = TexelBehaviour2D::has_collision(&self.texels[i].id) self.mark_dirty(position);
!= TexelBehaviour2D::has_collision(&id); let update_neighbours = self.texels[i].has_collision() != new_texel.has_collision();
let changed = self.texels[i].id != id; self.texels[i] = new_texel;
self.texels[i].id = id; // Update simulation frame
if let Some(simulation_frame) = 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 // Update neighbour mask
if update_neighbours { if update_neighbours {
for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS { for offset in Self::NEIGHBOUR_OFFSET_VECTORS {
// Flip neighbour's bit // Flip neighbour's bit
match self.get_texel_mut(&(*position + offset)) { match local_to_texel_index(&(*position + offset)) {
Some(mut neighbour) => { Some(index) => {
neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset]; self.neighbour_mask[index] ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset];
} }
None => (), None => (),
} }
} }
} }
changed true
} }
pub fn create_texture_data(&self) -> Vec<u8> { pub fn create_texture_data(&self) -> Vec<u8> {
let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4);
for y in (0..Chunk2D::SIZE_Y).rev() { for y in (0..Chunk2D::SIZE_Y).rev() {
for x in 0..Chunk2D::SIZE_X { for x in 0..Chunk2D::SIZE_X {
let id = &self let texel = &self.get_texel(&Vector2I::new(x as i32, y as i32)).unwrap();
.get_texel(&Vector2I::new(x as i32, y as i32)) let behaviour = texel.behaviour();
.unwrap() let mut color =
.id;
let behaviour = TexelBehaviour2D::from_id(id);
let color =
behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.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 color_data = color.as_rgba_u32();
let mut color_data: Vec<u8> = vec![ let mut color_data: Vec<u8> = vec![
((color_data >> 0) & 0xff) as u8, ((color_data >> 0) & 0xff) as u8,
@ -268,6 +232,7 @@ impl Chunk2D {
image_data 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<Vec<Vec2>> { pub fn create_collision_data(&self) -> Vec<Vec<Vec2>> {
let mut islands: Vec<Island> = Vec::new(); let mut islands: Vec<Island> = Vec::new();
for i in 0..self.texels.len() { for i in 0..self.texels.len() {
@ -287,7 +252,7 @@ impl Chunk2D {
let mut sides: Vec<Segment2I>; let mut sides: Vec<Segment2I>;
let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id); let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id);
if !has_collision { 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() .iter()
.clone() .clone()
.map(|side| Segment2I { .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( pub fn chunk_sprite_sync(
mut terrain_events: EventReader<TerrainEvent2D>, mut terrain_events: EventReader<TerrainEvent2D>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
@ -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( pub fn chunk_collision_sync(
mut terrain_events: EventReader<TerrainEvent2D>, mut terrain_events: EventReader<TerrainEvent2D>,
mut commands: Commands, mut commands: Commands,

View File

@ -1,6 +1,7 @@
use noise::{NoiseFn, PerlinSurflet}; use noise::{NoiseFn, PerlinSurflet};
use super::{chunk_index_to_global, Chunk2D, Chunk2DIndex}; use super::*;
use crate::util::{inverse_lerp, lerp};
pub struct TerrainGen2D { pub struct TerrainGen2D {
pub seed: u32, pub seed: u32,
@ -39,7 +40,7 @@ impl TerrainGen2D {
id = 13; id = 13;
} }
chunk.set_texel(&local, id, None); chunk.set_texel(&local, Texel2D { id, ..default() }, None);
} }
chunk chunk
} }

View File

@ -1,35 +1,32 @@
use lazy_static::lazy_static;
use std::collections::HashMap;
pub use u8 as TexelID; pub use u8 as TexelID;
pub use u8 as NeighbourMask;
use crate::util::Vector2I; use super::TexelBehaviour2D;
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Texel2D { pub struct Texel2D {
/// Identifier for a set of properties
pub id: TexelID, pub id: TexelID,
/// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order /// Used by gas materials
pub neighbour_mask: NeighbourMask, pub density: u8,
pub last_simulation: u8,
} }
lazy_static! { impl Default for Texel2D {
pub static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = { fn default() -> Self {
let mut map = HashMap::new(); Self {
for i in 0..Texel2D::NEIGHBOUR_OFFSET_VECTORS.len() { id: TexelID::default(),
map.insert(Texel2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8); density: u8::MAX,
} }
map }
};
} }
impl Texel2D { impl Texel2D {
pub const EMPTY: TexelID = 0; pub const EMPTY: TexelID = 0;
pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [
Vector2I { x: 0, y: 1 }, pub fn has_collision(&self) -> bool {
Vector2I { x: 1, y: 0 }, TexelBehaviour2D::has_collision(&self.id)
Vector2I { x: 0, y: -1 }, }
Vector2I { x: -1, y: 0 },
]; pub fn behaviour(&self) -> Option<TexelBehaviour2D> {
TexelBehaviour2D::from_id(&self.id)
}
} }

View File

@ -14,7 +14,7 @@ lazy_static! {
TexelBehaviour2D { TexelBehaviour2D {
name: Cow::Borrowed("loose sand"), name: Cow::Borrowed("loose sand"),
color: Color::rgb(0.61, 0.49, 0.38), color: Color::rgb(0.61, 0.49, 0.38),
gravity: Some(TexelGravity::Down(100)), gravity: Some(TexelGravity::Down(200)),
has_collision: true, has_collision: true,
..default() ..default()
}, },
@ -25,7 +25,7 @@ lazy_static! {
TexelBehaviour2D { TexelBehaviour2D {
name: Cow::Borrowed("loose stone"), name: Cow::Borrowed("loose stone"),
color: Color::rgb(0.21, 0.19, 0.17), color: Color::rgb(0.21, 0.19, 0.17),
gravity: Some(TexelGravity::Down(100)), gravity: Some(TexelGravity::Down(200)),
has_collision: true, has_collision: true,
..default() ..default()
}, },
@ -36,7 +36,7 @@ lazy_static! {
TexelBehaviour2D { TexelBehaviour2D {
name: Cow::Borrowed("loose sturdy stone"), name: Cow::Borrowed("loose sturdy stone"),
color: Color::rgb(0.11, 0.11, 0.11), color: Color::rgb(0.11, 0.11, 0.11),
gravity: Some(TexelGravity::Down(100)), gravity: Some(TexelGravity::Down(200)),
has_collision: true, has_collision: true,
..default() ..default()
}, },
@ -48,7 +48,7 @@ lazy_static! {
name: Cow::Borrowed("water"), name: Cow::Borrowed("water"),
color: Color::rgba(0.0, 0.0, 1.0, 0.5), color: Color::rgba(0.0, 0.0, 1.0, 0.5),
form: TexelForm::Liquid, form: TexelForm::Liquid,
gravity: Some(TexelGravity::Down(10)), gravity: Some(TexelGravity::Down(50)),
..default() ..default()
}, },
); );
@ -57,9 +57,9 @@ lazy_static! {
5, 5,
TexelBehaviour2D { TexelBehaviour2D {
name: Cow::Borrowed("oil"), name: Cow::Borrowed("oil"),
color: Color::rgba(0.0, 1.0, 0.0, 0.5), color: Color::rgba(0.5, 0.5, 0.25, 0.5),
form: TexelForm::Gas, form: TexelForm::Liquid,
gravity: Some(TexelGravity::Up(50)), gravity: Some(TexelGravity::Down(20)),
..default() ..default()
}, },
); );
@ -67,10 +67,31 @@ lazy_static! {
result.insert( result.insert(
6, 6,
TexelBehaviour2D { TexelBehaviour2D {
name: Cow::Borrowed("gas"), name: Cow::Borrowed("light gas"),
color: Color::rgba(0.5, 0.5, 0.25, 0.5), color: Color::rgba(0.0, 1.0, 0.0, 0.5),
form: TexelForm::Liquid, form: TexelForm::Gas,
gravity: Some(TexelGravity::Down(5)), gravity: Some(TexelGravity::Up(160)),
..default()
},
);
result.insert(
7,
TexelBehaviour2D {
name: Cow::Borrowed("heavy gas"),
color: Color::rgba(1.0, 0.5, 0.5, 0.5),
form: TexelForm::Gas,
gravity: Some(TexelGravity::Down(60)),
..default()
},
);
result.insert(
8,
TexelBehaviour2D {
name: Cow::Borrowed("oxygen"),
color: Color::rgba(0.5, 0.5, 0.5, 0.5),
form: TexelForm::Gas,
..default() ..default()
}, },
); );
@ -107,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, 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
@ -120,7 +148,16 @@ pub enum TexelForm {
Gas, Gas,
} }
#[derive(Clone, Copy, PartialEq)] impl TexelForm {
fn priority(&self) -> u8 {
FORM_DISPLACEMENT_PRIORITY
.get(self)
.cloned()
.unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TexelGravity { pub enum TexelGravity {
Down(u8), Down(u8),
Up(u8), Up(u8),
@ -135,7 +172,16 @@ impl From<TexelGravity> for Vector2I {
} }
} }
#[derive(Clone)] impl TexelGravity {
pub fn abs(&self) -> u8 {
match self {
TexelGravity::Down(grav) => *grav,
TexelGravity::Up(grav) => *grav,
}
}
}
#[derive(Clone, Debug)]
pub struct TexelBehaviour2D { pub struct TexelBehaviour2D {
pub name: Cow<'static, str>, pub name: Cow<'static, str>,
pub color: Color, pub color: Color,
@ -185,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)) => {