Extract and start creating a physics engine
This commit is contained in:
parent
1a7776b335
commit
7364b81d10
|
@ -1021,6 +1021,7 @@ dependencies = [
|
||||||
"async-channel 2.1.1",
|
"async-channel 2.1.1",
|
||||||
"async-std",
|
"async-std",
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
|
"cool_asserts",
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
"glib-build-tools 0.17.10",
|
"glib-build-tools 0.17.10",
|
||||||
|
|
|
@ -15,5 +15,8 @@ glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = "*"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
glib-build-tools = "0.17"
|
glib-build-tools = "0.17"
|
|
@ -1,15 +1,44 @@
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
|
use async_std::task;
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
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;
|
mod sand_area;
|
||||||
const CELL_SIZE: usize = 10;
|
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 {
|
pub struct SandViewPrivate {
|
||||||
area: RefCell<SandArea>,
|
rendering: RefCell<bool>,
|
||||||
|
last_update: RefCell<std::time::Instant>,
|
||||||
|
// area: RefCell<SandArea>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
#[glib::object_subclass]
|
||||||
|
@ -36,36 +65,41 @@ impl Default for SandView {
|
||||||
s.set_draw_func({
|
s.set_draw_func({
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |_, context, width, height| {
|
move |_, context, width, height| {
|
||||||
context.set_source_rgb(0., 0., 0.);
|
profile("redraw", || {
|
||||||
let _ = context.paint();
|
context.set_source_rgb(0., 0., 0.);
|
||||||
context.set_source_rgb(0.1, 0.1, 0.1);
|
let _ = context.paint();
|
||||||
for x in (0..width).step_by(CELL_SIZE) {
|
context.set_source_rgb(0.1, 0.1, 0.1);
|
||||||
context.move_to(x as f64, 0.);
|
/*
|
||||||
context.line_to(x as f64, HEIGHT as f64);
|
for x in (0..width).step_by(CELL_SIZE) {
|
||||||
}
|
context.move_to(x as f64, 0.);
|
||||||
for y in (0..height).step_by(CELL_SIZE) {
|
context.line_to(x as f64, HEIGHT as f64);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
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 {
|
impl SandView {
|
||||||
|
/*
|
||||||
fn set_area(&self, area: SandArea) {
|
fn set_area(&self, area: SandArea) {
|
||||||
*self.imp().area.borrow_mut() = area;
|
*self.imp().area.borrow_mut() = area;
|
||||||
self.queue_draw();
|
self.queue_draw();
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
fn set_last_update(&self, now: std::time::Instant) {
|
||||||
struct SandArea {
|
*self.imp().last_update.borrow_mut() = now;
|
||||||
width: usize,
|
}
|
||||||
height: usize,
|
|
||||||
grains: Vec<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SandArea {
|
fn last_update(&self) -> std::time::Instant {
|
||||||
fn default() -> Self {
|
self.imp().last_update.borrow().clone()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
async fn animate(sender: Sender<SandArea>) {
|
async fn animate(sender: Sender<SandArea>) {
|
||||||
let mut sand_area = SandArea::default();
|
let mut sand_area = SandArea::default();
|
||||||
sand_area.add_grain(20, 20);
|
sand_area.add_grain(20, 20);
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_millis(1000 / 60));
|
std::thread::sleep(Duration::from_millis(1000 / FPS));
|
||||||
sand_area = sand_area.tick();
|
sand_area = profile("next", || sand_area.next());
|
||||||
sender.send(sand_area.clone()).await;
|
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() {
|
fn main() {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
.application_id("com.luminescent-dreams.falling-sand")
|
.application_id("com.luminescent-dreams.falling-sand")
|
||||||
.build();
|
.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()
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.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| {
|
app.connect_activate(move |app| {
|
||||||
let window = gtk::ApplicationWindow::new(app);
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
|
@ -162,10 +189,27 @@ fn main() {
|
||||||
|
|
||||||
glib::spawn_future_local({
|
glib::spawn_future_local({
|
||||||
let view = view.clone();
|
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 {
|
async move {
|
||||||
while let Ok(area) = receiver.recv().await {
|
loop {
|
||||||
view.set_area(area);
|
/*
|
||||||
|
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();
|
app.run();
|
||||||
|
|
||||||
let _ = runtime.block_on(background_thread);
|
// let _ = runtime.block_on(background_thread);
|
||||||
|
let _ = runtime.block_on(physics_thread);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<RwLock<Vec<Grain>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Grain> {
|
||||||
|
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<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collision for Grain {
|
||||||
|
type Other = Grain;
|
||||||
|
|
||||||
|
fn collision_depth(&self, other: &Self::Other) -> Option<f64> {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
pub fn profile<F, T>(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<RwLock<HashMap<String, [f64; 100]>>>,
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
Loading…
Reference in New Issue