summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-15 22:46:53 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-15 22:46:53 -0700
commitd7eccba2055cd7784a0db6d9ee8692a4f3510931 (patch)
tree37f4c19781c788fa2af1ee4eeb7195f49a6d2d7a
parent05135beb87bbf0edace544b80ba40e327d9a89ac (diff)
fire, optimize viewport rendering
-rw-r--r--src/camera.rs8
-rw-r--r--src/main.rs6
-rw-r--r--src/renderer/mod.rs14
-rw-r--r--src/renderer/ui.rs35
-rw-r--r--src/sim/cell.rs21
-rw-r--r--src/sim/materials/fire.rs53
-rw-r--r--src/sim/materials/mod.rs14
-rw-r--r--src/sim/sim.rs65
-rw-r--r--src/sim/world.rs5
9 files changed, 169 insertions, 52 deletions
diff --git a/src/camera.rs b/src/camera.rs
index 0b69833..d14adbd 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -20,6 +20,14 @@ impl Camera {
(1.0 / half_w, -1.0 / half_h)
}
+ // xl, xu, yl, yu
+ pub fn viewport_bounds_world(&self) -> (f32, f32, f32, f32) {
+ let (xl, yl) = self.screen_position_to_world(0.0, 0.0);
+ let (xu, yu) =
+ self.screen_position_to_world(self.screen_size.0 as f32, self.screen_size.1 as f32);
+ (xl, xu, yl, yu)
+ }
+
pub fn handle_camera_input(&mut self, input: &Input, delta_time: f32) {
// wasd movement
let x: f32 = if input.is_left_pressed {
diff --git a/src/main.rs b/src/main.rs
index 92fe01e..7cbf9d0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -252,10 +252,10 @@ impl ApplicationHandler for App {
&& r > 0.9
{
let mut cell = Cell::from_material(self.config.brush_material);
+ // ensure we simulate on the first tick
cell.flags = (self.sim_seqno as u8) & 0b1;
world.set_cell_from_game_position(
- x, y, cell, // wake the chunk
- false,
+ x, y, cell, false, // wake the chunk
);
}
}
@@ -266,13 +266,13 @@ impl ApplicationHandler for App {
let secs_since_last_tick = (now - self.last_sim_tick).as_secs_f32();
let expected_secs_since_last_tick = 1.0 / SIM_FPS as f32;
+ self.last_sim_tick = now;
if self.sim_paused && self.ignore_pause_next_tick {
sim_tick(world, self.sim_seqno, self.config.use_threading);
self.sim_seqno += 1;
self.ignore_pause_next_tick = false;
} else if !self.sim_paused {
self.sim_ticks_due += secs_since_last_tick / expected_secs_since_last_tick;
- self.last_sim_tick = now;
let mut ticks_done = 0;
// don't ever tick more than 3 times per frame, or else we can get a pseudo deadlock
while self.sim_ticks_due >= 1.0 && ticks_done < 3 {
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index fcc5d36..4499688 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -212,8 +212,8 @@ impl RendererState {
let mut renderer_chunks: Vec<RendererChunk> = Vec::new();
// match number of world chunks
// TODO refactor so that this implicit
- for _ in -10..10 {
- for _ in -10..10 {
+ for _ in -10..1 {
+ for _ in -100..100 {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: None,
mip_level_count: 1,
@@ -336,7 +336,7 @@ impl RendererState {
.resizable(false)
// TODO collapse button
.show_collapsible(ui, &mut true, |panel_ui| {
- draw_egui(panel_ui, config, camera, diagnostics, input)
+ draw_egui(panel_ui, config, camera, diagnostics, input, world)
});
});
@@ -449,9 +449,13 @@ impl RendererState {
render_pass.set_bind_group(0, &self.camera_uniform_bind_group, &[]);
+ let (xl, xu, yl, yu) = camera.viewport_bounds_world();
+ let ((cxl, cyl), _) = World::split_game_position(xl.floor() as i32, yl.floor() as i32);
+ let ((cxu, cyu), _) = World::split_game_position(xu.floor() as i32, yu.floor() as i32);
+
// TODO only visible chunks
- for cx in -10..10 {
- for cy in -10..10 {
+ for cx in cxl..=cxu {
+ for cy in cyl..=cyu {
if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) {
let render_chunk = &self.renderer_chunks[*idx];
render_pass.set_bind_group(1, &render_chunk.bind_group, &[]);
diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs
index 87ab878..b4e6f74 100644
--- a/src/renderer/ui.rs
+++ b/src/renderer/ui.rs
@@ -1,6 +1,9 @@
use egui::{Color32, Stroke, Ui, epaint::CircleShape};
-use crate::{Camera, Config, Diagnostics, Input, sim::materials::MaterialId};
+use crate::{
+ Camera, Config, Diagnostics, Input,
+ sim::{materials::MaterialId, world::World},
+};
pub fn draw_egui<'a>(
ui: &mut Ui,
@@ -8,6 +11,7 @@ pub fn draw_egui<'a>(
camera: &mut Camera,
diagnostics: &Diagnostics,
input: &Input,
+ world: &World,
) {
puffin::profile_function!();
ui.heading("Config");
@@ -24,7 +28,7 @@ pub fn draw_egui<'a>(
.icon(move |ui, rect, _, _| {
ui.painter().add(egui::Shape::Circle(CircleShape {
center: rect.center(),
- radius: rect.width() / 2.5,
+ radius: rect.height() / 2.0,
stroke: Stroke::NONE,
fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2),
}));
@@ -54,11 +58,24 @@ pub fn draw_egui<'a>(
))
});
- input
- .last_mouse_pos_on_board
- .map(|p| ui.label(format!("Mouse (board): x,y=({x}, {y})", x = p.0, y = p.1,)));
-
- input
- .last_mouse_pos_on_board
- .map(|p| ui.label(format!("Mouse (chunk): x,y=({x}, {y})", x = p.0, y = p.1,)));
+ if let Some((x, y)) = input.last_mouse_pos_on_board {
+ ui.heading("Entity");
+ ui.label(format!("x,y=({x}, {y})"));
+ if let Some(cell) = world.get_cell_from_game_position(x, y) {
+ let material = cell.material.def();
+ let cell_label = ui.label(
+ egui::RichText::new(format!("Cell: {}", material.name)).color(Color32::LIGHT_BLUE),
+ );
+ ui.painter().add(egui::Shape::Circle(CircleShape {
+ center: cell_label.rect.right_center() + egui::vec2(10.0, 0.0),
+ radius: cell_label.rect.height() / 2.5,
+ stroke: Stroke::NONE,
+ fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2),
+ }));
+ ui.label(
+ egui::RichText::new(format!("Flags: {:b}", cell.flags)).color(Color32::YELLOW),
+ );
+ ui.label(egui::RichText::new(format!("Data: {:b}", cell.data)).color(Color32::RED));
+ }
+ }
}
diff --git a/src/sim/cell.rs b/src/sim/cell.rs
index 2e26ddc..d3ca302 100644
--- a/src/sim/cell.rs
+++ b/src/sim/cell.rs
@@ -4,6 +4,20 @@ use crate::sim::materials::MaterialId;
pub struct Cell {
pub material: MaterialId,
pub flags: u8,
+ pub data: u16,
+}
+
+impl Cell {
+ const FLAG_PARITY: u8 = 0b0000_0001;
+
+ #[inline]
+ pub fn parity(self) -> u8 {
+ self.flags & Self::FLAG_PARITY
+ }
+ #[inline]
+ pub fn flip_parity(&mut self) {
+ self.flags ^= Self::FLAG_PARITY;
+ }
}
impl Cell {
@@ -11,9 +25,14 @@ impl Cell {
Cell {
material: MaterialId::Void,
flags: 0,
+ data: 0,
}
}
pub fn from_material(material: MaterialId) -> Cell {
- Cell { material, flags: 0 }
+ Cell {
+ material,
+ flags: 0,
+ data: 0,
+ }
}
}
diff --git a/src/sim/materials/fire.rs b/src/sim/materials/fire.rs
new file mode 100644
index 0000000..b155986
--- /dev/null
+++ b/src/sim/materials/fire.rs
@@ -0,0 +1,53 @@
+use rand::RngExt;
+
+use crate::sim::{cell::Cell, materials::MaterialId, sim::UpdateCtx};
+
+trait FireCellView {
+ fn get_ticks_lived(self) -> u16;
+ fn set_ticks_lived(&mut self, ticks: u16) -> ();
+ fn is_flammable(self) -> bool;
+}
+
+impl FireCellView for Cell {
+ fn get_ticks_lived(self) -> u16 {
+ self.data
+ }
+ fn set_ticks_lived(&mut self, ticks: u16) -> () {
+ self.data = ticks;
+ }
+ // this could be a property of the material def, I think it's better here for now
+ fn is_flammable(self) -> bool {
+ [MaterialId::Wood].contains(&self.material)
+ }
+}
+
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ let ticks_lived = ctx.cell.get_ticks_lived();
+ // 2 seconds
+ if ticks_lived > 240 {
+ // kill ourselves, with a chance to turn into ash
+ if ctx.rng.random_range(0.0..1.0) > 0.8 {
+ // TODO ash material
+ ctx.set_cell(0, 0, Cell::from_material(MaterialId::Sand));
+ } else {
+ ctx.set_cell(0, 0, Cell::void());
+ }
+ return;
+ }
+
+ // 4 times in our lifespan, try to spread in a random direction
+ // since it's random, fire may die out if there's nothing in that direction
+ // a 1-thick line of flammable cells has a 31% chance to die each iteration
+ if ticks_lived % 60 == 0 {
+ let (dx, dy) = (ctx.rng.random_range(-1..=1), ctx.rng.random_range(-1..=1));
+ if let Some(target) = ctx.get_cell(dx, dy)
+ && target.is_flammable()
+ {
+ ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Fire));
+ }
+ }
+
+ ctx.cell.set_ticks_lived(ticks_lived + 1);
+ ctx.set_cell(0, 0, *ctx.cell);
+}
diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs
index d2d0e0e..764318a 100644
--- a/src/sim/materials/mod.rs
+++ b/src/sim/materials/mod.rs
@@ -1,5 +1,6 @@
use crate::sim::sim::UpdateCtx;
+mod fire;
mod gas;
mod sand;
mod water;
@@ -12,6 +13,7 @@ pub enum MaterialId {
Wood,
Water,
Gas,
+ Fire,
}
pub struct MaterialDef {
@@ -21,7 +23,7 @@ pub struct MaterialDef {
pub sim_update: Option<fn(ctx: &mut UpdateCtx) -> ()>,
}
-static MATERIALS: [MaterialDef; 5] = [
+static MATERIALS: [MaterialDef; 6] = [
MaterialDef {
name: "Void",
color: (0x00, 0x00, 0x00, 0x00),
@@ -52,15 +54,23 @@ static MATERIALS: [MaterialDef; 5] = [
density: 10,
sim_update: Some(gas::sim_update),
},
+ MaterialDef {
+ name: "Fire",
+ color: (0xFC, 0x66, 0x00, 0xAA),
+ // for now this matches wood
+ density: 50,
+ sim_update: Some(fire::sim_update),
+ },
];
impl MaterialId {
- pub const ALL: [MaterialId; 5] = [
+ pub const ALL: [MaterialId; 6] = [
MaterialId::Void,
MaterialId::Sand,
MaterialId::Wood,
MaterialId::Water,
MaterialId::Gas,
+ MaterialId::Fire,
];
#[inline]
pub fn def(self) -> &'static MaterialDef {
diff --git a/src/sim/sim.rs b/src/sim/sim.rs
index 4c6d908..84c3126 100644
--- a/src/sim/sim.rs
+++ b/src/sim/sim.rs
@@ -1,6 +1,7 @@
use std::marker::PhantomData;
use fxhash::FxHashMap;
+use rand::{Rng, SeedableRng, rngs::SmallRng};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use crate::{
@@ -30,17 +31,6 @@ impl<'a> ChunkAccess<'a> {
unsafe impl Sync for ChunkAccess<'_> {}
-pub struct UpdateCtx<'a, 'b, 'c> {
- pub chunks: &'a mut [Option<&'b mut Chunk>; 9],
- pub seqno: u64,
- pub seqno_parity: u8,
-
- pub x: i32,
- pub y: i32,
- pub cell: &'c mut Cell,
- pub material: &'c MaterialDef,
-}
-
fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option<Cell> {
let dcx = x.div_euclid(CHUNK_SIZE);
let dcy = y.div_euclid(CHUNK_SIZE);
@@ -74,17 +64,28 @@ pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell
if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] {
chunk.set_cell_at_local_position(nc_x, nc_y, cell);
chunk.needs_texture_update = true;
- chunk.sleeping = false;
}
} else {
if let Some(target) = &mut chunks[4] {
target.set_cell_at_local_position(x as u8, y as u8, cell);
target.needs_texture_update = true;
- target.sleeping = false;
}
}
}
+pub struct UpdateCtx<'a, 'b, 'c> {
+ pub chunks: &'a mut [Option<&'b mut Chunk>; 9],
+ pub seqno: u64,
+ pub seqno_parity: u8,
+
+ pub x: i32,
+ pub y: i32,
+ pub cell: &'c mut Cell,
+ pub material: &'c MaterialDef,
+
+ pub rng: &'c mut dyn Rng,
+}
+
impl UpdateCtx<'_, '_, '_> {
pub fn get_cell(&self, dx: i32, dy: i32) -> Option<Cell> {
let x = self.x + dx;
@@ -92,7 +93,7 @@ impl UpdateCtx<'_, '_, '_> {
get_cell(self.chunks, x, y)
}
- fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) {
+ pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) {
// cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood
// as this would wake a chunk outside of the neighbourhood
debug_assert!(dx > -15 && dx < 15);
@@ -122,8 +123,8 @@ impl UpdateCtx<'_, '_, '_> {
pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) {
puffin::profile_function!();
- // scan bottom to top to enable contiguous falling
let seqno_parity = (seqno as u8) & 0b1;
+ let mut rng = SmallRng::seed_from_u64(seqno);
if chunks[4].is_some() {
for y in (0..CHUNK_SIZE).rev() {
@@ -135,24 +136,28 @@ pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) {
};
let mut cell = get_cell(chunks, x, y).unwrap();
- if cell.flags & 0b1 == seqno_parity {
- // flip the parity bit
- // TODO if the cell doesn't move this doesn't stay
- cell.flags = cell.flags ^ 0b1;
- let material = cell.material.def();
+ let material = cell.material.def();
+
+ if let Some(update) = material.sim_update {
+ if cell.parity() == seqno_parity {
+ cell.flip_parity();
+ // apply the flipped parity in case the sim target doesn't
+ set_cell(chunks, x, y, cell);
+
+ let mut update_ctx = UpdateCtx {
+ chunks,
+ seqno,
+ seqno_parity,
- let mut update_ctx = UpdateCtx {
- chunks,
- seqno,
- seqno_parity,
+ x,
+ y,
+ cell: &mut cell,
+ material,
- x,
- y,
- cell: &mut cell,
- material,
- };
+ // TODO this is platform-dependent, will break for multiplayer
+ rng: &mut rng,
+ };
- if let Some(update) = material.sim_update {
update(&mut update_ctx);
}
}
diff --git a/src/sim/world.rs b/src/sim/world.rs
index 6645d4e..5993b10 100644
--- a/src/sim/world.rs
+++ b/src/sim/world.rs
@@ -48,6 +48,7 @@ impl World {
self.chunks[idx].set_cell_at_local_position(dx, dy, cell);
// this is a temporary hack
self.chunks[idx].sleeping = sleeping;
+ self.chunks[idx].needs_texture_update = true;
}
}
@@ -63,8 +64,8 @@ impl World {
chunk_position_to_chunk_idx: FxHashMap::default(),
};
- for y in -10..10 {
- for x in -10..10 {
+ for y in -10..1 {
+ for x in -100..100 {
world.insert(x, y, Chunk::void());
}
}