mod camera; mod config; mod renderer; mod sim; use futures::executor; use glam::{IVec2, Vec2}; use rand::{random_range, random_ratio}; use std::{collections::VecDeque, sync::Arc, time::Instant}; use winit::{ application::ApplicationHandler, event::{ ElementState, KeyEvent, MouseButton, WindowEvent::{self}, }, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, keyboard::{KeyCode, PhysicalKey}, window::Window, }; use crate::{ camera::Camera, config::{PHYSICS_DELTA_TIME, PHYSICS_FPS, SIM_FPS, WINDOW_TITLE}, renderer::RendererState, sim::{ cell::{ cell::Cell, materials::{MaterialId, fire::FireCellView}, }, cell_sim::{sim::sim_tick, world::World}, particle_sim::{ParticleManager, particle::Particle}, rb_sim::{DebugRenderMode, RbSimManager, debug_ops::DebugOperator}, write_rb_entity_to_world, }, }; pub type Error = Box; pub type Result = std::result::Result; struct Config { brush_radius: f32, brush_material: MaterialId, dropper_material: MaterialId, use_threading: bool, cells_render: bool, debug_render: bool, debug_render_mode: DebugRenderMode, } struct Input { last_mouse_pos_on_screen: Option<(f32, f32)>, last_mouse_world_pos: Option<(f32, f32)>, last_mouse_chunk_pos: Option<(i32, i32)>, last_mouse_local_pos: Option<(u8, u8)>, is_lmb_pressed: bool, is_rmb_pressed: bool, trigger_test_1: bool, trigger_test_2: bool, trigger_test_3: bool, trigger_test_4: bool, trigger_test_5: bool, // keybindings is_up_pressed: bool, is_left_pressed: bool, is_down_pressed: bool, is_right_pressed: bool, } struct Diagnostics { frame_times: VecDeque, fps: f32, } struct App { window: Option>, renderer_state: Option, input: Input, camera: Option, // grid world: Option, // physics rb_sim_manager: Option, particle_manager: Option, // sim state // the last/current (not yet completed) seqno sim_seqno: u64, sim_paused: bool, ignore_pause_next_tick: bool, last_sim_update: Instant, sim_updates_due: f32, // physics state last_physics_update: Instant, physics_updates_due: f32, last_render: Instant, config: Config, diagnostics: Diagnostics, } impl App { // called as often as possible // delta time is the real seconds elapsed since the last time this was called fn update(&mut self, delta_time: f32) { // apply inputs if let Some(camera) = &mut self.camera { camera.handle_camera_input(&self.input, delta_time) } // --TEST SPAWNING-- if self.input.trigger_test_1 && let Some(lm) = self.input.last_mouse_world_pos && let Some(rbsm) = &mut self.rb_sim_manager { self.input.trigger_test_1 = false; rbsm.test_spawn_box(lm.0, lm.1, self.config.dropper_material); } if self.input.trigger_test_2 && let Some(lm) = self.input.last_mouse_world_pos && let Some(rbsm) = &mut self.rb_sim_manager { self.input.trigger_test_2 = false; rbsm.test_spawn_ball(lm.0, lm.1, self.config.dropper_material); } if self.input.trigger_test_3 && let Some(lm) = self.input.last_mouse_world_pos && let Some(pm) = &mut self.particle_manager { self.input.trigger_test_3 = false; for i in 0..100 { pm.particles.push(Particle { position: Vec2::new( lm.0 + random_range(-25.0..25.0), lm.1 + random_range(-25.0..25.0), ), velocity: Vec2::ZERO, material: MaterialId::Sand, life: 2.0, }) } } if self.input.trigger_test_4 && let Some(lm) = self.input.last_mouse_world_pos && let Some(world) = &mut self.world && let Some(pm) = &mut self.particle_manager && let Some(rbsm) = &mut self.rb_sim_manager { let mouse = Vec2::new(lm.0, lm.1); self.input.trigger_test_4 = false; let r: i32 = 30; for x in -r..r { for y in -r..r { if x.pow(2) + y.pow(2) < r.pow(2) { // if there's a cell, convert it to a particle let pos = Vec2::new(mouse.x + x as f32, mouse.y + y as f32); let ipos = pos.round().as_ivec2(); if let Some(cell) = world.get_cell_from_game_position(ipos.x, ipos.y) { let mut fire = Cell::from_material(MaterialId::Fire); fire.set_ticks_lived(300); fire.match_parity(self.sim_seqno + 1); if cell.material == MaterialId::Void && random_range(0.0..1.0) > 0.5 { world.set_cell_from_game_position(ipos.x, ipos.y, fire, false); } else { world.set_cell_from_game_position(ipos.x, ipos.y, fire, false); if random_range(0.0..1.0) > 0.6 { let d = pos.distance(mouse); if d < 0.001 { continue; } // skip the center cell let dir = (pos - mouse) / d; // normalize, reusing d let falloff = 1.0 - (d / r as f32); // 1 at center, 0 at edge let vel = (Vec2::new(0.0, -0.6) + dir) * 300.0 * random_range(0.5..1.5) * falloff; pm.particles .push(Particle::new(pos, vel, cell.material, 5.0)); } } } } } } } // --TEST DRAWING-- if (self.input.is_lmb_pressed || self.input.is_rmb_pressed) && let Some(lm) = self.input.last_mouse_world_pos { // start with the bounding box of the drawing brush circle + some margin // clamp the bounding box to the board sie let bb_xl = (lm.0 - self.config.brush_radius).round() as i32; let bb_xu = (lm.0 + self.config.brush_radius).round() as i32; let bb_yl = (lm.1 - self.config.brush_radius).round() as i32; let bb_yu = (lm.1 + self.config.brush_radius).round() as i32; // for each point, check if the distance is less than the brush size and write the pixel for x in bb_xl..bb_xu { for y in bb_yl..bb_yu { let r = random_range(0.0..1.0); if ((x - lm.0.round() as i32).pow(2) + (y - lm.1.round() as i32).pow(2)) < (self.config.brush_radius as i32).pow(2) && r > 0.9 { let cell = if self.input.is_lmb_pressed { let mut cell = Cell::from_material(self.config.brush_material); cell.match_parity(self.sim_seqno); cell } else { Cell::void() }; // ensure we simulate on the first tick if let Some(world) = &mut self.world { world.set_cell_from_game_position( x, y, cell, false, // wake the chunk ) } } } } } } // called SIM_FPS times per second // will be called before the render and before the physics update(s) // may be called multiple times if the sim time is behind fn sim_update(&mut self) { if let Some(world) = &mut self.world && let Some(rbsm) = &mut self.rb_sim_manager { // before we tick, write all the rb entities into the sim world // TODO optimize let entity_ids: Vec = rbsm.rb_entities.keys().copied().collect(); let mut cells_written_by_entity: Vec<(u32, Vec<(u8, u8, i32, i32)>)> = Vec::new(); for entity_id in entity_ids { let cells_written = write_rb_entity_to_world(world, rbsm, entity_id, self.sim_seqno); cells_written_by_entity.push((entity_id, cells_written)); } sim_tick(world, self.sim_seqno, self.config.use_threading); self.sim_seqno += 1; // after we tick, remove the written rb cells and update the entities // TODO optimize for (entity_id, cells_written) in cells_written_by_entity { let rb_entity = rbsm.rb_entities.get_mut(&entity_id).unwrap(); for (lx, ly, x, y) in cells_written { // update the entity // TODO we should skip cells that weren't changed? // TODO optimize let new_local_cell = world.get_cell_from_game_position(x, y).unwrap(); if new_local_cell.material != MaterialId::Void && !new_local_cell.rb() { panic!( "Someone swapped into this rb's cell! ({x}, {y}, {m:#?}, {f})", m = new_local_cell.material, f = new_local_cell.flags ); } rb_entity.set_cell_at_local_position(lx, ly, new_local_cell); // update the world world.set_cell_from_game_position(x, y, Cell::void(), false); } } } } // called PHYSICS_FPS times per second // will be called before the render // may be called multiple times if the physics time is behind // physics_delta_time is statically 1/PHYSICS_FPS fn physics_update(&mut self, physics_delta_time: f32) { // before we move the rigidbodies, upsert the current terrain state // TODO use the chunk sleeping, and make this range dynamic if let Some(physics_manager) = &mut self.rb_sim_manager { if let Some(world) = &self.world { for cx in -5..5 { for cy in -5..5 { if let Some(chunk) = world .chunk_position_to_chunk_idx .get(&(cx, cy)) .map(|&idx| &world.chunks[idx]) && !chunk.sleeping { physics_manager.upsert_chunk_collider(cx, cy, chunk); } } } } physics_manager.rb_tick(physics_delta_time); } // move the particles if let Some(particle_manager) = &mut self.particle_manager && let Some(world) = &mut self.world { particle_manager.particle_tick(world, physics_delta_time); } } } impl Default for App { fn default() -> Self { Self { window: None, renderer_state: None, input: Input { last_mouse_pos_on_screen: None, last_mouse_world_pos: None, last_mouse_chunk_pos: None, last_mouse_local_pos: None, trigger_test_1: false, trigger_test_2: false, trigger_test_3: false, trigger_test_4: false, trigger_test_5: false, is_lmb_pressed: false, is_rmb_pressed: false, is_up_pressed: false, is_left_pressed: false, is_down_pressed: false, is_right_pressed: false, }, camera: None, world: Some(World::from_default_size()), rb_sim_manager: Some(RbSimManager::new()), particle_manager: Some(ParticleManager::new()), sim_seqno: 0, sim_paused: false, ignore_pause_next_tick: false, last_sim_update: Instant::now(), sim_updates_due: 0.0, last_physics_update: Instant::now(), physics_updates_due: 0.0, last_render: Instant::now(), config: Config { use_threading: true, brush_radius: 10.0, brush_material: MaterialId::Sand, dropper_material: MaterialId::Wood, cells_render: true, debug_render: false, debug_render_mode: DebugRenderMode::default(), }, diagnostics: Diagnostics { fps: 0.0, frame_times: VecDeque::new(), }, } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let window = Arc::new( event_loop .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) .unwrap(), ); self.window = Some(window.clone()); self.renderer_state = Some(executor::block_on(RendererState::new(window.clone()))); let size = window.inner_size(); self.camera = Some(Camera::new((size.width as i32, size.height as i32))); // let surface = SurfaceTexture::new(size.width, size.height, window_ref); // let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); // pixels.set_scaling_mode(ScalingMode::Fill); } fn window_event( &mut self, event_loop: &ActiveEventLoop, _: winit::window::WindowId, event: WindowEvent, ) { if let Some(renderer_state) = &mut self.renderer_state && let Some(window) = &mut self.window { let egui_response = renderer_state.egui_state.on_window_event(window, &event); // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks if egui_response.consumed { return; } } match event { WindowEvent::KeyboardInput { event: KeyEvent { physical_key: PhysicalKey::Code(code), state, repeat, .. }, .. } => { let pressed = state.is_pressed(); match code { KeyCode::KeyW => self.input.is_up_pressed = pressed, KeyCode::KeyA => self.input.is_left_pressed = pressed, KeyCode::KeyS => self.input.is_down_pressed = pressed, KeyCode::KeyD => self.input.is_right_pressed = pressed, KeyCode::KeyC => self.world = Some(World::from_default_size()), KeyCode::KeyP => { self.particle_manager .as_mut() .map(|pm| pm.particles = Vec::new()); } KeyCode::KeyV => { if let Some(rbsm) = self.rb_sim_manager.as_mut() { let entity_ids: Vec = rbsm.rb_entities.keys().copied().collect(); for entity_id in entity_ids { rbsm.destroy_rb_entity(entity_id); } } } KeyCode::KeyP if pressed && !repeat => { if let Some(rbsm) = self.rb_sim_manager.as_mut() { for &id in rbsm.rb_entities.keys() { write_rb_entity_to_world( self.world.as_mut().unwrap(), rbsm, id, self.sim_seqno, ); } let entity_ids: Vec = rbsm.rb_entities.keys().copied().collect(); for entity_id in entity_ids { rbsm.destroy_rb_entity(entity_id); } } } KeyCode::Space if pressed => self.sim_paused = !self.sim_paused, KeyCode::KeyX if pressed => self.ignore_pause_next_tick = true, KeyCode::Digit1 if pressed && !repeat => self.input.trigger_test_1 = true, KeyCode::Digit2 if pressed && !repeat => self.input.trigger_test_2 = true, KeyCode::Digit3 if pressed && !repeat => self.input.trigger_test_3 = true, KeyCode::Digit4 if pressed && !repeat => self.input.trigger_test_4 = true, KeyCode::Digit5 if pressed && !repeat => self.input.trigger_test_5 = true, _ => {} } } WindowEvent::CursorMoved { position, .. } => { self.input.last_mouse_pos_on_screen = Some((position.x as f32, position.y as f32)); if let Some(camera) = &mut self.camera { let world_pos = camera.screen_position_to_world(position.x as f32, position.y as f32); self.input.last_mouse_world_pos = Some(world_pos); let ((cx, cy), (lx, ly)) = World::split_game_position(world_pos.0 as i32, world_pos.1 as i32); self.input.last_mouse_chunk_pos = Some((cx, cy)); self.input.last_mouse_local_pos = Some((lx, ly)); } } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left { self.input.is_lmb_pressed = state == ElementState::Pressed } else if button == MouseButton::Right { self.input.is_rmb_pressed = state == ElementState::Pressed } } WindowEvent::Resized(size) => { if let Some(renderer_state) = &mut self.renderer_state && let Some(camera) = &mut self.camera { renderer_state.resize(size.width, size.height); camera.resize((size.width as i32, size.height as i32)); } } WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::RedrawRequested => { #[cfg(feature = "profiler")] puffin::GlobalProfiler::lock().new_frame(); puffin::profile_scope!("redraw_requested"); // compute FPS diagnostics let now = Instant::now(); let delta_time = (now - self.last_render).as_secs_f32(); self.last_render = now; self.diagnostics.frame_times.push_back(delta_time); if self.diagnostics.frame_times.len() > 30 { self.diagnostics.frame_times.pop_front(); } let average_frame_time = self.diagnostics.frame_times.iter().sum::() / self.diagnostics.frame_times.len() as f32; self.diagnostics.fps = 1.0 / average_frame_time; // UPDATE self.update(delta_time); // SIM UPDATE let secs_since_last_sim_update = (now - self.last_sim_update).as_secs_f32(); let expected_secs_since_last_sim_update = 1.0 / SIM_FPS as f32; self.last_sim_update = now; if self.sim_paused && self.ignore_pause_next_tick { self.sim_update(); self.ignore_pause_next_tick = false; } else if !self.sim_paused { self.sim_updates_due += secs_since_last_sim_update / expected_secs_since_last_sim_update; let mut updates_done = 0; // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock while self.sim_updates_due >= 1.0 && updates_done < 3 { self.sim_update(); self.sim_updates_due -= 1.0; updates_done += 1; } self.sim_updates_due = self.sim_updates_due.min(3.0); } // PHYSICS UPDATE let secs_since_last_physics_update = (now - self.last_physics_update).as_secs_f32(); let expected_secs_since_last_physics_update = 1.0 / PHYSICS_FPS as f32; self.last_physics_update = now; if !self.sim_paused { self.physics_updates_due += secs_since_last_physics_update / expected_secs_since_last_physics_update; let mut updates_done = 0; // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock while self.physics_updates_due >= 1.0 && updates_done < 3 { self.physics_update(PHYSICS_DELTA_TIME); self.physics_updates_due -= 1.0; updates_done += 1; } self.physics_updates_due = self.physics_updates_due.min(3.0); } if let Some(renderer_state) = &mut self.renderer_state && let Some(world) = &mut self.world && let Some(rb_sim_manager) = &mut self.rb_sim_manager && let Some(particle_manager) = &mut self.particle_manager && let Some(camera) = &mut self.camera { renderer_state.render( world, rb_sim_manager, particle_manager, camera, &mut self.config, &self.diagnostics, &self.input, ); } } _ => {} } } fn about_to_wait(&mut self, _: &ActiveEventLoop) { if let Some(window) = &self.window { window.request_redraw(); } else { panic!("No window!") } } } #[cfg(feature = "profiler")] fn start_profiler() { let _server = puffin_http::Server::new("127.0.0.1:8585").unwrap(); puffin::set_scopes_on(true); std::mem::forget(_server); // keep serving for the process lifetime std::process::Command::new("puffin_viewer") .args(["--url", "127.0.0.1:8585"]) .spawn() .ok(); // don't die if it isn't installed } fn main() -> Result<()> { #[cfg(feature = "profiler")] start_profiler(); let event_loop = EventLoop::new()?; event_loop.set_control_flow(ControlFlow::Poll); let mut app = App::default(); event_loop.run_app(&mut app)?; Ok(()) }