From c91debdc8c8e5e1aadb1056a94b1786ef27cd144 Mon Sep 17 00:00:00 2001 From: Jakub Arnold Date: Sun, 24 Sep 2023 00:19:30 +0200 Subject: [PATCH] Nice physics example --- Cargo.lock | 2 +- comfy/examples/physics.rs | 220 +++++++++++++++++++++++++++---------- comfy/src/macros.rs | 46 +++++--- comfy/src/update_stages.rs | 2 +- 4 files changed, 194 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9257938..6a7e6fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "blobs" version = "0.2.0" -source = "git+https://github.com/darthdeus/blobs.git#5ada419e205e0a062de558e3462dd99c3298b0d7" +source = "git+https://github.com/darthdeus/blobs.git#6f5e0d42c531d039c18c23e51f202a00eefb4045" dependencies = [ "approx", "assert_approx_eq", diff --git a/comfy/examples/physics.rs b/comfy/examples/physics.rs index 90eaef0..7c8d04b 100644 --- a/comfy/examples/physics.rs +++ b/comfy/examples/physics.rs @@ -1,94 +1,194 @@ use blobs::*; use comfy::*; -define_versions!(); - -pub async fn run() { - let config = GameConfig { - game_name: "Physics Example", - version: version_str(), - ..Default::default() - }; - - let engine = EngineState::new(config); - - let game = ComfyGame::new(engine); - - run_comfy_main_async(game).await; -} - -fn main() { - #[cfg(feature = "color-backtrace")] - color_backtrace::install(); - - #[cfg(not(target_arch = "wasm32"))] - { - pollster::block_on(run()); - } - - #[cfg(target_arch = "wasm32")] - { - wasm_bindgen_futures::spawn_local(run()); - } +// This example shows an integration between comfy and blobs, a simple 2d physics engine. It's not +// the most beautiful example, and maybe a bit verbose for what it does, but it tries to showcase +// some more extensible ways of using comfy. +comfy_game!( + "Physics Example", + GameContext, + GameState, + make_context, + setup, + update +); + +pub enum BallSpawningSpeed { + Comfy, + Uncomfy, } pub struct GameState { + pub spawn_timer: f32, pub physics: Physics, + pub ball_spawning_speed: BallSpawningSpeed, } impl GameState { pub fn new(_c: &mut EngineContext) -> Self { - Self { physics: Physics::new(Vec2::ZERO, false) } + Self { + spawn_timer: 0.0, + physics: Physics::new(vec2(0.0, -20.0), false), + ball_spawning_speed: BallSpawningSpeed::Comfy, + } } } pub struct GameContext<'a, 'b: 'a> { + // While we could access delta through .engine, it's easier to just expose it once and then + // benefit all over the codebase. + pub delta: f32, + // We'll continually spawn balls using a simple f32 timer. + pub spawn_timer: &'a mut f32, + pub ball_spawning_speed: &'a mut BallSpawningSpeed, + // We could just write c.engine.egui instead, but ... getting in the habit + // of re-exporting things into the `GameContext` usually ends up being nice. + pub egui: &'a egui::Context, pub physics: &'a mut Physics, pub engine: &'a mut EngineContext<'b>, } -pub struct ComfyGame { - pub engine: EngineState, - pub state: Option, +fn make_context<'a, 'b: 'a>( + state: &'a mut GameState, + engine: &'a mut EngineContext<'b>, +) -> GameContext<'a, 'b> { + GameContext { + spawn_timer: &mut state.spawn_timer, + delta: engine.delta, + ball_spawning_speed: &mut state.ball_spawning_speed, + egui: engine.egui, + physics: &mut state.physics, + engine, + } } -impl ComfyGame { - pub fn new(engine: EngineState) -> Self { - Self { state: None, engine } - } +fn setup(c: &mut GameContext) { + let rbd_handle = c.physics.insert_rbd(RigidBodyBuilder::new().build()); + + c.physics.insert_collider_with_parent( + ColliderBuilder::new().build(), + rbd_handle, + ); + + // We can add a circle constraint which prevents any body from leaving the constrained area. + // Note that currently blobs constraints work based on position and ignore collider radius. + c.physics + .constraints + .push(Constraint { position: Vec2::ZERO, radius: 5.0 }); + + // We'll need SFX for this + c.engine.load_sound_from_bytes( + // Every sound gets a string name later used to reference it. + "comfy-bell", + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../assets/bell-sfx.ogg" + )), + StaticSoundSettings::default(), + ); } -impl GameLoop for ComfyGame { - fn update(&mut self) { - let mut c = self.engine.make_context(); +fn update(c: &mut GameContext) { + *c.spawn_timer -= c.delta; + + let limit = match c.ball_spawning_speed { + BallSpawningSpeed::Comfy => 100, + BallSpawningSpeed::Uncomfy => 1000, + }; - if self.state.is_none() { - let mut state = GameState::new(&mut c); - setup(&mut state, &mut c); + if *c.spawn_timer <= 0.0 && c.physics.rbd_count() < limit { + let max_time = match c.ball_spawning_speed { + BallSpawningSpeed::Comfy => 0.4, + BallSpawningSpeed::Uncomfy => 0.01, + }; - self.state = Some(state); - } + // Every time the timer expires we reset it to a random value. + *c.spawn_timer = random_range(0.01, max_time); - if let Some(state) = self.state.as_mut() { - run_early_update_stages(&mut c); - run_mid_update_stages(&mut c); + let rbd_handle = c.physics.insert_rbd( + RigidBodyBuilder::new().position(random_circle(2.0)).build(), + ); - update(&mut GameContext { - physics: &mut state.physics, - engine: &mut c, - }); + c.physics.insert_collider_with_parent( + ColliderBuilder::new().radius(random_range(0.05, 0.4)).build(), + rbd_handle, + ); - run_late_update_stages(&mut c); - } + play_sound("comfy-bell"); } - fn engine(&mut self) -> &mut EngineState { - &mut self.engine + c.physics.step(c.delta as f64); + + // We could iterate rigid bodies individually, but blobs also has a nice way of collecting + // debug data in a simple format for rendering/logginc that we'll use to draw all bodies and + // colliders. + let debug = c.physics.debug_data(); + + for body in debug.bodies.iter() { + draw_circle(body.transform.translation, 0.1, RED.alpha(0.8), 5); } -} -fn setup(_state: &mut GameState, _c: &mut EngineContext) {} + for constraint in c.physics.constraints.iter() { + draw_circle( + constraint.position, + constraint.radius, + WHITE.alpha(0.1), + 3, + ); + } + + // Let's use the colliders arena to draw the colliders for a change. We could + // also just use `debug.colliders`. + for (_, collider) in c.physics.col_set.iter() { + // We'll draw collider circles behind the rigid body circles using a lower z_index. + draw_circle( + collider.absolute_transform.translation, + collider.radius, + BLUE.alpha(0.5), + 4, + ); + } -fn update(_c: &mut GameContext) { - draw_circle(Vec2::ZERO, 0.5, RED, 0); + draw_text( + "Be warned that blobs isn't the most stable with lots of balls :)", + Position::screen_percent(0.5, 0.1).to_world(), + WHITE, + TextAlign::Center, + ); + + draw_text( + &format!("There are now {} balls", c.physics.rbd_count()), + Position::screen_percent(0.5, 0.15).to_world(), + WHITE, + TextAlign::Center, + ); + + egui::Window::new("More balls") + .anchor(egui::Align2::LEFT_TOP, egui::vec2(10.0, 10.0)) + .show(c.egui, |ui| { + match c.ball_spawning_speed { + BallSpawningSpeed::Comfy => { + if ui.button("Make it go faster").clicked() { + *c.ball_spawning_speed = BallSpawningSpeed::Uncomfy; + } + } + BallSpawningSpeed::Uncomfy => { + if ui + .add(egui::Button::new( + egui::RichText::new("NOOOO, PLS STOP!!!").size( + rescale( + c.physics.rbd_count() as f32, + 0.0..limit as f32, + 10.0..100.0, + ), + ), + )) + .clicked() + { + *c.ball_spawning_speed = BallSpawningSpeed::Comfy; + c.physics.reset(); + } + } + } + }); } diff --git a/comfy/src/macros.rs b/comfy/src/macros.rs index 8ab605f..76d9d5d 100644 --- a/comfy/src/macros.rs +++ b/comfy/src/macros.rs @@ -116,33 +116,51 @@ macro_rules! simple_game { #[macro_export] macro_rules! comfy_game { - ($name:literal, $make_context:ident, $context:ident, $state:ident, $setup:ident, $update:ident) => { - define_main!($name); + ($name:literal, $context:ident, $state:ident, $make_context:ident, $setup:ident, $update:ident) => { + define_main!($name, ComfyGame); - pub struct Game { + pub struct ComfyGame { + pub engine: EngineState, pub state: Option<$state>, + pub setup_called: bool, } - impl Game { - pub fn new() -> Self { - Self { state: None } + impl ComfyGame { + pub fn new(engine: EngineState) -> Self { + Self { state: None, engine, setup_called: false } } } - impl GameLoop for Game { - fn update(&mut self, c: &mut EngineContext) { - if self.state.is_none() { - let mut state = $state::new(c); - $setup(&mut state, c); + impl GameLoop for ComfyGame { + fn update(&mut self) { + let mut c = self.engine.make_context(); - self.state = Some(state); + if self.state.is_none() { + self.state = Some(GameState::new(&mut c)); } if let Some(state) = self.state.as_mut() { - let mut game_c = make_context(state, c); - $update(&mut game_c); + run_early_update_stages(&mut c); + + { + let mut game_c = $make_context(state, &mut c); + + if !self.setup_called { + self.setup_called = true; + + $setup(&mut game_c); + } + + $update(&mut game_c); + } + + run_late_update_stages(&mut c); } } + + fn engine(&mut self) -> &mut EngineState { + &mut self.engine + } } }; } diff --git a/comfy/src/update_stages.rs b/comfy/src/update_stages.rs index a01635b..89efb44 100644 --- a/comfy/src/update_stages.rs +++ b/comfy/src/update_stages.rs @@ -40,7 +40,7 @@ pub fn run_early_update_stages(c: &mut EngineContext) { run_mid_update_stages(c); } -pub fn run_mid_update_stages(c: &mut EngineContext) { +fn run_mid_update_stages(c: &mut EngineContext) { timings_add_value("delta", delta()); pause_system(c);