From 7364b81d1072c5e8d5f684aca4499c97a911c3c7 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 6 Feb 2024 08:54:21 -0500 Subject: [PATCH] Extract and start creating a physics engine --- Cargo.lock | 1 + falling-sand/Cargo.toml | 3 + falling-sand/src/main.rs | 225 +++++++++++++++++++++--------------- falling-sand/src/physics.rs | 191 ++++++++++++++++++++++++++++++ falling-sand/src/profile.rs | 27 +++++ 5 files changed, 357 insertions(+), 90 deletions(-) create mode 100644 falling-sand/src/physics.rs create mode 100644 falling-sand/src/profile.rs diff --git a/Cargo.lock b/Cargo.lock index 66126d8..9c4adac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1021,6 +1021,7 @@ dependencies = [ "async-channel 2.1.1", "async-std", "cairo-rs", + "cool_asserts", "gio", "glib", "glib-build-tools 0.17.10", diff --git a/falling-sand/Cargo.toml b/falling-sand/Cargo.toml index 43d0795..7476635 100644 --- a/falling-sand/Cargo.toml +++ b/falling-sand/Cargo.toml @@ -15,5 +15,8 @@ glib = { version = "0.18" } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } tokio = { version = "1", features = [ "full" ] } +[dev-dependencies] +cool_asserts = "*" + [build-dependencies] glib-build-tools = "0.17" \ No newline at end of file diff --git a/falling-sand/src/main.rs b/falling-sand/src/main.rs index 8778eff..07ffe59 100644 --- a/falling-sand/src/main.rs +++ b/falling-sand/src/main.rs @@ -1,15 +1,44 @@ use async_channel::Sender; +use async_std::task; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, time::Duration}; +use std::{ + cell::RefCell, + sync::{Arc, RwLock}, + time::Duration, +}; -const WIDTH: usize = 601; -const HEIGHT: usize = 601; -const CELL_SIZE: usize = 10; +/* +mod sand_area; +use sand_area::{SandArea, CELL_SIZE, HEIGHT, WIDTH}; +*/ -#[derive(Debug, Default)] +mod profile; +use profile::profile; + +mod physics; +use physics::World; + +pub const WIDTH: usize = 601; +pub const HEIGHT: usize = 601; +pub const CELL_SIZE: usize = 1; +const FPS: u64 = 60; + +#[derive(Debug)] pub struct SandViewPrivate { - area: RefCell, + rendering: RefCell, + last_update: RefCell, + // area: RefCell, +} + +impl Default for SandViewPrivate { + fn default() -> Self { + Self { + rendering: RefCell::new(false), + last_update: RefCell::new(std::time::Instant::now()), + // area: RefCell::new(SandArea::default()), + } + } } #[glib::object_subclass] @@ -36,36 +65,41 @@ impl Default for SandView { s.set_draw_func({ let s = s.clone(); move |_, context, width, height| { - context.set_source_rgb(0., 0., 0.); - let _ = context.paint(); - context.set_source_rgb(0.1, 0.1, 0.1); - for x in (0..width).step_by(CELL_SIZE) { - context.move_to(x as f64, 0.); - context.line_to(x as f64, HEIGHT as f64); - } - for y in (0..height).step_by(CELL_SIZE) { - context.move_to(0., y as f64); - context.line_to(WIDTH as f64, y as f64); - } - let _ = context.stroke(); - - let area = s.imp().area.borrow(); - for x in 0..area.width { - for y in 0..area.height { - if area.grain(x, y) { - context.set_source_rgb(0.8, 0.8, 0.8); - } else { - context.set_source_rgb(0., 0., 0.); - } - context.rectangle( - (x * CELL_SIZE + 1) as f64, - (y * CELL_SIZE + 1) as f64, - (CELL_SIZE - 2) as f64, - (CELL_SIZE - 2) as f64, - ); - let _ = context.fill(); + profile("redraw", || { + context.set_source_rgb(0., 0., 0.); + let _ = context.paint(); + context.set_source_rgb(0.1, 0.1, 0.1); + /* + for x in (0..width).step_by(CELL_SIZE) { + context.move_to(x as f64, 0.); + context.line_to(x as f64, HEIGHT as f64); } - } + for y in (0..height).step_by(CELL_SIZE) { + context.move_to(0., y as f64); + context.line_to(WIDTH as f64, y as f64); + } + let _ = context.stroke(); + */ + /* + let area = s.imp().area.borrow(); + for x in 0..area.width { + for y in 0..area.height { + if area.grain(x, y) { + context.set_source_rgb(0.8, 0.8, 0.8); + } else { + context.set_source_rgb(0., 0., 0.); + } + context.rectangle( + (x * CELL_SIZE) as f64, + (y * CELL_SIZE) as f64, + (CELL_SIZE) as f64, + (CELL_SIZE) as f64, + ); + } + } + let _ = context.fill(); + */ + }) } }); @@ -74,84 +108,77 @@ impl Default for SandView { } impl SandView { + /* fn set_area(&self, area: SandArea) { *self.imp().area.borrow_mut() = area; self.queue_draw(); } -} + */ -#[derive(Clone, Debug)] -struct SandArea { - width: usize, - height: usize, - grains: Vec, -} + fn set_last_update(&self, now: std::time::Instant) { + *self.imp().last_update.borrow_mut() = now; + } -impl Default for SandArea { - fn default() -> Self { - let width = WIDTH / CELL_SIZE; - let height = HEIGHT / CELL_SIZE; - Self { - width, - height, - grains: vec![false; width * height], - } - } -} - -impl SandArea { - pub fn add_grain(&mut self, x: usize, y: usize) { - let addr = self.addr(x, y); - self.grains[addr] = true; - } - - pub fn grain(&self, x: usize, y: usize) -> bool { - self.grains[self.addr(x, y)] - } - - pub fn tick(self) -> Self { - let mut new_grains = vec![false; self.width * self.height]; - for x in 0..self.width { - for y in 1..self.height { - let addr_above = self.addr(x, y - 1); - let addr = self.addr(x, y); - new_grains[addr] = self.grains[addr_above]; - } - } - Self { - width: self.width, - height: self.height, - grains: new_grains, - } - } - - fn addr(&self, x: usize, y: usize) -> usize { - y * self.width + x + fn last_update(&self) -> std::time::Instant { + self.imp().last_update.borrow().clone() } } +/* async fn animate(sender: Sender) { let mut sand_area = SandArea::default(); sand_area.add_grain(20, 20); loop { - std::thread::sleep(Duration::from_millis(1000 / 60)); - sand_area = sand_area.tick(); + std::thread::sleep(Duration::from_millis(1000 / FPS)); + sand_area = profile("next", || sand_area.next()); sender.send(sand_area.clone()).await; } } +*/ + +async fn run_physics(world: World) { + let mut last_update = std::time::Instant::now(); + let physics_tickspeed = std::time::Duration::from_millis(1000 / 60); + loop { + let now = std::time::Instant::now(); + + profile("world next", || world.next(now - last_update)); + last_update = wait_for_update(last_update, physics_tickspeed).await; + } +} + +async fn wait_for_update( + last_update: std::time::Instant, + tickspeed: std::time::Duration, +) -> std::time::Instant { + let now = std::time::Instant::now(); + let next_update = last_update + tickspeed; + task::sleep(next_update - now).await; + std::time::Instant::now() +} fn main() { let app = gtk::Application::builder() .application_id("com.luminescent-dreams.falling-sand") .build(); - let (sender, receiver) = async_channel::bounded(5); + // let (sender, receiver) = async_channel::bounded(5); + + let world = World::default(); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); - let background_thread = runtime.spawn(animate(sender)); + + let physics_thread = runtime.spawn(run_physics(world.clone())); + /* + let background_thread = runtime.spawn(animate(sender)); + */ + + /* + let sand_area = Arc::new(RwLock::new(SandArea::default())); + */ app.connect_activate(move |app| { let window = gtk::ApplicationWindow::new(app); @@ -162,10 +189,27 @@ fn main() { glib::spawn_future_local({ let view = view.clone(); - let receiver = receiver.clone(); + view.set_last_update(std::time::Instant::now()); + let fps_delay = std::time::Duration::from_millis(1000 / FPS); + // let sand_area = sand_area.clone(); + println!("FPS delay: {:?}", fps_delay); + async move { - while let Ok(area) = receiver.recv().await { - view.set_area(area); + loop { + /* + profile("setting area", || { + view.set_area(sand_area.read().unwrap().clone()) + }); + */ + + // Now, determine how much time remains before the next update. This can vary frame by frame, based on how much rendering has to be done. + let now = std::time::Instant::now(); + let mut wait_time = (view.last_update() + fps_delay) - now; + if wait_time < std::time::Duration::from_millis(0) { + wait_time = fps_delay; + } + view.set_last_update(now); + async_std::task::sleep(wait_time).await; } } }); @@ -173,5 +217,6 @@ fn main() { app.run(); - let _ = runtime.block_on(background_thread); + // let _ = runtime.block_on(background_thread); + let _ = runtime.block_on(physics_thread); } diff --git a/falling-sand/src/physics.rs b/falling-sand/src/physics.rs new file mode 100644 index 0000000..6153a95 --- /dev/null +++ b/falling-sand/src/physics.rs @@ -0,0 +1,191 @@ +use crate::profile; +use std::sync::{Arc, RwLock}; + +const GRAVITY: f64 = 1.; + +fn within(val1: f64, val2: f64, tolerance: f64) -> bool { + (val1 - val2).abs() <= tolerance +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct Point { + x: f64, + y: f64, +} + +impl Point { + fn distance_sq(&self, other: &Point) -> f64 { + let xdist = other.x - self.x; + let ydist = other.y - self.y; + + xdist * xdist + ydist * ydist + } + + fn eq(&self, other: &Point, tolerance: f64) -> bool { + within(self.x, other.x, tolerance) && within(self.y, other.y, tolerance) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct Vector { + dx: f64, + dy: f64, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct Grain { + radius: f64, + location: Point, + velocity: Vector, +} + +impl Grain { + fn move_by(&mut self, delta_t: std::time::Duration) { + self.location.x = self.location.x + self.velocity.dx * (delta_t.as_secs_f64()); + self.location.y = self.location.y + self.velocity.dy * (delta_t.as_secs_f64()); + } +} + +#[derive(Clone, Default)] +pub struct World { + grains: Arc>>, +} + +impl World { + pub fn new() -> Self { + Self { + grains: Arc::new(RwLock::new(vec![])), + } + } + + pub fn add_grain(&self, grain: Grain) { + self.grains.write().unwrap().push(grain); + } + + pub fn grain_count(&self) -> usize { + self.grains.read().unwrap().len() + } + + pub fn grains(&self) -> Vec { + self.grains.read().unwrap().clone() + } + + pub fn next(&self, delta_t: std::time::Duration) { + self.move_objects(delta_t); + } + + fn move_objects(&self, delta_t: std::time::Duration) { + for grain in self.grains.write().unwrap().iter_mut() { + grain.move_by(delta_t); + } + } +} + +pub trait Collision { + type Other; + fn collision_depth(&self, other: &Self::Other) -> Option; +} + +impl Collision for Grain { + type Other = Grain; + + fn collision_depth(&self, other: &Self::Other) -> Option { + let radii = (self.radius + other.radius) * (self.radius + other.radius); + let distance = self.location.distance_sq(&other.location); + if distance < radii { + Some(radii.sqrt() - distance.sqrt()) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use cool_asserts::assert_matches; + + #[test] + fn it_detects_grain_grain_collision() { + let grain_1 = Grain { + radius: 10., + location: Point { x: 0., y: 0. }, + velocity: Vector { dx: 0., dy: 0. }, + }; + let grain_2 = Grain { + radius: 10., + location: Point { x: 1., y: 0. }, + velocity: Vector { dx: 0., dy: 0. }, + }; + let grain_3 = Grain { + radius: 4., + location: Point { x: 15., y: 0. }, + velocity: Vector { dx: 0., dy: 0. }, + }; + + assert_matches!(grain_1.collision_depth(&grain_2), Some(v) => { + assert!(within(v, 19., 0.1)); + } ); + assert_matches!(grain_1.collision_depth(&grain_3), None); + } + + #[test] + fn it_moves_a_grain_according_to_time() { + let mut grain_1 = Grain { + radius: 10., + location: Point { x: 0., y: 0. }, + velocity: Vector { dx: 5., dy: 5. }, + }; + + grain_1.move_by(std::time::Duration::from_millis(500)); + let dest = Point { x: 2.5, y: 2.5 }; + assert!(grain_1.location.eq(&dest, 0.001)); + } + + #[test] + fn it_moves_many_grains() { + let grain_1 = Grain { + radius: 5., + location: Point { x: 0., y: 0. }, + velocity: Vector { dx: 0., dy: -1. }, + }; + let grain_1_b = Grain { + location: Point { x: 0., y: -0.5 }, + ..grain_1 + }; + let grain_2 = Grain { + radius: 5., + location: Point { x: 5., y: 5. }, + velocity: Vector { dx: 0., dy: 1. }, + }; + let grain_2_b = Grain { + location: Point { x: 5., y: 5.5 }, + ..grain_2 + }; + let grain_3 = Grain { + radius: 5., + location: Point { x: 15., y: 0. }, + velocity: Vector { dx: 1., dy: 1. }, + }; + let grain_3_b = Grain { + location: Point { x: 15.5, y: 0.5 }, + ..grain_3 + }; + + let world = World::new(); + world.add_grain(grain_1); + world.add_grain(grain_2); + world.add_grain(grain_3); + + world.next(std::time::Duration::from_millis(500)); + let grains = world.grains(); + for grain in grains { + let tolerance = 0.001; + assert!( + grain.location.eq(&grain_1_b.location, tolerance) + || grain.location.eq(&grain_2_b.location, tolerance) + || grain.location.eq(&grain_3_b.location, tolerance) + ); + } + } +} diff --git a/falling-sand/src/profile.rs b/falling-sand/src/profile.rs new file mode 100644 index 0000000..c47ede2 --- /dev/null +++ b/falling-sand/src/profile.rs @@ -0,0 +1,27 @@ +pub fn profile(tag: &str, f: F) -> T +where + F: FnOnce() -> T, +{ + let start_time = std::time::Instant::now(); + let retval = f(); + let end_time = std::time::Instant::now(); + println!("[{}] {:?}", tag, end_time - start_time); + retval +} + +/* +struct Profile { + metric: Arc>>, + receiver: async_channel::Receiver<(String, f64)>, +} + +impl Profile { + async fn run(&self) { + loop { + if let Ok(name, value) = self.receiver.recv().await { + self.metric.write().unwrap().entry(name).and_modify(|metric| metric.).or_insert(vec![0.; 100]); + } + } + } +} +*/