feat: property-based simulation

feat/simulation
hheik 2022-12-26 22:49:19 +02:00
parent 67512ff926
commit 6c1b68d1fd
5 changed files with 273 additions and 160 deletions

View File

@ -81,23 +81,20 @@ fn debug_painter(
return;
};
if key_input.just_pressed(KeyCode::Key1) {
brush.tile = 1;
}
if key_input.just_pressed(KeyCode::Key2) {
brush.tile = 2;
}
if key_input.just_pressed(KeyCode::Key3) {
brush.tile = 3;
}
if key_input.just_pressed(KeyCode::Key4) {
brush.tile = 4;
}
if key_input.just_pressed(KeyCode::Key5) {
brush.tile = 5;
}
if key_input.just_pressed(KeyCode::Key6) {
brush.tile = 6;
for (index, key) in vec![
KeyCode::Key1,
KeyCode::Key2,
KeyCode::Key3,
KeyCode::Key4,
KeyCode::Key5,
KeyCode::Key6,
KeyCode::Key7,
KeyCode::Key8,
KeyCode::Key9,
].iter().enumerate() {
if key_input.just_pressed(*key) {
brush.tile = index as u8 + 1;
}
}
let origin = Vector2I::from(world_pos);
@ -116,8 +113,9 @@ fn debug_painter(
for x in origin.x - (radius - 1)..origin.x + radius {
let dx = (x - origin.x).abs();
let dy = (y - origin.y).abs();
let draw = dx * dx + dy * dy <= (radius - 1) * (radius - 1);
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
if draw {
let pos: Vector2I = Vector2I { x, y };
debug_draw.line_colored(
Vec3::from(pos) + Vec3::new(0.45, 0.45, 1.0),
@ -127,12 +125,7 @@ fn debug_painter(
);
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
{
// 6 is special
if id == 6 {
terrain.mark_dirty(&pos)
} else {
terrain.set_texel(&pos, id, None)
}
terrain.set_texel(&pos, id, None)
}
}
}

View File

@ -94,6 +94,8 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
{
if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
chunk.mark_clean();
} else {
continue;
};
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();
@ -106,104 +108,65 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
}
for y in y_range.iter() {
'texel_loop: for x in x_range.iter() {
for x in x_range.iter() {
let local = Vector2I::new(*x, *y);
let global = local_to_global(&local, &chunk_index);
let texel = if let Some(texel) = terrain.get_texel(&global) {
if texel.last_simulation == simulation_frame {
continue 'texel_loop;
}
texel
} else {
continue;
};
let tb = if let Some(tb) = TexelBehaviour2D::from_id(&texel.id) {
tb
} else {
if terrain
.get_texel(&global)
.map_or(true, |t| t.last_simulation == simulation_frame)
{
continue;
};
// TODO: generalise "check for empty space and move" behaviour
match tb.form {
TexelForm::Liquid => {
// Check if there is space below
{
let below_pos = global + Vector2I::DOWN;
if terrain.get_texel(&below_pos).map_or(true, |texel| {
TexelBehaviour2D::is_empty(&texel.id)
|| TexelBehaviour2D::is_gas(&texel.id)
}) {
let below_id =
terrain.get_texel(&below_pos).map_or(0, |texel| texel.id);
terrain.set_texel(&below_pos, texel.id, Some(simulation_frame));
terrain.set_texel(&global, below_id, Some(simulation_frame));
continue;
}
}
// Check if there is space to the side
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
dirs.reverse();
}
for dir in dirs.iter() {
let side_pos = global + *dir;
if terrain.get_texel(&side_pos).map_or(true, |texel| {
TexelBehaviour2D::is_empty(&texel.id)
|| TexelBehaviour2D::is_gas(&texel.id)
}) {
let side_id =
terrain.get_texel(&side_pos).map_or(0, |texel| texel.id);
terrain.set_texel(&side_pos, texel.id, Some(simulation_frame));
terrain.set_texel(&global, side_id, Some(simulation_frame));
continue 'texel_loop;
};
}
}
TexelForm::Gas => {
// Check if there is space above
{
let above_pos = global + Vector2I::UP;
if terrain
.get_texel(&above_pos)
.map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id))
{
let above_id =
terrain.get_texel(&above_pos).map_or(0, |texel| texel.id);
terrain.set_texel(&above_pos, texel.id, Some(simulation_frame));
terrain.set_texel(&global, above_id, Some(simulation_frame));
continue;
}
}
// Check if there is space to the side
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
dirs.reverse();
}
for dir in dirs.iter() {
let side_pos = global + *dir;
if terrain
.get_texel(&side_pos)
.map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id))
{
let side_id =
terrain.get_texel(&side_pos).map_or(0, |texel| texel.id);
terrain.set_texel(&side_pos, texel.id, Some(simulation_frame));
terrain.set_texel(&global, side_id, Some(simulation_frame));
continue 'texel_loop;
};
}
}
_ => (),
}
simulate_texel(global, &mut terrain, &frame_counter);
}
}
}
}
}
fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &FrameCounter) {
let (_, behaviour) = match terrain.get_texel_behaviour(&global) {
(Some(texel), Some(behaviour)) => (texel, behaviour),
(_, _) => return,
};
let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
// Gravity
if let Some(gravity) = behaviour.gravity {
let grav_offset = Vector2I::from(gravity);
let grav_pos = global + grav_offset;
// Try falling
{
let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
terrain.swap_texels(&global, &grav_pos, Some(simulation_frame));
return;
}
}
// Try "sliding"
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
dirs.reverse();
}
for dir in dirs.iter() {
let slide_pos = match behaviour.form {
TexelForm::Solid => grav_pos + *dir,
TexelForm::Liquid | TexelForm::Gas => global + *dir,
};
let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos);
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
terrain.swap_texels(&global, &slide_pos, Some(simulation_frame));
return;
}
}
}
}
fn emit_terrain_events(
mut terrain: ResMut<Terrain2D>,
mut terrain_events: EventWriter<TerrainEvent2D>,
@ -298,6 +261,17 @@ impl Terrain2D {
.map_or(None, |chunk| chunk.get_texel(&global_to_local(global)))
}
pub fn get_texel_behaviour(
&self,
global: &Vector2I,
) -> (Option<Texel2D>, Option<TexelBehaviour2D>) {
let texel = self.get_texel(global);
(
texel,
texel.map_or(None, |t| TexelBehaviour2D::from_id(&t.id)),
)
}
pub fn set_texel(&mut self, global: &Vector2I, id: TexelID, simulation_frame: Option<u8>) {
let index = global_to_chunk_index(global);
let changed = match self.index_to_chunk_mut(&index) {
@ -316,6 +290,18 @@ impl Terrain2D {
self.mark_dirty(&(*global + Vector2I::LEFT));
}
}
pub fn swap_texels(
&mut self,
from_global: &Vector2I,
to_global: &Vector2I,
simulation_frame: Option<u8>,
) {
let from = self.get_texel(from_global).map_or(0, |t| t.id);
let to = self.get_texel(to_global).map_or(0, |t| t.id);
self.set_texel(to_global, from, simulation_frame);
self.set_texel(from_global, to, simulation_frame);
}
}
pub fn local_to_texel_index(position: &Vector2I) -> Option<usize> {

View File

@ -98,7 +98,7 @@ impl ChunkRect {
pub struct Chunk2D {
pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize],
// TODO: handle multiple dirty rects
// TODO: handle multiple dirty rects?
pub dirty_rect: Option<ChunkRect>,
}
@ -212,13 +212,18 @@ impl Chunk2D {
local_to_texel_index(position).map(|i| &mut self.texels[i])
}
pub fn set_texel(&mut self, position: &Vector2I, id: TexelID, simulation_frame: Option<u8>) -> bool {
pub fn set_texel(
&mut self,
position: &Vector2I,
id: TexelID,
simulation_frame: Option<u8>,
) -> bool {
let i = local_to_texel_index(position).expect("Texel index out of range");
if self.texels[i].id != id {
self.mark_dirty(position);
}
let update_neighbours =
TexelBehaviour2D::is_solid(&self.texels[i].id) != TexelBehaviour2D::is_solid(&id);
let update_neighbours = TexelBehaviour2D::has_collision(&self.texels[i].id)
!= TexelBehaviour2D::has_collision(&id);
let changed = self.texels[i].id != id;
self.texels[i].id = id;
if let Some(simulation_frame) = simulation_frame {
@ -280,7 +285,8 @@ impl Chunk2D {
| if local.x == 0 { 1 << 3 } else { 0 };
let mut sides: Vec<Segment2I>;
if !TexelBehaviour2D::is_solid(&self.texels[i].id) {
let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id);
if !has_collision {
sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize]
.iter()
.clone()
@ -289,7 +295,7 @@ impl Chunk2D {
to: side.to + local,
})
.collect();
} else if TexelBehaviour2D::is_solid(&self.texels[i].id) && edge_mask != 0 {
} else if has_collision && edge_mask != 0 {
sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2);
for i in 0..MST_EDGE_CASE_MAP.len() {
if edge_mask & (1 << i) != 0 {
@ -430,7 +436,7 @@ pub fn chunk_spawner(
..default()
},
texture,
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)),
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 1.0)),
..default()
},
..default()

View File

@ -30,13 +30,13 @@ impl TerrainGen2D {
let mut id = 0;
if value > 0.35 {
id = 1;
id = 11;
}
if value > 0.42 {
id = 2;
id = 12;
}
if value > 0.9 {
id = 3;
id = 13;
}
chunk.set_texel(&local, id, None);

View File

@ -1,3 +1,5 @@
use crate::util::Vector2I;
use super::TexelID;
use bevy::prelude::*;
use lazy_static::lazy_static;
@ -7,32 +9,110 @@ lazy_static! {
static ref ID_MAP: HashMap<TexelID, TexelBehaviour2D> = {
let mut result = HashMap::new();
result.insert(1, TexelBehaviour2D {
color: Color::rgb(0.61, 0.49, 0.38),
..default()
});
result.insert(
1,
TexelBehaviour2D {
name: String::from("loose sand"),
color: Color::rgb(0.61, 0.49, 0.38),
gravity: Some(TexelGravity::Down(100)),
has_collision: true,
..default()
},
);
result.insert(2, TexelBehaviour2D {
color: Color::rgb(0.21, 0.19, 0.17),
..default()
});
result.insert(
2,
TexelBehaviour2D {
name: String::from("loose stone"),
color: Color::rgb(0.21, 0.19, 0.17),
gravity: Some(TexelGravity::Down(100)),
has_collision: true,
..default()
},
);
result.insert(3, TexelBehaviour2D {
color: Color::rgb(0.11, 0.11, 0.11),
..default()
});
result.insert(
3,
TexelBehaviour2D {
name: String::from("loose sturdy stone"),
color: Color::rgb(0.11, 0.11, 0.11),
gravity: Some(TexelGravity::Down(100)),
has_collision: true,
..default()
},
);
result.insert(4, TexelBehaviour2D {
color: Color::rgb(0.0, 0.0, 1.0),
form: TexelForm::Liquid,
..default()
});
result.insert(
4,
TexelBehaviour2D {
name: String::from("water"),
color: Color::rgba(0.0, 0.0, 1.0, 0.5),
form: TexelForm::Liquid,
gravity: Some(TexelGravity::Down(10)),
..default()
},
);
result.insert(5, TexelBehaviour2D {
color: Color::rgb(0.0, 1.0, 0.0),
form: TexelForm::Gas,
..default()
});
result.insert(
5,
TexelBehaviour2D {
name: String::from("oil"),
color: Color::rgba(0.0, 1.0, 0.0, 0.5),
form: TexelForm::Gas,
gravity: Some(TexelGravity::Up(50)),
..default()
},
);
result.insert(
6,
TexelBehaviour2D {
name: String::from("gas"),
color: Color::rgba(0.5, 0.5, 0.25, 0.5),
form: TexelForm::Liquid,
gravity: Some(TexelGravity::Down(5)),
..default()
},
);
result.insert(
11,
TexelBehaviour2D {
name: String::from("sand"),
color: Color::rgb(0.61, 0.49, 0.38),
has_collision: true,
..default()
},
);
result.insert(
12,
TexelBehaviour2D {
name: String::from("stone"),
color: Color::rgb(0.21, 0.19, 0.17),
has_collision: true,
..default()
},
);
result.insert(
13,
TexelBehaviour2D {
name: String::from("sturdy stone"),
color: Color::rgb(0.11, 0.11, 0.11),
has_collision: true,
..default()
},
);
result.insert(
u8::MAX,
TexelBehaviour2D {
color: Color::BLACK,
has_collision: true,
..default()
},
);
result
};
@ -41,38 +121,86 @@ lazy_static! {
#[derive(Clone, Copy, Default, PartialEq)]
pub enum TexelForm {
#[default]
// Solid materials, when affected by gravity, create pyramid-like piles
Solid,
// Liquid materials, when affected by gravity, act like solids but also try to stabilise the surface level by traveling flat surfaces
Liquid,
// Gas materials act like liquids, but also have density/pressure that causes them to disperse
Gas,
}
#[derive(Clone, Copy, Default)]
#[derive(Clone, Copy, PartialEq)]
pub enum TexelGravity {
Down(u8),
Up(u8),
}
impl From<TexelGravity> for Vector2I {
fn from(gravity: TexelGravity) -> Self {
match gravity {
TexelGravity::Down(_) => Vector2I::DOWN,
TexelGravity::Up(_) => Vector2I::UP,
}
}
}
#[derive(Clone)]
pub struct TexelBehaviour2D {
// pub flammability: Option<f32>,
// pub gravity: Option<f32>,
pub form: TexelForm,
pub name: String,
pub color: Color,
pub form: TexelForm,
pub has_collision: bool,
pub gravity: Option<TexelGravity>,
pub toughness: Option<f32>,
}
impl Default for TexelBehaviour2D {
fn default() -> Self {
TexelBehaviour2D {
name: "Unnamed material".to_string(),
color: Color::PINK,
form: TexelForm::Solid,
has_collision: false,
gravity: None,
toughness: None,
}
}
}
// TODO: change form-based functions like is_solid to behaviour based (e.g. has_collision)
impl TexelBehaviour2D {
pub fn from_id(id: &TexelID) -> Option<Self> {
ID_MAP.get(id).copied()
ID_MAP.get(id).cloned()
}
pub fn is_empty(id: &TexelID) -> bool {
ID_MAP.get(id).is_none()
}
pub fn is_solid(id: &TexelID) -> bool {
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Solid)
pub fn has_collision(id: &TexelID) -> bool {
ID_MAP.get(id).map_or(false, |b| b.has_collision)
}
pub fn is_liquid(id: &TexelID) -> bool {
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Liquid)
}
pub fn can_displace(from: &TexelBehaviour2D, to: &Option<TexelBehaviour2D>) -> bool {
let to = if let Some(to) = to { to } else { return true };
pub fn is_gas(id: &TexelID) -> bool {
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Gas)
match (from.form, to.form) {
(_, TexelForm::Solid) => false,
(_, _) => {
if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) {
match (from_grav, to_grav) {
(TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => {
from_grav > to_grav
}
(TexelGravity::Up(from_grav), TexelGravity::Up(to_grav)) => {
from_grav > to_grav
}
(_, _) => true,
}
} else {
true
}
}
}
}
}