diff --git a/crates/system/src/phys/mod.rs b/crates/system/src/phys/mod.rs index 593cc6d..72c9604 100644 --- a/crates/system/src/phys/mod.rs +++ b/crates/system/src/phys/mod.rs @@ -1,13 +1,12 @@ //! This module provides a physics-based simulation of one system. -pub mod controller; pub mod objects; -mod particlebuilder; +pub mod physimage; +mod physsim; +mod physwrapper; mod stepresources; -mod systemsim; -mod wrapper; -pub use particlebuilder::*; +pub use physimage::*; +pub use physsim::{PhysSim, PhysSimShipHandle}; +pub use physwrapper::PhysWrapper; pub use stepresources::*; -pub use systemsim::{PhysSim, PhysSimShipHandle}; -pub use wrapper::Wrapper; diff --git a/crates/system/src/phys/particlebuilder.rs b/crates/system/src/phys/objects/effect.rs similarity index 51% rename from crates/system/src/phys/particlebuilder.rs rename to crates/system/src/phys/objects/effect.rs index 402f3ab..b011713 100644 --- a/crates/system/src/phys/particlebuilder.rs +++ b/crates/system/src/phys/objects/effect.rs @@ -1,45 +1,46 @@ -use galactica_content::{Effect, SpriteHandle}; -use nalgebra::{Point2, Rotation2, Vector2}; +use galactica_content::{Content, EffectHandle, SpriteAutomaton}; +use nalgebra::{Rotation2, Vector2}; use rand::Rng; +use rapier2d::dynamics::{RigidBodyBuilder, RigidBodyHandle, RigidBodyType}; + +use crate::phys::{PhysStepResources, PhysWrapper}; /// Instructions to create a new particle -#[derive(Debug)] -pub struct ParticleBuilder { +#[derive(Debug, Clone)] +pub struct PhysEffect { /// The sprite to use for this particle - pub sprite: SpriteHandle, - - /// This object's center, in world coordinates. - pub pos: Point2, + pub anim: SpriteAutomaton, /// This particle's velocity, in world coordinates - pub velocity: Vector2, - - /// This particle's angle, in radians - pub angle: f32, - - /// This particle's angular velocity (rad/sec) - pub angvel: f32, + pub rigid_body: RigidBodyHandle, /// This particle's lifetime, in seconds - pub lifetime: f32, + lifetime: f32, /// The size of this particle, /// given as height in world units. pub size: f32, - /// Fade this particle out over this many seconds as it expires + /// Fade this particle over this many seconds as it expires pub fade: f32, + + /// If true, this effect has been destroyed, + /// and needs to be removed from the game. + is_destroyed: bool, } -impl ParticleBuilder { - /// Create a ParticleBuilder from an Effect - pub fn from_content( - effect: &Effect, - pos: Point2, +impl PhysEffect { + /// Create a new particle inside `Wrapper` + pub fn new( + ct: &Content, + wrapper: &mut PhysWrapper, + effect: EffectHandle, + pos: Vector2, parent_angle: f32, parent_velocity: Vector2, target_velocity: Vector2, ) -> Self { + let effect = ct.get_effect(effect); let mut rng = rand::thread_rng(); let velocity = { @@ -65,13 +66,17 @@ impl ParticleBuilder { parent_angle + effect.angle + rng.gen_range(-effect.angle_rng..=effect.angle_rng) }; - ParticleBuilder { - sprite: effect.sprite, - pos, - velocity, + println!("{:?}", effect.angvel_rng); - angle, - angvel, + let rb = RigidBodyBuilder::new(RigidBodyType::KinematicVelocityBased) + .position(pos.into()) + .rotation(angle) + .angvel(angvel) + .linvel(velocity); + + PhysEffect { + anim: SpriteAutomaton::new(ct, effect.sprite), + rigid_body: wrapper.insert_rigid_body(rb.build()), lifetime: 0f32 .max(effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng)), @@ -80,6 +85,28 @@ impl ParticleBuilder { size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)), fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)), + + is_destroyed: false, } } + + /// Step this effect's state by `t` seconds + pub fn step(&mut self, res: &PhysStepResources, wrapper: &mut PhysWrapper) { + if self.is_destroyed { + return; + } + + self.anim.step(res.ct, res.t); + self.lifetime -= res.t; + + if self.lifetime <= 0.0 { + wrapper.remove_rigid_body(self.rigid_body); + self.is_destroyed = true; + } + } + + /// Should this effect be deleted? + pub fn is_destroyed(&self) -> bool { + self.is_destroyed + } } diff --git a/crates/system/src/phys/objects/mod.rs b/crates/system/src/phys/objects/mod.rs index 1b5e903..0e4219f 100644 --- a/crates/system/src/phys/objects/mod.rs +++ b/crates/system/src/phys/objects/mod.rs @@ -1,7 +1,9 @@ //! This module contains game objects that may interact with the physics engine. -mod collapse; + +mod effect; mod projectile; mod ship; +pub use effect::*; pub use projectile::PhysProjectile; -pub use ship::{PhysSimShip, ShipControls}; +pub use ship::*; diff --git a/crates/system/src/phys/objects/projectile.rs b/crates/system/src/phys/objects/projectile.rs index 2d1100e..cd0ddc9 100644 --- a/crates/system/src/phys/objects/projectile.rs +++ b/crates/system/src/phys/objects/projectile.rs @@ -1,7 +1,12 @@ use galactica_content::{AnimationState, Content, FactionHandle, Projectile, SpriteAutomaton}; +use nalgebra::Vector2; use rand::Rng; use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle}; +use crate::phys::{physsim::NewObjects, PhysStepResources, PhysWrapper}; + +use super::PhysEffect; + /// A single projectile in this simulation #[derive(Debug, Clone)] pub struct PhysProjectile { @@ -12,7 +17,7 @@ pub struct PhysProjectile { anim: SpriteAutomaton, /// The remaining lifetime of this projectile, in seconds - pub lifetime: f32, + lifetime: f32, /// The faction this projectile belongs to pub faction: FactionHandle, @@ -25,6 +30,10 @@ pub struct PhysProjectile { /// This projectile's size variation pub size_rng: f32, + + /// If true, this projectile has been destroyed + /// and is waiting to be deallocated. + is_destroyed: bool, } impl PhysProjectile { @@ -47,18 +56,88 @@ impl PhysProjectile { lifetime, faction, size_rng: rng.gen_range(-size_rng..=size_rng), + is_destroyed: false, } } /// Process this projectile's state after `t` seconds - pub fn tick(&mut self, ct: &Content, t: f32) { - self.lifetime -= t; - self.anim.step(ct, t) + pub(in crate::phys) fn step( + &mut self, + res: &PhysStepResources, + new: &mut NewObjects, + wrapper: &mut PhysWrapper, + ) { + self.lifetime -= res.t; + self.anim.step(res.ct, res.t); + + if self.lifetime <= 0.0 { + self.destroy(res, new, wrapper, true); + } } - /// Has this projectile expired? - pub fn is_expired(&self) -> bool { - return self.lifetime < 0.0; + /// Destroy this projectile without creating an expire effect + pub(in crate::phys) fn destroy_silent( + &mut self, + res: &PhysStepResources, + new: &mut NewObjects, + wrapper: &mut PhysWrapper, + ) { + self.destroy(res, new, wrapper, false); + } + + /// Destroy this projectile + fn destroy( + &mut self, + res: &PhysStepResources, + new: &mut NewObjects, + wrapper: &mut PhysWrapper, + expire: bool, + ) { + if self.is_destroyed { + return; + } + + let rigid_body = wrapper.remove_rigid_body(self.rigid_body).unwrap(); + let mut rng = rand::thread_rng(); + + if expire { + match &self.content.expire_effect { + None => {} + Some(handle) => { + let x = res.ct.get_effect(*handle); + let pos = *rigid_body.translation(); + let vel = rigid_body.velocity_at_point(rigid_body.center_of_mass()); + let angle = rigid_body.rotation().angle(); + + let velocity = { + let a = rng + .gen_range(-x.velocity_scale_parent_rng..=x.velocity_scale_parent_rng); + + let velocity = (x.velocity_scale_parent + a) * vel; + + velocity + }; + + new.effects.push(PhysEffect::new( + res.ct, + wrapper, + *handle, + pos, + angle, + velocity, + Vector2::new(0.0, 0.0), + )); + } + } + }; + + self.is_destroyed = true; + wrapper.remove_rigid_body(self.rigid_body); + } + + /// Should this effect be deleted? + pub(in crate::phys) fn should_remove(&self) -> bool { + self.is_destroyed } } diff --git a/crates/system/src/phys/objects/ship.rs b/crates/system/src/phys/objects/ship.rs deleted file mode 100644 index 1d0bd56..0000000 --- a/crates/system/src/phys/objects/ship.rs +++ /dev/null @@ -1,316 +0,0 @@ -use galactica_content::{ - AnimationState, Content, EnginePoint, FactionHandle, OutfitHandle, ShipHandle, SpriteAutomaton, -}; -use nalgebra::{point, vector, Rotation2, Vector2}; -use rand::Rng; -use rapier2d::{ - dynamics::{RigidBody, RigidBodyHandle}, - geometry::{Collider, ColliderHandle}, -}; - -use crate::data::{ShipData, ShipPersonality, ShipState}; - -use super::{ - super::{ParticleBuilder, PhysStepResources}, - collapse::ShipCollapseSequence, -}; - -/// A ship's controls -#[derive(Debug, Clone)] -pub struct ShipControls { - /// True if turning left - pub left: bool, - - /// True if turning right - pub right: bool, - - /// True if foward thrust - pub thrust: bool, - - /// True if firing guns - pub guns: bool, -} - -impl ShipControls { - /// Create a new, empty ShipControls - pub fn new() -> Self { - ShipControls { - left: false, - right: false, - thrust: false, - guns: false, - } - } -} - -/// A ship instance in the physics system -#[derive(Debug, Clone)] -pub struct PhysSimShip { - /// This ship's physics handle - pub rigid_body: RigidBodyHandle, - - /// This ship's collider - pub collider: ColliderHandle, - - /// This ship's game data - pub(crate) data: ShipData, - - /// This ship's sprite animation state - anim: SpriteAutomaton, - - /// Animation state for each of this ship's engines - engine_anim: Vec<(EnginePoint, SpriteAutomaton)>, - - /// This ship's controls - pub(crate) controls: ShipControls, - - /// This ship's controls during the last frame - last_controls: ShipControls, - - /// This ship's collapse sequence - collapse_sequence: Option, -} - -impl PhysSimShip { - /// Make a new ship - pub(crate) fn new( - ct: &Content, - handle: ShipHandle, - personality: ShipPersonality, - faction: FactionHandle, - rigid_body: RigidBodyHandle, - collider: ColliderHandle, - ) -> Self { - let ship_ct = ct.get_ship(handle); - PhysSimShip { - anim: SpriteAutomaton::new(ct, ship_ct.sprite), - rigid_body, - collider, - data: ShipData::new(ct, handle, faction, personality), - engine_anim: Vec::new(), - controls: ShipControls::new(), - last_controls: ShipControls::new(), - collapse_sequence: Some(ShipCollapseSequence::new(ship_ct.collapse.length)), - } - } - - /// Step this ship's state by t seconds - pub fn step( - &mut self, - res: &mut PhysStepResources, - rigid_body: &mut RigidBody, - collider: &mut Collider, - ) { - self.data.step(res.t); - self.anim.step(res.ct, res.t); - - for (_, e) in &mut self.engine_anim { - e.step(res.ct, res.t); - } - - if !self.controls.thrust && self.last_controls.thrust { - let flare = self.get_flare(res.ct); - if flare.is_some() { - let flare_outfit = flare.unwrap(); - let flare = res.ct.get_outfit(flare_outfit); - if flare.engine_flare_on_stop.is_some() { - for (_, e) in &mut self.engine_anim { - e.jump_to(res.ct, flare.engine_flare_on_stop.unwrap()); - } - } - }; - } else if self.controls.thrust && !self.last_controls.thrust { - let flare = self.get_flare(res.ct); - if flare.is_some() { - let flare_outfit = flare.unwrap(); - let flare = res.ct.get_outfit(flare_outfit); - if flare.engine_flare_on_start.is_some() { - for (_, e) in &mut self.engine_anim { - e.jump_to(res.ct, flare.engine_flare_on_start.unwrap()); - } - } - }; - } - - match self.data.get_state() { - ShipState::Collapsing { .. } => { - // Borrow checker hack, so we may pass self.data - // to the collapse sequence - let mut seq = self.collapse_sequence.take().unwrap(); - seq.step(res, &self.data, rigid_body, collider); - self.collapse_sequence = Some(seq); - - if self.collapse_sequence.as_ref().unwrap().is_done() { - self.data.finish_collapse(); - } - } - ShipState::Flying { .. } => { - self.step_physics(res, rigid_body, collider); - self.step_effects(res, rigid_body, collider); - } - - ShipState::UnLanding { .. } | ShipState::Landing { .. } => { - self.step_physics(res, rigid_body, collider); - } - - ShipState::Dead | ShipState::Landed { .. } => {} - } - - self.last_controls = self.controls.clone(); - } - - /// Update this frame's physics - fn step_physics( - &mut self, - res: &mut PhysStepResources, - rigid_body: &mut RigidBody, - _collider: &mut Collider, - ) { - let ship_rot = rigid_body.rotation(); - let engine_force = ship_rot * (Vector2::new(1.0, 0.0) * res.t); - - if self.controls.thrust { - rigid_body.apply_impulse( - vector![engine_force.x, engine_force.y] - * self.data.get_outfits().get_engine_thrust(), - true, - ); - } - - if self.controls.right { - rigid_body.apply_torque_impulse( - self.data.get_outfits().get_steer_power() * -100.0 * res.t, - true, - ); - } - - if self.controls.left { - rigid_body.apply_torque_impulse( - self.data.get_outfits().get_steer_power() * 100.0 * res.t, - true, - ); - } - } - - /// Spawn this frame's particles - fn step_effects( - &mut self, - res: &mut PhysStepResources, - rigid_body: &mut RigidBody, - collider: &mut Collider, - ) { - let ship_content = res.ct.get_ship(self.data.get_content()); - let ship_pos = rigid_body.translation(); - let ship_rot = rigid_body.rotation(); - let ship_ang = ship_rot.angle(); - let mut rng = rand::thread_rng(); - - if self.data.get_hull() <= ship_content.damage.hull { - for e in &ship_content.damage.effects { - if rng.gen_range(0.0..=1.0) <= res.t / e.frequency { - let effect = res.ct.get_effect(e.effect); - - let pos = if let Some(pos) = e.pos { - Vector2::new(pos.x, pos.y) - } else { - // Pick a random point inside this ship's collider - let mut y = 0.0; - let mut x = 0.0; - let mut a = false; - while !a { - x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0; - y = rng.gen_range(-1.0..=1.0) - * ship_content.size * res.ct.get_sprite(ship_content.sprite).aspect - / 2.0; - a = collider.shape().contains_local_point(&point![x, y]); - } - Vector2::new(x, y) - }; - - let pos = ship_pos + (Rotation2::new(ship_ang) * pos); - let velocity = rigid_body.velocity_at_point(&point![pos.x, pos.y]); - - res.particles.push(ParticleBuilder::from_content( - effect, - pos.into(), - 0.0, - velocity, - Vector2::new(0.0, 0.0), - )) - } - } - } - } -} - -/// Public mutable -impl PhysSimShip { - fn get_flare(&mut self, ct: &Content) -> Option { - // TODO: better way to pick flare sprite - for (h, _) in self.data.get_outfits().iter_outfits() { - let c = ct.get_outfit(*h); - if c.engine_flare_sprite.is_some() { - return Some(*h); - } - } - return None; - } - - /// Re-create this ship's engine flare animations - /// Should be called whenever we change outfits - fn update_flares(&mut self, ct: &Content) { - let flare_outfit = self.get_flare(ct); - if flare_outfit.is_none() { - self.engine_anim = Vec::new(); - return; - } - let flare = ct - .get_outfit(flare_outfit.unwrap()) - .engine_flare_sprite - .unwrap(); - - self.engine_anim = ct - .get_ship(self.data.get_content()) - .engines - .iter() - .map(|e| (e.clone(), SpriteAutomaton::new(ct, flare))) - .collect(); - } - - /// Add one outfit to this ship - pub fn add_outfit(&mut self, ct: &Content, o: OutfitHandle) { - self.data.add_outfit(ct.get_outfit(o)); - self.update_flares(ct); - } - - /// Add many outfits to this ship - pub fn add_outfits(&mut self, ct: &Content, outfits: impl IntoIterator) { - for o in outfits { - self.data.add_outfit(ct.get_outfit(o)); - } - self.update_flares(ct); - } -} - -/// Public immutable -impl PhysSimShip { - /// Get this ship's control state - pub fn get_controls(&self) -> &ShipControls { - &self.controls - } - - /// Get this ship's engine animations - pub fn iter_engine_anim(&self) -> impl Iterator { - self.engine_anim.iter() - } - - /// Get this ship's animation state - pub fn get_anim_state(&self) -> AnimationState { - self.anim.get_texture_idx() - } - - /// Get this ship's game data struct - pub fn get_data(&self) -> &ShipData { - &self.data - } -} diff --git a/crates/system/src/phys/controller/autopilot/landing.rs b/crates/system/src/phys/objects/ship/autopilot/landing.rs similarity index 68% rename from crates/system/src/phys/controller/autopilot/landing.rs rename to crates/system/src/phys/objects/ship/autopilot/landing.rs index 17a062f..c337bee 100644 --- a/crates/system/src/phys/controller/autopilot/landing.rs +++ b/crates/system/src/phys/objects/ship/autopilot/landing.rs @@ -1,13 +1,7 @@ -use std::collections::HashMap; - use galactica_util::{clockwise_angle, to_radians}; use nalgebra::Vector2; -use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; -use crate::phys::{ - objects::{PhysSimShip, ShipControls}, - PhysStepResources, -}; +use crate::phys::{objects::ShipControls, PhysImage, PhysSimShipHandle, PhysStepResources}; // TODO: no wobble // TODO: slow down when near planet @@ -16,13 +10,11 @@ use crate::phys::{ /// Land this ship on the given object pub fn auto_landing( _res: &PhysStepResources, - rigid_bodies: &RigidBodySet, - ships: &HashMap, - this_ship: ColliderHandle, + img: &PhysImage, + this_ship: PhysSimShipHandle, target_pos: Vector2, ) -> Option { - let rigid_body_handle = ships.get(&this_ship).unwrap().rigid_body; - let rigid_body = rigid_bodies.get(rigid_body_handle).unwrap(); + let rigid_body = &img.get_ship(&this_ship).unwrap().rigidbody; let my_pos = *rigid_body.translation(); let my_rot = rigid_body.rotation() * Vector2::new(1.0, 0.0); let my_vel = rigid_body.linvel(); diff --git a/crates/system/src/phys/controller/autopilot/mod.rs b/crates/system/src/phys/objects/ship/autopilot/mod.rs similarity index 100% rename from crates/system/src/phys/controller/autopilot/mod.rs rename to crates/system/src/phys/objects/ship/autopilot/mod.rs diff --git a/crates/system/src/phys/objects/collapse.rs b/crates/system/src/phys/objects/ship/collapse.rs similarity index 78% rename from crates/system/src/phys/objects/collapse.rs rename to crates/system/src/phys/objects/ship/collapse.rs index e253ea7..b7f79b3 100644 --- a/crates/system/src/phys/objects/collapse.rs +++ b/crates/system/src/phys/objects/ship/collapse.rs @@ -1,10 +1,15 @@ use galactica_content::{CollapseEvent, Content, Ship}; use nalgebra::{Point2, Vector2}; use rand::{rngs::ThreadRng, Rng}; -use rapier2d::{dynamics::RigidBody, geometry::Collider}; +use rapier2d::{ + dynamics::RigidBodyHandle, + geometry::{Collider, ColliderHandle}, +}; -use super::super::{ParticleBuilder, PhysStepResources}; -use crate::data::ShipData; +use crate::{ + data::ShipData, + phys::{objects::PhysEffect, physsim::NewObjects, PhysStepResources, PhysWrapper}, +}; #[derive(Debug, Clone)] pub(super) struct ShipCollapseSequence { @@ -49,10 +54,14 @@ impl ShipCollapseSequence { pub(super) fn step( &mut self, res: &mut PhysStepResources, + wrapper: &mut PhysWrapper, + new: &mut NewObjects, ship_data: &ShipData, - rigid_body: &mut RigidBody, - collider: &mut Collider, + rigid_body_handle: RigidBodyHandle, + collider_handle: ColliderHandle, ) { + let rigid_body = wrapper.get_rigid_body(rigid_body_handle).unwrap().clone(); + let collider = wrapper.get_collider(collider_handle).unwrap().clone(); let ship_content = res.ct.get_ship(ship_data.get_content()); let ship_pos = rigid_body.translation(); let ship_rot = rigid_body.rotation(); @@ -71,26 +80,26 @@ impl ShipCollapseSequence { // ^^ Don't miss events scheduled at the very start of the sequence! { for spawner in &event.effects { - let effect = res.ct.get_effect(spawner.effect); - for _ in 0..spawner.count as usize { let pos: Vector2 = if let Some(pos) = spawner.pos { Vector2::new(pos.x, pos.y) } else { - self.random_in_ship(res.ct, ship_content, collider) + self.random_in_ship(res.ct, ship_content, &collider) }; let pos = ship_pos + (ship_rot * pos); let velocity = rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y)); - res.particles.push(ParticleBuilder::from_content( - effect, - pos.into(), + new.effects.push(PhysEffect::new( + res.ct, + wrapper, + spawner.effect, + pos, 0.0, velocity, Vector2::new(0.0, 0.0), - )) + )); } } } @@ -100,8 +109,6 @@ impl ShipCollapseSequence { // Create collapse effects for spawner in &ship_content.collapse.effects { - let effect = res.ct.get_effect(spawner.effect); - // Probability of adding a particle this frame. // The area of this function over [0, 1] should be 1. let pdf = |x: f32| { @@ -121,23 +128,22 @@ impl ShipCollapseSequence { let pos = if let Some(pos) = spawner.pos { Vector2::new(pos.x, pos.y) } else { - self.random_in_ship(res.ct, ship_content, collider) + self.random_in_ship(res.ct, ship_content, &collider) }; // Position, adjusted for ship rotation let pos = ship_pos + (ship_rot * pos); let vel = rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y)); - res.particles.push(ParticleBuilder { - sprite: effect.sprite, - pos: pos.into(), - velocity: vel, - angle: 0.0, - angvel: 0.0, - lifetime: effect.lifetime, - size: effect.size, - fade: 0.0, - }); + new.effects.push(PhysEffect::new( + res.ct, + wrapper, + spawner.effect, + pos, + 0.0, + vel, + Vector2::new(0.0, 0.0), + )); } } diff --git a/crates/system/src/phys/controller/mod.rs b/crates/system/src/phys/objects/ship/controller/mod.rs similarity index 63% rename from crates/system/src/phys/controller/mod.rs rename to crates/system/src/phys/objects/ship/controller/mod.rs index a713cce..61f4d4c 100644 --- a/crates/system/src/phys/controller/mod.rs +++ b/crates/system/src/phys/objects/ship/controller/mod.rs @@ -1,19 +1,15 @@ //! Various implementations of [`crate::ShipBehavior`] -pub(crate) mod autopilot; mod null; mod point; use null::*; use point::PointShipController; -use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; -use std::{collections::HashMap, fmt::Debug}; +use std::fmt::Debug; -use super::{ - objects::{PhysSimShip, ShipControls}, - PhysStepResources, -}; +use super::ShipControls; +use crate::phys::{PhysImage, PhysSimShipHandle, PhysStepResources}; /// Represents a ship controller #[derive(Debug, Clone)] @@ -40,13 +36,12 @@ impl ShipController { pub fn update_controls( &mut self, res: &PhysStepResources, - rigid_bodies: &RigidBodySet, - ships: &HashMap, - this_ship: ColliderHandle, + img: &PhysImage, + this_ship: PhysSimShipHandle, ) -> Option { match self { - Self::Null(n) => n.update_controls(res, rigid_bodies, ships, this_ship), - Self::Point(p) => p.update_controls(res, rigid_bodies, ships, this_ship), + Self::Null(n) => n.update_controls(res, img, this_ship), + Self::Point(p) => p.update_controls(res, img, this_ship), } } } @@ -61,8 +56,7 @@ where fn update_controls( &mut self, res: &PhysStepResources, - rigid_bodies: &RigidBodySet, - ships: &HashMap, - this_ship: ColliderHandle, + img: &PhysImage, + this_ship: PhysSimShipHandle, ) -> Option; } diff --git a/crates/system/src/phys/controller/null.rs b/crates/system/src/phys/objects/ship/controller/null.rs similarity index 59% rename from crates/system/src/phys/controller/null.rs rename to crates/system/src/phys/objects/ship/controller/null.rs index 67e9504..e52cb73 100644 --- a/crates/system/src/phys/controller/null.rs +++ b/crates/system/src/phys/objects/ship/controller/null.rs @@ -1,13 +1,5 @@ -use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; -use std::collections::HashMap; - -use super::{ - super::{ - objects::{PhysSimShip, ShipControls}, - PhysStepResources, - }, - ShipControllerStruct, -}; +use super::{ShipControllerStruct, ShipControls}; +use crate::phys::{PhysImage, PhysSimShipHandle, PhysStepResources}; /// The Null controller is assigned to objects that are static or not controlled by the computer. /// Most notably, the player's ship has a Null controller. @@ -25,9 +17,8 @@ impl ShipControllerStruct for NullShipController { fn update_controls( &mut self, _res: &PhysStepResources, - _rigid_bodies: &RigidBodySet, - _ships: &HashMap, - _this_ship: ColliderHandle, + _img: &PhysImage, + _this_ship: PhysSimShipHandle, ) -> Option { None } diff --git a/crates/system/src/phys/controller/point.rs b/crates/system/src/phys/objects/ship/controller/point.rs similarity index 58% rename from crates/system/src/phys/controller/point.rs rename to crates/system/src/phys/objects/ship/controller/point.rs index c447915..3249c46 100644 --- a/crates/system/src/phys/controller/point.rs +++ b/crates/system/src/phys/objects/ship/controller/point.rs @@ -1,19 +1,15 @@ use galactica_content::Relationship; use galactica_util::clockwise_angle; use nalgebra::Vector2; -use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; -use std::collections::HashMap; -use crate::data::ShipState; - -use super::{ - super::{ - objects::{PhysSimShip, ShipControls}, - PhysStepResources, - }, - ShipControllerStruct, +use crate::{ + data::ShipState, + phys::{PhysImage, PhysSimShipHandle}, }; +use super::{ShipControllerStruct, ShipControls}; +use crate::phys::PhysStepResources; + /// "Point" ship controller. /// Point and shoot towards the nearest enemy. #[derive(Debug, Clone)] @@ -30,43 +26,47 @@ impl ShipControllerStruct for PointShipController { fn update_controls( &mut self, res: &PhysStepResources, - rigid_bodies: &RigidBodySet, - ships: &HashMap, - this_ship: ColliderHandle, + img: &PhysImage, + this_ship: PhysSimShipHandle, ) -> Option { let mut controls = ShipControls::new(); - let my_ship = ships.get(&this_ship).unwrap(); - let this_rigidbody = rigid_bodies.get(my_ship.rigid_body).unwrap(); + let my_ship = match img.get_ship(&this_ship) { + None => return None, + Some(s) => s, + }; + + let this_rigidbody = &my_ship.rigidbody; let my_position = this_rigidbody.translation(); let my_rotation = this_rigidbody.rotation(); let my_angvel = this_rigidbody.angvel(); - let my_faction = res.ct.get_faction(my_ship.data.get_faction()); + let my_faction = res.ct.get_faction(my_ship.ship.data.get_faction()); // Iterate all possible targets - let mut hostile_ships = ships - .values() - .filter( - // Target only flying ships we're hostile towards - |s| match my_faction.relationships.get(&s.data.get_faction()).unwrap() { - Relationship::Hostile => match s.data.get_state() { - ShipState::Flying { .. } => true, - _ => false, - }, + let mut hostile_ships = img.iter_ships().filter( + // Target only flying ships we're hostile towards + |s| match my_faction + .relationships + .get(&s.ship.data.get_faction()) + .unwrap() + { + Relationship::Hostile => match s.ship.data.get_state() { + ShipState::Flying { .. } => true, _ => false, }, - ) - .map(|s| rigid_bodies.get(s.rigid_body).unwrap()); + _ => false, + }, + ); // Find the closest target let mut closest_enemy_position = match hostile_ships.next() { - Some(c) => c.translation(), + Some(s) => s.rigidbody.translation(), None => return Some(controls), // Do nothing if no targets are available }; let mut d = (my_position - closest_enemy_position).magnitude(); - for r in hostile_ships { - let p = r.translation(); + for s in hostile_ships { + let p = s.rigidbody.translation(); let new_d = (my_position - p).magnitude(); if new_d < d { d = new_d; diff --git a/crates/system/src/phys/objects/ship/controls.rs b/crates/system/src/phys/objects/ship/controls.rs new file mode 100644 index 0000000..880dab9 --- /dev/null +++ b/crates/system/src/phys/objects/ship/controls.rs @@ -0,0 +1,27 @@ +/// A ship's controls +#[derive(Debug, Clone)] +pub struct ShipControls { + /// True if turning left + pub left: bool, + + /// True if turning right + pub right: bool, + + /// True if foward thrust + pub thrust: bool, + + /// True if firing guns + pub guns: bool, +} + +impl ShipControls { + /// Create a new, empty ShipControls + pub fn new() -> Self { + ShipControls { + left: false, + right: false, + thrust: false, + guns: false, + } + } +} diff --git a/crates/system/src/phys/objects/ship/mod.rs b/crates/system/src/phys/objects/ship/mod.rs new file mode 100644 index 0000000..1153baa --- /dev/null +++ b/crates/system/src/phys/objects/ship/mod.rs @@ -0,0 +1,8 @@ +mod autopilot; +mod collapse; +mod controller; +mod controls; +mod ship; + +pub use controls::ShipControls; +pub use ship::PhysShip; diff --git a/crates/system/src/phys/objects/ship/ship.rs b/crates/system/src/phys/objects/ship/ship.rs new file mode 100644 index 0000000..59036fd --- /dev/null +++ b/crates/system/src/phys/objects/ship/ship.rs @@ -0,0 +1,543 @@ +use galactica_content::{ + AnimationState, Content, EnginePoint, FactionHandle, GunPoint, OutfitHandle, + ProjectileCollider, ShipHandle, SpriteAutomaton, +}; +use nalgebra::{vector, Point2, Rotation2, Vector2}; +use rand::Rng; +use rapier2d::{ + dynamics::{RigidBodyBuilder, RigidBodyHandle}, + geometry::{ColliderBuilder, ColliderHandle, Group, InteractionGroups}, + pipeline::ActiveEvents, +}; + +use super::{autopilot, collapse::ShipCollapseSequence, controller::ShipController, ShipControls}; +use crate::{ + data::{ShipAutoPilot, ShipData, ShipPersonality, ShipState}, + phys::{ + objects::{PhysEffect, PhysProjectile}, + physsim::NewObjects, + PhysImage, PhysSimShipHandle, PhysStepResources, PhysWrapper, + }, +}; + +/// A ship instance in the physics system +#[derive(Debug, Clone)] +pub struct PhysShip { + /// This ship's physics handle + pub rigid_body: RigidBodyHandle, + + /// This ship's collider + pub collider: ColliderHandle, + + /// This ship's game data + pub(crate) data: ShipData, + + /// This ship's sprite animation state + anim: SpriteAutomaton, + + /// Animation state for each of this ship's engines + engine_anim: Vec<(EnginePoint, SpriteAutomaton)>, + + /// This ship's controls + pub(crate) controls: ShipControls, + + /// This ship's controls during the last frame + last_controls: ShipControls, + + /// This ship's collapse sequence + collapse_sequence: Option, + + /// This ship's controller + controller: ShipController, + + /// If true, this ship's collider has been destroyed, + /// and this ship is waiting to be destroyed. + /// + /// Note that this is NOT "in-game" destroyed, + /// but rather "internal game logic" destroyed. + /// In-game destroyed corresponds to the "Dead" state. + is_destroyed: bool, +} + +impl PhysShip { + /// Make a new ship + pub(crate) fn new( + ct: &Content, + handle: ShipHandle, + personality: ShipPersonality, + faction: FactionHandle, + rigid_body: RigidBodyHandle, + collider: ColliderHandle, + ) -> Self { + let ship_ct = ct.get_ship(handle); + PhysShip { + anim: SpriteAutomaton::new(ct, ship_ct.sprite), + rigid_body, + collider, + data: ShipData::new(ct, handle, faction, personality), + engine_anim: Vec::new(), + controls: ShipControls::new(), + last_controls: ShipControls::new(), + collapse_sequence: Some(ShipCollapseSequence::new(ship_ct.collapse.length)), + is_destroyed: false, + controller: match personality { + ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(), + ShipPersonality::Point => ShipController::new_point(), + }, + } + } + + /// Step this ship's state by t seconds + pub(in crate::phys) fn step( + &mut self, + res: &mut PhysStepResources, + img: &PhysImage, + wrapper: &mut PhysWrapper, + new: &mut NewObjects, + ) { + if self.is_destroyed { + return; + } + + self.data.step(res.t); + self.anim.step(res.ct, res.t); + + for (_, e) in &mut self.engine_anim { + e.step(res.ct, res.t); + } + + // Flare animations + if !self.controls.thrust && self.last_controls.thrust { + let flare = self.get_flare(res.ct); + if flare.is_some() { + let flare_outfit = flare.unwrap(); + let flare = res.ct.get_outfit(flare_outfit); + if flare.engine_flare_on_stop.is_some() { + for (_, e) in &mut self.engine_anim { + e.jump_to(res.ct, flare.engine_flare_on_stop.unwrap()); + } + } + }; + } else if self.controls.thrust && !self.last_controls.thrust { + let flare = self.get_flare(res.ct); + if flare.is_some() { + let flare_outfit = flare.unwrap(); + let flare = res.ct.get_outfit(flare_outfit); + if flare.engine_flare_on_start.is_some() { + for (_, e) in &mut self.engine_anim { + e.jump_to(res.ct, flare.engine_flare_on_start.unwrap()); + } + } + }; + } + + match self.data.get_state() { + ShipState::Collapsing { .. } => { + // Borrow checker hack, so we may pass self.data + // to the collapse sequence + let mut seq = self.collapse_sequence.take().unwrap(); + seq.step( + res, + wrapper, + new, + &self.data, + self.rigid_body, + self.collider, + ); + self.collapse_sequence = Some(seq); + + if self.collapse_sequence.as_ref().unwrap().is_done() { + self.data.finish_collapse(); + } + } + ShipState::Flying { autopilot } => { + // Compute new controls + let controls = match autopilot { + ShipAutoPilot::Landing { target } => { + let target_obj = res.ct.get_system_object(*target); + let controls = autopilot::auto_landing( + res, + img, + PhysSimShipHandle(self.collider), + Vector2::new(target_obj.pos.x, target_obj.pos.y), + ); + + // Try to land the ship. + // True if success, false if failure. + // Failure implies no state changes. + let landed = 'landed: { + let r = wrapper.get_rigid_body(self.rigid_body).unwrap(); + + let t_pos = Vector2::new(target_obj.pos.x, target_obj.pos.y); + let s_pos = Vector2::new( + r.position().translation.x, + r.position().translation.y, + ); + + // TODO: deactivate collider when landing. + // Can't just set_active(false), since we still need that collider's mass. + + // We're in land range... + if (t_pos - s_pos).magnitude() > target_obj.size / 2.0 { + break 'landed false; + } + + // And we'll stay in land range long enough. + if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0)) + .magnitude() > target_obj.size / 2.0 + { + break 'landed false; + } + + let collider = wrapper.get_collider_mut(self.collider).unwrap(); + collider.set_collision_groups(InteractionGroups::new( + Group::GROUP_1, + Group::empty(), + )); + self.data.start_land_on(*target); + break 'landed true; + }; + + if landed { + None + } else { + controls + } + } + ShipAutoPilot::None => { + self.controller + .update_controls(res, img, PhysSimShipHandle(self.collider)) + } + }; + + if let Some(controls) = controls { + self.controls = controls; + } + + self.step_physics(res, wrapper); + self.step_effects(res, wrapper, new); + + // If we're firing, try to fire each gun + if self.controls.guns { + // TODO: don't allocate here. This is a hack to satisfy the borrow checker, + // convert this to a refcell or do the replace dance. + let pairs: Vec<(GunPoint, Option)> = self + .data + .get_outfits() + .iter_gun_points() + .map(|(p, o)| (p.clone(), o.clone())) + .collect(); + + for (gun_point, outfit) in pairs { + if self.data.fire_gun(res.ct, &gun_point) { + let outfit = outfit.unwrap(); + + let mut rng = rand::thread_rng(); + + let rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap(); + let ship_pos = rigid_body.translation(); + let ship_rot = rigid_body.rotation(); + let ship_ang = ship_rot.angle(); + let ship_vel = + rigid_body.velocity_at_point(rigid_body.center_of_mass()); + + let pos = ship_pos + (ship_rot * gun_point.pos); + + let outfit = res.ct.get_outfit(outfit); + let outfit = outfit.gun.as_ref().unwrap(); + + let spread = rng.gen_range( + -outfit.projectile.angle_rng..=outfit.projectile.angle_rng, + ); + let vel = ship_vel + + (Rotation2::new(ship_ang + spread) + * Vector2::new( + outfit.projectile.speed + + rng.gen_range( + -outfit.projectile.speed_rng + ..=outfit.projectile.speed_rng, + ), + 0.0, + )); + + let rigid_body = RigidBodyBuilder::kinematic_velocity_based() + .translation(pos) + .rotation(ship_ang) + .linvel(vel) + .build(); + + let mut collider = match &outfit.projectile.collider { + ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius) + .sensor(true) + .active_events(ActiveEvents::COLLISION_EVENTS) + .build(), + }; + + collider.set_collision_groups(InteractionGroups::new( + Group::GROUP_2, + Group::GROUP_1, + )); + + let rigid_body = wrapper.insert_rigid_body(rigid_body); + let collider = wrapper.insert_collider(collider, rigid_body); + + new.projectiles.push(PhysProjectile::new( + res.ct, + outfit.projectile.clone(), + rigid_body, + self.data.get_faction(), + collider, + )); + } + } + } + } + + ShipState::UnLanding { + to_position, + current_z, + from, + } => { + let from_obj = res.ct.get_system_object(*from); + + let controls = autopilot::auto_landing( + &res, + img, + PhysSimShipHandle(self.collider), + Vector2::new(to_position.translation.x, to_position.translation.y), + ); + let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap(); + let max_d = (Vector2::new(from_obj.pos.x, from_obj.pos.y) + - Vector2::new(to_position.translation.x, to_position.translation.y)) + .magnitude(); + let now_d = (r.translation() + - Vector2::new(to_position.translation.x, to_position.translation.y)) + .magnitude(); + let f = now_d / max_d; + + let current_z = *current_z; + let zdist = 1.0 - from_obj.pos.z; + + if current_z <= 1.0 { + // Finish unlanding ship + self.data.finish_unland_to(); + wrapper + .get_collider_mut(self.collider) + .unwrap() + .set_collision_groups(InteractionGroups::new( + Group::GROUP_1, + Group::GROUP_1 | Group::GROUP_2, + )); + } else if current_z <= 1.5 { + self.data + .set_unlanding_z(1f32.max(current_z - (0.5 * res.t) / 0.5)); + self.controls = ShipControls::new(); + } else { + self.data.set_unlanding_z(1.0 - zdist * f); + + if let Some(controls) = controls { + self.controls = controls; + } + self.step_physics(res, wrapper); + }; + } + + ShipState::Landing { target, current_z } => { + let target_obj = res.ct.get_system_object(*target); + let controls = autopilot::auto_landing( + &res, + img, + PhysSimShipHandle(self.collider), + Vector2::new(target_obj.pos.x, target_obj.pos.y), + ); + + let current_z = *current_z; + let zdist = target_obj.pos.z - 1.0; + + if current_z >= target_obj.pos.z { + // Finish landing ship + self.data.finish_land_on(); + let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap(); + r.set_enabled(false); + r.set_angvel(0.0, false); + r.set_linvel(nalgebra::Vector2::new(0.0, 0.0), false); + } else { + self.data.set_landing_z(current_z + zdist * res.t / 2.0); + + if let Some(controls) = controls { + self.controls = controls; + } + self.step_physics(res, wrapper); + }; + } + + ShipState::Landed { .. } => {} + + ShipState::Dead => { + wrapper.remove_rigid_body(self.rigid_body); + self.is_destroyed = true; + } + }; + + self.last_controls = self.controls.clone(); + } + + /// Should this ship's data be removed? + pub fn should_remove(&self) -> bool { + self.is_destroyed + } + + /// Update this frame's physics + fn step_physics(&mut self, res: &mut PhysStepResources, wrapper: &mut PhysWrapper) { + let rigid_body = &mut wrapper.get_rigid_body_mut(self.rigid_body).unwrap(); + let ship_rot = rigid_body.rotation(); + let engine_force = ship_rot * (Vector2::new(1.0, 0.0) * res.t); + + if self.controls.thrust { + rigid_body.apply_impulse( + vector![engine_force.x, engine_force.y] + * self.data.get_outfits().get_engine_thrust(), + true, + ); + } + + if self.controls.right { + rigid_body.apply_torque_impulse( + self.data.get_outfits().get_steer_power() * -100.0 * res.t, + true, + ); + } + + if self.controls.left { + rigid_body.apply_torque_impulse( + self.data.get_outfits().get_steer_power() * 100.0 * res.t, + true, + ); + } + } + + /// Spawn this frame's particles + fn step_effects( + &mut self, + res: &mut PhysStepResources, + wrapper: &mut PhysWrapper, + new: &mut NewObjects, + ) { + let rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap().clone(); + let collider = wrapper.get_collider(self.collider).unwrap().clone(); + + let ship_content = res.ct.get_ship(self.data.get_content()); + let ship_pos = rigid_body.translation(); + let ship_rot = rigid_body.rotation(); + let ship_ang = ship_rot.angle(); + let mut rng = rand::thread_rng(); + + if self.data.get_hull() <= ship_content.damage.hull { + for e in &ship_content.damage.effects { + if rng.gen_range(0.0..=1.0) <= res.t / e.frequency { + let pos = if let Some(pos) = e.pos { + Vector2::new(pos.x, pos.y) + } else { + // Pick a random point inside this ship's collider + let mut y = 0.0; + let mut x = 0.0; + let mut a = false; + while !a { + x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0; + y = rng.gen_range(-1.0..=1.0) + * ship_content.size * res.ct.get_sprite(ship_content.sprite).aspect + / 2.0; + a = collider.shape().contains_local_point(&Point2::new(x, y)); + } + Vector2::new(x, y) + }; + + let pos = ship_pos + (Rotation2::new(ship_ang) * pos); + let velocity = rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y)); + + new.effects.push(PhysEffect::new( + res.ct, + wrapper, + e.effect, + pos.into(), + 0.0, + velocity, + Vector2::new(0.0, 0.0), + )); + } + } + } + } +} + +/// Public mutable +impl PhysShip { + fn get_flare(&mut self, ct: &Content) -> Option { + // TODO: better way to pick flare sprite + for (h, _) in self.data.get_outfits().iter_outfits() { + let c = ct.get_outfit(*h); + if c.engine_flare_sprite.is_some() { + return Some(*h); + } + } + return None; + } + + /// Re-create this ship's engine flare animations + /// Should be called whenever we change outfits + fn update_flares(&mut self, ct: &Content) { + let flare_outfit = self.get_flare(ct); + if flare_outfit.is_none() { + self.engine_anim = Vec::new(); + return; + } + let flare = ct + .get_outfit(flare_outfit.unwrap()) + .engine_flare_sprite + .unwrap(); + + self.engine_anim = ct + .get_ship(self.data.get_content()) + .engines + .iter() + .map(|e| (e.clone(), SpriteAutomaton::new(ct, flare))) + .collect(); + } + + /// Add one outfit to this ship + pub fn add_outfit(&mut self, ct: &Content, o: OutfitHandle) { + self.data.add_outfit(ct.get_outfit(o)); + self.update_flares(ct); + } + + /// Add many outfits to this ship + pub fn add_outfits(&mut self, ct: &Content, outfits: impl IntoIterator) { + for o in outfits { + self.data.add_outfit(ct.get_outfit(o)); + } + self.update_flares(ct); + } +} + +/// Public immutable +impl PhysShip { + /// Get this ship's control state + pub fn get_controls(&self) -> &ShipControls { + &self.controls + } + + /// Get this ship's engine animations + pub fn iter_engine_anim(&self) -> impl Iterator { + self.engine_anim.iter() + } + + /// Get this ship's animation state + pub fn get_anim_state(&self) -> AnimationState { + self.anim.get_texture_idx() + } + + /// Get this ship's game data struct + pub fn get_data(&self) -> &ShipData { + &self.data + } +} diff --git a/crates/system/src/phys/physimage.rs b/crates/system/src/phys/physimage.rs new file mode 100644 index 0000000..56a8f88 --- /dev/null +++ b/crates/system/src/phys/physimage.rs @@ -0,0 +1,95 @@ +//! Provides a snapshot of one frame of a physics simulation + +use std::collections::HashMap; + +use super::{ + objects::{PhysEffect, PhysProjectile, PhysShip}, + PhysSimShipHandle, +}; +use rapier2d::dynamics::RigidBody; + +/// A snapshot of one frame of a physics simulation +#[derive(Debug)] +pub struct PhysImage { + /// The ships in this frame + pub(crate) ships: Vec, + + /// Map ship handles to indices in ships + pub(crate) ship_map: HashMap, + + /// The projectiles in this frame + pub(crate) projectiles: Vec, + + /// The effects in this frame + pub(crate) effects: Vec, +} + +impl PhysImage { + /// Create an empty simulation image + pub fn new() -> Self { + Self { + ships: Vec::new(), + projectiles: Vec::new(), + ship_map: HashMap::new(), + effects: Vec::new(), + } + } + + /// Clear all buffers in this image + pub fn clear(&mut self) { + self.ship_map.clear(); + self.projectiles.clear(); + self.effects.clear(); + self.ships.clear(); + } + + /// Iterate ships in this image + pub fn iter_ships(&self) -> impl Iterator { + self.ships.iter() + } + + /// Get a ship by its handle + pub fn get_ship(&self, handle: &PhysSimShipHandle) -> Option<&PhysShipImage> { + self.ship_map.get(handle).map(|x| &self.ships[*x]) + } + + /// Iterate projectiles in this image + pub fn iter_projectiles(&self) -> impl Iterator { + self.projectiles.iter() + } + + /// Iterate effects in this image + pub fn iter_effects(&self) -> impl Iterator { + self.effects.iter() + } +} + +/// A snapshot of a ship's state +#[derive(Debug)] +pub struct PhysShipImage { + /// The ship's data + pub ship: PhysShip, + + /// The ship's rigidbody + pub rigidbody: RigidBody, +} + +/// a snapshot of a projectile's state +#[derive(Debug)] +pub struct PhysProjectileImage { + /// The projectile's data + pub projectile: PhysProjectile, + + /// The projectile's rigidbody + pub rigidbody: RigidBody, +} + +/// a snapshot of a projectile's state +#[derive(Debug)] +pub struct PhysEffectImage { + /// The effect's data + pub effect: PhysEffect, + + /// The effect's rigidbody + pub rigidbody: RigidBody, +} diff --git a/crates/system/src/phys/physsim.rs b/crates/system/src/phys/physsim.rs new file mode 100644 index 0000000..3a293c5 --- /dev/null +++ b/crates/system/src/phys/physsim.rs @@ -0,0 +1,372 @@ +use galactica_content::Relationship; +use galactica_content::{Content, FactionHandle, ShipHandle, SystemHandle}; +use galactica_playeragent::PlayerAgent; +use nalgebra::{Isometry2, Point2, Rotation2, Vector2}; +use rand::Rng; +use rapier2d::{ + dynamics::RigidBodyBuilder, + geometry::{ColliderHandle, Group, InteractionGroups}, +}; +use std::collections::HashMap; + +use super::PhysEffectImage; +use super::{ + objects::{PhysEffect, PhysProjectile, PhysShip}, + PhysImage, PhysProjectileImage, PhysShipImage, PhysStepResources, PhysWrapper, +}; +use crate::data::{ShipAutoPilot, ShipPersonality, ShipState}; + +// TODO: replace with a more generic handle +/// A handle for a ship in this simulation +/// This lets other crates reference ships +/// without including `rapier2d`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PhysSimShipHandle(pub ColliderHandle); + +pub(super) struct NewObjects { + pub projectiles: Vec, + pub effects: Vec, +} + +/// A list of new objects to create this frame +impl NewObjects { + /// Create an empty NewObjects + /// This should only be called once in each PhysSim + pub fn new() -> Self { + Self { + projectiles: Vec::new(), + effects: Vec::new(), + } + } + + /// Clear all lists in this struct. + /// Call after saving all new objects. + pub fn clear(&mut self) { + self.projectiles.clear(); + self.effects.clear(); + } +} + +/// Manages the physics state of one system +pub struct PhysSim { + /// The system this sim is attached to + _system: SystemHandle, + + wrapper: PhysWrapper, + + new: NewObjects, + + effects: Vec, + projectiles: HashMap, + ships: HashMap, +} + +// Private methods +impl PhysSim { + pub(super) fn start_unland_ship(&mut self, ct: &Content, collider: ColliderHandle) { + let ship = self.ships.get_mut(&collider).unwrap(); + let obj = ship.data.get_state().landed_on().unwrap(); + let obj = ct.get_system_object(obj); + + let mut rng = rand::thread_rng(); + let radius = rng.gen_range(500.0..=1500.0); + let angle = rng.gen_range(0.0..std::f32::consts::TAU); + let target_offset = Rotation2::new(angle) * Vector2::new(radius, 0.0); + let target_trans = Vector2::new(obj.pos.x, obj.pos.y) + target_offset; + let target_pos = Isometry2::new(target_trans, angle); + + ship.data.start_unland_to(ct, target_pos); + + let r = self.wrapper.get_rigid_body_mut(ship.rigid_body).unwrap(); + r.set_enabled(true); + r.set_position( + Isometry2::new(Vector2::new(obj.pos.x, obj.pos.y), angle), + true, + ); + } + + pub(super) fn collide_projectile_ship( + &mut self, + res: &mut PhysStepResources, + projectile_h: ColliderHandle, + ship_h: ColliderHandle, + ) { + let projectile = self.projectiles.get_mut(&projectile_h); + let ship = self.ships.get_mut(&ship_h); + if projectile.is_none() || ship.is_none() { + return; + } + let projectile = projectile.unwrap(); + let ship = ship.unwrap(); + + let f = res.ct.get_faction(projectile.faction); + let r = f.relationships.get(&ship.data.get_faction()).unwrap(); + let destory_projectile = match r { + Relationship::Hostile => match ship.data.get_state() { + ShipState::Flying { .. } => { + ship.data.apply_damage(projectile.content.damage); + true + } + ShipState::Collapsing { .. } => true, + _ => false, + }, + _ => false, + }; + + if destory_projectile { + let pr = self.wrapper.get_rigid_body(projectile.rigid_body).unwrap(); + let v = + pr.velocity_at_point(pr.center_of_mass()).normalize() * projectile.content.force; + let pos = *pr.translation(); + let _ = pr; + + let r = self.wrapper.get_rigid_body_mut(ship.rigid_body).unwrap(); + r.apply_impulse_at_point(Vector2::new(v.x, v.y), Point2::new(pos.x, pos.y), true); + + // Borrow again, we can only have one at a time + let pr = self.wrapper.get_rigid_body(projectile.rigid_body).unwrap(); + let pos = *pr.translation(); + let angle = pr.rotation().angle(); + + match &projectile.content.impact_effect { + None => {} + Some(x) => { + let r = ship.rigid_body; + let sr = self.wrapper.get_rigid_body(r).unwrap(); + let parent_velocity = pr.velocity_at_point(pr.center_of_mass()); + let target_velocity = + sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y)); + + self.effects.push(PhysEffect::new( + res.ct, + &mut self.wrapper, + *x, + pos, + angle, + parent_velocity, + target_velocity, + )); + } + }; + + projectile.destroy_silent(res, &mut self.new, &mut self.wrapper); + } + } +} + +// Public methods +impl PhysSim { + /// Create a new physics system + pub fn new(_ct: &Content, system: SystemHandle) -> Self { + Self { + _system: system, + wrapper: PhysWrapper::new(), + + new: NewObjects::new(), + effects: Vec::new(), + projectiles: HashMap::new(), + ships: HashMap::new(), + } + } + + /// Add a ship to this physics system + pub fn add_ship( + &mut self, + ct: &Content, + handle: ShipHandle, + faction: FactionHandle, + personality: ShipPersonality, + position: Point2, + ) -> PhysSimShipHandle { + let ship_content = ct.get_ship(handle); + let mut cl = ship_content.collider.0.clone(); + // TODO: additonal ship mass from outfits and cargo + + let rb = RigidBodyBuilder::dynamic() + .angular_damping(ship_content.angular_drag) + .linear_damping(ship_content.linear_drag) + .translation(Vector2::new(position.x, position.y)) + .can_sleep(false); + + cl.set_collision_groups(InteractionGroups::new( + Group::GROUP_1, + Group::GROUP_1 | Group::GROUP_2, + )); + + let ridid_body = self.wrapper.insert_rigid_body(rb.build()); + let collider = self.wrapper.insert_collider(cl, ridid_body); + + self.ships.insert( + collider, + PhysShip::new(ct, handle, personality, faction, ridid_body, collider), + ); + + return PhysSimShipHandle(collider); + } + + /// Update a player ship's controls + pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) { + if player.ship.is_none() { + return; + } + let ship_object = self.ships.get_mut(&player.ship.unwrap()); + if let Some(ship_object) = ship_object { + match ship_object.data.get_state() { + ShipState::Landing { .. } + | ShipState::UnLanding { .. } + | ShipState::Collapsing { .. } + | ShipState::Dead => {} + + ShipState::Flying { + autopilot: ShipAutoPilot::None, + } => { + ship_object.controls.guns = player.input.pressed_guns(); + ship_object.controls.left = player.input.pressed_left(); + ship_object.controls.right = player.input.pressed_right(); + ship_object.controls.thrust = player.input.pressed_thrust(); + + if player.input.pressed_land() { + ship_object.data.set_autopilot(ShipAutoPilot::Landing { + target: player.selection.get_planet().unwrap(), + }) + } + } + + ShipState::Flying { .. } => { + // Any input automatically releases autopilot + if player.input.pressed_left() + || player.input.pressed_right() + || player.input.pressed_thrust() + || player.input.pressed_guns() + { + ship_object.data.set_autopilot(ShipAutoPilot::None); + } + } + + ShipState::Landed { .. } => { + if player.input.pressed_land() { + self.start_unland_ship(ct, player.ship.unwrap()); + } + } + }; + } + } + + /// Step this simulation by `t` seconds + pub fn step(&mut self, mut res: PhysStepResources, img: &PhysImage) { + res.timing.start_physics_sim(); + + // Update physics + self.wrapper.step(res.t); + + // Handle collision events + while let Ok(event) = &self.wrapper.collision_queue.try_recv() { + if event.started() { + let a = event.collider1(); + let b = event.collider2(); + + // If projectiles are part of this collision, make sure + // `a` is one of them. + let (a, b) = if self.projectiles.contains_key(&b) { + (b, a) + } else { + (a, b) + }; + + let p = self.projectiles.get(&a); + let s = self.ships.get_mut(&b); + if p.is_none() || s.is_none() { + continue; + } + self.collide_projectile_ship(&mut res, a, b); + } + } + + // Step and garbage-collect projectiles + self.projectiles.retain(|_, proj| { + proj.step(&mut res, &mut self.new, &mut self.wrapper); + !proj.should_remove() + }); + + // Step and garbage-collect ships + res.timing.start_physics_ships(); + self.ships.retain(|_, ship| { + ship.step(&mut res, img, &mut self.wrapper, &mut self.new); + !ship.should_remove() + }); + res.timing.mark_physics_ships(); + + // Step and garbage-collect effects + self.effects.retain_mut(|x| { + x.step(&res, &mut self.wrapper); + !x.is_destroyed() + }); + + // Process new objects + for p in self.new.projectiles.iter() { + self.projectiles.insert(p.collider, p.clone()); + } + for e in self.new.effects.iter() { + self.effects.push(e.clone()); + } + self.new.clear(); + + res.timing.mark_physics_sim(); + } + + /// Update an image with the current simulation state + pub fn update_image(&self, img: &mut PhysImage) { + img.clear(); + + for s in self.ships.values() { + img.ship_map + .insert(PhysSimShipHandle(s.collider), img.ships.len()); + img.ships.push(PhysShipImage { + ship: s.clone(), + rigidbody: self.wrapper.get_rigid_body(s.rigid_body).unwrap().clone(), + }) + } + + for p in self.projectiles.values() { + img.projectiles.push(PhysProjectileImage { + projectile: p.clone(), + rigidbody: self.wrapper.get_rigid_body(p.rigid_body).unwrap().clone(), + }) + } + + for e in self.effects.iter() { + img.effects.push(PhysEffectImage { + effect: e.clone(), + rigidbody: self.wrapper.get_rigid_body(e.rigid_body).unwrap().clone(), + }) + } + } +} + +// Public getters +impl PhysSim { + /// Get a ship physics object + pub fn get_ship(&self, ship: &PhysSimShipHandle) -> Option<&PhysShip> { + self.ships.get(&ship.0) + } + + /// Get a ship physics object + pub fn get_ship_mut(&mut self, ship: &PhysSimShipHandle) -> Option<&mut PhysShip> { + self.ships.get_mut(&ship.0) + } + + /// Iterate over all ships in this physics system + pub fn iter_ships(&self) -> impl Iterator + '_ { + self.ships.values() + } + + /// Iterate over all ships in this physics system + pub fn iter_projectiles(&self) -> impl Iterator + '_ { + self.projectiles.values() + } + + /// Iterate over all particles in this physics system + pub fn iter_particles(&self) -> impl Iterator + '_ { + self.effects.iter() + } +} diff --git a/crates/system/src/phys/physwrapper.rs b/crates/system/src/phys/physwrapper.rs new file mode 100644 index 0000000..9d39ff4 --- /dev/null +++ b/crates/system/src/phys/physwrapper.rs @@ -0,0 +1,124 @@ +use crossbeam::channel::Receiver; +use rapier2d::{ + dynamics::{ + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, + RigidBody, RigidBodyHandle, RigidBodySet, + }, + geometry::{BroadPhase, Collider, ColliderHandle, ColliderSet, CollisionEvent, NarrowPhase}, + na::vector, + pipeline::{ChannelEventCollector, PhysicsPipeline}, +}; + +/// Wraps Rapier2d physics parameters +pub struct PhysWrapper { + ip: IntegrationParameters, + pp: PhysicsPipeline, + im: IslandManager, + bp: BroadPhase, + np: NarrowPhase, + ij: ImpulseJointSet, + mj: MultibodyJointSet, + ccd: CCDSolver, + + rigid_body_set: RigidBodySet, + collider_set: ColliderSet, + + collision_handler: ChannelEventCollector, + + /// Collision event queue + /// this should be emptied after every frame. + pub collision_queue: Receiver, +} + +impl PhysWrapper { + /// Make a new physics wrapper + pub fn new() -> Self { + let (collision_send, collision_queue) = crossbeam::channel::unbounded(); + let (contact_force_send, _) = crossbeam::channel::unbounded(); + + Self { + ip: IntegrationParameters::default(), + pp: PhysicsPipeline::new(), + im: IslandManager::new(), + bp: BroadPhase::new(), + np: NarrowPhase::new(), + ij: ImpulseJointSet::new(), + mj: MultibodyJointSet::new(), + ccd: CCDSolver::new(), + collision_queue, + collision_handler: ChannelEventCollector::new(collision_send, contact_force_send), + + rigid_body_set: RigidBodySet::new(), + collider_set: ColliderSet::new(), + } + } + + /// Step physics sim by `t` seconds + pub fn step(&mut self, t: f32) { + self.ip.dt = t; + self.pp.step( + &vector![0.0, 0.0], + &self.ip, + &mut self.im, + &mut self.bp, + &mut self.np, + &mut self.rigid_body_set, + &mut self.collider_set, + &mut self.ij, + &mut self.mj, + &mut self.ccd, + None, + &(), + &mut self.collision_handler, + ); + } + + /// Remove a rigid body and all its colliders from this physics wrapper + pub fn remove_rigid_body(&mut self, body: RigidBodyHandle) -> Option { + return self.rigid_body_set.remove( + body, + &mut self.im, + &mut self.collider_set, + &mut self.ij, + &mut self.mj, + true, + ); + } +} + +impl PhysWrapper { + /// Get a rigidbody from this physics wrapper + pub fn get_rigid_body(&self, h: RigidBodyHandle) -> Option<&RigidBody> { + self.rigid_body_set.get(h) + } + + /// Get a rigidbody from this physics wrapper, mutably + pub fn get_rigid_body_mut(&mut self, h: RigidBodyHandle) -> Option<&mut RigidBody> { + self.rigid_body_set.get_mut(h) + } + + /// Get a collider from this physics wrapper + pub fn get_collider(&self, h: ColliderHandle) -> Option<&Collider> { + self.collider_set.get(h) + } + + /// Get a collider from this physics wrapper, mutably + pub fn get_collider_mut(&mut self, h: ColliderHandle) -> Option<&mut Collider> { + self.collider_set.get_mut(h) + } + + /// Add a rigid body + pub fn insert_rigid_body(&mut self, rb: RigidBody) -> RigidBodyHandle { + self.rigid_body_set.insert(rb) + } + + /// Attach a collider to a rigid body + pub fn insert_collider( + &mut self, + collider: Collider, + parent_handle: RigidBodyHandle, + ) -> ColliderHandle { + self.collider_set + .insert_with_parent(collider, parent_handle, &mut self.rigid_body_set) + } +} diff --git a/crates/system/src/phys/stepresources.rs b/crates/system/src/phys/stepresources.rs index 5518152..0d028ef 100644 --- a/crates/system/src/phys/stepresources.rs +++ b/crates/system/src/phys/stepresources.rs @@ -1,4 +1,3 @@ -use super::{wrapper::Wrapper, ParticleBuilder}; use galactica_content::Content; use galactica_util::timing::Timing; @@ -10,12 +9,6 @@ pub struct PhysStepResources<'a> { /// Length of time step pub t: f32, - /// Particles to create - pub particles: &'a mut Vec, - /// Record detailed frame timing breakdown pub timing: &'a mut Timing, - - /// Physics computer - pub wrapper: &'a mut Wrapper, } diff --git a/crates/system/src/phys/systemsim/mod.rs b/crates/system/src/phys/systemsim/mod.rs deleted file mode 100644 index 0f22382..0000000 --- a/crates/system/src/phys/systemsim/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -use galactica_content::{Content, FactionHandle, ShipHandle, SystemHandle}; -use galactica_playeragent::PlayerAgent; -use nalgebra::{Point2, Vector2}; - -use rapier2d::{ - dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle, RigidBodySet}, - geometry::{ColliderHandle, ColliderSet, Group, InteractionGroups}, -}; -use std::collections::HashMap; - -use crate::data::{ShipAutoPilot, ShipPersonality, ShipState}; - -use super::{ - controller::ShipController, - objects::{PhysProjectile, PhysSimShip}, -}; - -// TODO: replace with a more generic handle -/// A handle for a ship in this simulation -/// This lets other crates reference ships -/// without including `rapier2d`. -pub struct PhysSimShipHandle(pub ColliderHandle); - -/// Manages the physics state of one system -#[derive(Clone)] -pub struct PhysSim { - /// The system this sim is attached to - _system: SystemHandle, - - rigid_body_set: RigidBodySet, - collider_set: ColliderSet, - - projectiles: HashMap, - ships: HashMap, - ship_behaviors: HashMap, -} - -mod step; -mod steputil; - -// Public methods -impl PhysSim { - /// Create a new physics system - pub fn new(_ct: &Content, system: SystemHandle) -> Self { - Self { - _system: system, - rigid_body_set: RigidBodySet::new(), - collider_set: ColliderSet::new(), - projectiles: HashMap::new(), - ships: HashMap::new(), - ship_behaviors: HashMap::new(), - } - } - - /// Add a ship to this physics system - pub fn add_ship( - &mut self, - ct: &Content, - handle: ShipHandle, - faction: FactionHandle, - personality: ShipPersonality, - position: Point2, - ) -> PhysSimShipHandle { - let ship_content = ct.get_ship(handle); - let mut cl = ship_content.collider.0.clone(); - // TODO: additonal ship mass from outfits and cargo - - let rb = RigidBodyBuilder::dynamic() - .angular_damping(ship_content.angular_drag) - .linear_damping(ship_content.linear_drag) - .translation(Vector2::new(position.x, position.y)) - .can_sleep(false); - - cl.set_collision_groups(InteractionGroups::new( - Group::GROUP_1, - Group::GROUP_1 | Group::GROUP_2, - )); - - let ridid_body = self.rigid_body_set.insert(rb.build()); - let collider = - self.collider_set - .insert_with_parent(cl, ridid_body, &mut self.rigid_body_set); - - self.ship_behaviors.insert( - collider, - match personality { - ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(), - ShipPersonality::Point => ShipController::new_point(), - }, - ); - - self.ships.insert( - collider, - PhysSimShip::new(ct, handle, personality, faction, ridid_body, collider), - ); - - return PhysSimShipHandle(collider); - } - - /// Update a player ship's controls - pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) { - if player.ship.is_none() { - return; - } - let ship_object = self.ships.get_mut(&player.ship.unwrap()); - if let Some(ship_object) = ship_object { - match ship_object.data.get_state() { - ShipState::Landing { .. } - | ShipState::UnLanding { .. } - | ShipState::Collapsing { .. } - | ShipState::Dead => {} - - ShipState::Flying { - autopilot: ShipAutoPilot::None, - } => { - ship_object.controls.guns = player.input.pressed_guns(); - ship_object.controls.left = player.input.pressed_left(); - ship_object.controls.right = player.input.pressed_right(); - ship_object.controls.thrust = player.input.pressed_thrust(); - - if player.input.pressed_land() { - ship_object.data.set_autopilot(ShipAutoPilot::Landing { - target: player.selection.get_planet().unwrap(), - }) - } - } - - ShipState::Flying { .. } => { - // Any input automatically releases autopilot - if player.input.pressed_left() - || player.input.pressed_right() - || player.input.pressed_thrust() - || player.input.pressed_guns() - { - ship_object.data.set_autopilot(ShipAutoPilot::None); - } - } - - ShipState::Landed { .. } => { - if player.input.pressed_land() { - self.start_unland_ship(ct, player.ship.unwrap()); - } - } - }; - } - } -} - -// Public getters -impl PhysSim { - /// Get a ship physics object - pub fn get_ship(&self, ship: &PhysSimShipHandle) -> Option<&PhysSimShip> { - self.ships.get(&ship.0) - } - - /// Get a ship physics object - pub fn get_ship_mut(&mut self, ship: &PhysSimShipHandle) -> Option<&mut PhysSimShip> { - self.ships.get_mut(&ship.0) - } - - /// Get a rigid body from a handle - pub fn get_rigid_body(&self, r: RigidBodyHandle) -> Option<&RigidBody> { - self.rigid_body_set.get(r) - } - - /// Get a rigid body from a handle - pub fn get_rigid_body_mut(&mut self, r: RigidBodyHandle) -> Option<&mut RigidBody> { - self.rigid_body_set.get_mut(r) - } - - /// Iterate over all ships in this physics system - pub fn iter_ship_body(&self) -> impl Iterator + '_ { - self.ships - .values() - .map(|x| (x, self.rigid_body_set.get(x.rigid_body).unwrap())) - } - - /// Iterate over all ships in this physics system - pub fn iter_ships(&self) -> impl Iterator + '_ { - self.ships.values() - } - - /// Iterate over all ships in this physics system - pub fn iter_projectiles(&self) -> impl Iterator + '_ { - self.projectiles.values() - } -} diff --git a/crates/system/src/phys/systemsim/step.rs b/crates/system/src/phys/systemsim/step.rs deleted file mode 100644 index ac8a466..0000000 --- a/crates/system/src/phys/systemsim/step.rs +++ /dev/null @@ -1,334 +0,0 @@ -use galactica_content::{GunPoint, OutfitHandle, ProjectileCollider}; -use nalgebra::{Rotation2, Vector2}; -use rand::Rng; -use rapier2d::{ - dynamics::RigidBodyBuilder, - geometry::{ColliderBuilder, Group, InteractionGroups}, - pipeline::ActiveEvents, -}; - -use crate::{ - data::{ShipAutoPilot, ShipState}, - phys::{ - controller::autopilot, - objects::{PhysProjectile, ShipControls}, - ParticleBuilder, PhysStepResources, - }, -}; - -use super::PhysSim; - -impl PhysSim { - /// Run ship updates: - /// - updates ship controls (runs behaviors) - /// - applies ship controls - /// - creates projectiles - fn step_ships(&mut self, res: &mut PhysStepResources) { - // We can't apply these right away since self is borrowed - // by the iterator - // TODO: don't allocate! - let mut projectiles = Vec::new(); - let mut to_remove = Vec::new(); - // Again, borrow checker hack. TODO: fix - let keys: Vec<_> = self.ships.keys().map(|c| *c).collect(); - for collider in keys { - // Borrow immutably for now... - // (required to compute controls) - let ship = self.ships.get(&collider).unwrap(); - match ship.data.get_state() { - ShipState::Dead => { - to_remove.push(collider); - } - - ShipState::Landed { .. } | ShipState::Collapsing { .. } => { - let ship = self.ships.get_mut(&collider).unwrap(); - ship.step( - res, - &mut self.rigid_body_set[ship.rigid_body], - &mut self.collider_set[ship.collider], - ); - } - - ShipState::UnLanding { - to_position, - current_z, - from, - } => { - let from_obj = res.ct.get_system_object(*from); - let controls = autopilot::auto_landing( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - Vector2::new(to_position.translation.x, to_position.translation.y), - ); - let r = &mut self.rigid_body_set[ship.rigid_body]; - let max_d = (Vector2::new(from_obj.pos.x, from_obj.pos.y) - - Vector2::new(to_position.translation.x, to_position.translation.y)) - .magnitude(); - let now_d = (r.translation() - - Vector2::new(to_position.translation.x, to_position.translation.y)) - .magnitude(); - let f = now_d / max_d; - - let current_z = *current_z; - let ship = self.ships.get_mut(&collider).unwrap(); - let zdist = 1.0 - from_obj.pos.z; - - if current_z <= 1.0 { - self.finish_unland_ship(collider); - } else if current_z <= 1.5 { - ship.data - .set_unlanding_z(1f32.max(current_z - (0.5 * res.t) / 0.5)); - ship.controls = ShipControls::new(); - } else { - ship.data.set_unlanding_z(1.0 - zdist * f); - - if let Some(controls) = controls { - ship.controls = controls; - } - ship.step(res, r, &mut self.collider_set[ship.collider]) - }; - } - - ShipState::Landing { target, current_z } => { - let target_obj = res.ct.get_system_object(*target); - let controls = autopilot::auto_landing( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - Vector2::new(target_obj.pos.x, target_obj.pos.y), - ); - - let current_z = *current_z; - let ship = self.ships.get_mut(&collider).unwrap(); - let r = &mut self.rigid_body_set[ship.rigid_body]; - let zdist = target_obj.pos.z - 1.0; - - if current_z >= target_obj.pos.z { - self.finish_land_ship(collider); - } else { - ship.data.set_landing_z(current_z + zdist * res.t / 2.0); - - if let Some(controls) = controls { - ship.controls = controls; - } - ship.step(res, r, &mut self.collider_set[ship.collider]) - }; - } - - ShipState::Flying { autopilot } => { - // Compute new controls - // This is why we borrow immutably first - let controls = match autopilot { - ShipAutoPilot::Landing { - target: target_handle, - } => { - let target_obj = res.ct.get_system_object(*target_handle); - let controls = autopilot::auto_landing( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - Vector2::new(target_obj.pos.x, target_obj.pos.y), - ); - - let landed = self.try_land_ship(res.ct, collider, *target_handle); - if landed { - None - } else { - controls - } - } - - ShipAutoPilot::None => { - let b = self.ship_behaviors.get_mut(&collider).unwrap(); - b.update_controls( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - ) - } - }; - - // Re-borrow mutably to apply changes - let ship = self.ships.get_mut(&collider).unwrap(); - - if let Some(controls) = controls { - ship.controls = controls; - } - ship.step( - res, - &mut self.rigid_body_set[ship.rigid_body], - &mut self.collider_set[ship.collider], - ); - - // If we're firing, try to fire each gun - if ship.controls.guns { - // TODO: don't allocate here. This is a hack to satisfy the borrow checker, - // convert this to a refcell or do the replace dance. - let pairs: Vec<(GunPoint, Option)> = ship - .data - .get_outfits() - .iter_gun_points() - .map(|(p, o)| (p.clone(), o.clone())) - .collect(); - - for (gun, outfit) in pairs { - if ship.data.fire_gun(res.ct, &gun) { - projectiles.push((ship.collider, gun.clone(), outfit.unwrap())); - } - } - } - } - } - } - - // Remove ships that don't exist - for c in to_remove { - self.remove_ship(res, c); - } - - // Create projectiles - for (collider, gun_point, outfit) in projectiles { - let mut rng = rand::thread_rng(); - - let ship = self.ships.get(&collider).unwrap(); - let rigid_body = self.get_rigid_body(ship.rigid_body).unwrap(); - let ship_pos = rigid_body.translation(); - let ship_rot = rigid_body.rotation(); - let ship_ang = ship_rot.angle(); - let ship_vel = rigid_body.velocity_at_point(rigid_body.center_of_mass()); - - let pos = ship_pos + (ship_rot * gun_point.pos); - - let outfit = res.ct.get_outfit(outfit); - let outfit = outfit.gun.as_ref().unwrap(); - - let spread = rng.gen_range(-outfit.projectile.angle_rng..=outfit.projectile.angle_rng); - let vel = ship_vel - + (Rotation2::new(ship_ang + spread) - * Vector2::new( - outfit.projectile.speed - + rng.gen_range( - -outfit.projectile.speed_rng..=outfit.projectile.speed_rng, - ), - 0.0, - )); - - let rigid_body = RigidBodyBuilder::kinematic_velocity_based() - .translation(pos) - .rotation(ship_ang) - .linvel(vel) - .build(); - - let mut collider = match &outfit.projectile.collider { - ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius) - .sensor(true) - .active_events(ActiveEvents::COLLISION_EVENTS) - .build(), - }; - - collider.set_collision_groups(InteractionGroups::new(Group::GROUP_2, Group::GROUP_1)); - - let rigid_body = self.rigid_body_set.insert(rigid_body); - let collider = self.collider_set.insert_with_parent( - collider, - rigid_body, - &mut self.rigid_body_set, - ); - - self.projectiles.insert( - collider.clone(), - PhysProjectile::new( - res.ct, - outfit.projectile.clone(), - rigid_body, - ship.data.get_faction(), - collider, - ), - ); - } - } - - /// Step this physics system by `t` seconds - pub fn step(&mut self, mut res: PhysStepResources) { - res.timing.start_physics_ships(); - self.step_ships(&mut res); - res.timing.mark_physics_ships(); - - res.timing.start_physics_sim(); - - // Update physics - res.wrapper - .step(res.t, &mut self.rigid_body_set, &mut self.collider_set); - - // Handle collision events - while let Ok(event) = &res.wrapper.collision_queue.try_recv() { - if event.started() { - let a = event.collider1(); - let b = event.collider2(); - - // If projectiles are part of this collision, make sure - // `a` is one of them. - let (a, b) = if self.projectiles.contains_key(&b) { - (b, a) - } else { - (a, b) - }; - - let p = self.projectiles.get(&a); - let s = self.ships.get_mut(&b); - if p.is_none() || s.is_none() { - continue; - } - self.collide_projectile_ship(&mut res, a, b); - } - } - - // Delete projectiles - let mut to_remove = Vec::new(); - for (c, p) in &mut self.projectiles { - p.tick(res.ct, res.t); - if p.is_expired() { - to_remove.push(*c); - } - } - - let mut rng = rand::thread_rng(); - for c in to_remove { - let (pr, p) = self.remove_projectile(&mut res, c).unwrap(); - - match &p.content.expire_effect { - None => {} - Some(x) => { - let x = res.ct.get_effect(*x); - let pos = *pr.translation(); - let vel = pr.velocity_at_point(pr.center_of_mass()); - let angle = pr.rotation().angle(); - - let velocity = { - let a = rng - .gen_range(-x.velocity_scale_parent_rng..=x.velocity_scale_parent_rng); - - let velocity = (x.velocity_scale_parent + a) * vel; - - velocity - }; - - res.particles.push(ParticleBuilder::from_content( - x, - pos.into(), - -angle, - velocity, - Vector2::new(0.0, 0.0), - )); - } - }; - } - - res.timing.mark_physics_sim(); - } -} diff --git a/crates/system/src/phys/systemsim/steputil.rs b/crates/system/src/phys/systemsim/steputil.rs deleted file mode 100644 index 1a22c28..0000000 --- a/crates/system/src/phys/systemsim/steputil.rs +++ /dev/null @@ -1,212 +0,0 @@ -use galactica_content::{Content, Relationship, SystemObjectHandle}; -use nalgebra::{Isometry2, Point2, Rotation2, Vector2}; -use rand::Rng; -use rapier2d::{ - dynamics::RigidBody, - geometry::{ColliderHandle, Group, InteractionGroups}, -}; - -use crate::{ - data::ShipState, - phys::{objects::PhysProjectile, ParticleBuilder, PhysStepResources}, -}; - -use super::PhysSim; - -// Private methods -impl PhysSim { - pub(super) fn remove_projectile( - &mut self, - res: &mut PhysStepResources, - c: ColliderHandle, - ) -> Option<(RigidBody, PhysProjectile)> { - let p = match self.projectiles.remove(&c) { - Some(p) => p, - None => return None, - }; - let r = self - .rigid_body_set - .remove( - p.rigid_body, - &mut res.wrapper.im, - &mut self.collider_set, - &mut res.wrapper.ij, - &mut res.wrapper.mj, - true, - ) - .unwrap(); - - return Some((r, p)); - } - - /// Try to land the given ship on the given planet. - /// State changes on success, and nothing happens on failure. - /// returns `true` if landing was successful. - pub(super) fn try_land_ship( - &mut self, - ct: &Content, - collider: ColliderHandle, - target_handle: SystemObjectHandle, - ) -> bool { - let ship = self.ships.get_mut(&collider).unwrap(); - let r = self.rigid_body_set.get(ship.rigid_body).unwrap(); - - let target = ct.get_system_object(target_handle); - let t_pos = Vector2::new(target.pos.x, target.pos.y); - let s_pos = Vector2::new(r.position().translation.x, r.position().translation.y); - - // TODO: deactivate collider when landing. - // Can't just set_active(false), since we still need that collider's mass. - - // We're in land range... - if (t_pos - s_pos).magnitude() > target.size / 2.0 { - return false; - } - - // And we'll stay in land range long enough. - if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0)).magnitude() - > target.size / 2.0 - { - return false; - } - - let collider = self.collider_set.get_mut(collider).unwrap(); - collider.set_collision_groups(InteractionGroups::new(Group::GROUP_1, Group::empty())); - ship.data.start_land_on(target_handle); - return true; - } - - /// Finish landing this ship on a planet. - /// Called after the landing animation finishes. - pub(super) fn finish_land_ship(&mut self, collider: ColliderHandle) { - let ship = self.ships.get_mut(&collider).unwrap(); - ship.data.finish_land_on(); - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.set_enabled(false); - r.set_angvel(0.0, false); - r.set_linvel(nalgebra::Vector2::new(0.0, 0.0), false); - } - - pub(super) fn start_unland_ship(&mut self, ct: &Content, collider: ColliderHandle) { - let ship = self.ships.get_mut(&collider).unwrap(); - let obj = ship.data.get_state().landed_on().unwrap(); - let obj = ct.get_system_object(obj); - - let mut rng = rand::thread_rng(); - let radius = rng.gen_range(500.0..=1500.0); - let angle = rng.gen_range(0.0..std::f32::consts::TAU); - let target_offset = Rotation2::new(angle) * Vector2::new(radius, 0.0); - let target_trans = Vector2::new(obj.pos.x, obj.pos.y) + target_offset; - let target_pos = Isometry2::new(target_trans, angle); - - ship.data.start_unland_to(ct, target_pos); - - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.set_enabled(true); - r.set_position( - Isometry2::new(Vector2::new(obj.pos.x, obj.pos.y), angle), - true, - ); - } - - pub(super) fn finish_unland_ship(&mut self, collider: ColliderHandle) { - let ship = self.ships.get_mut(&collider).unwrap(); - ship.data.finish_unland_to(); - self.collider_set - .get_mut(ship.collider) - .unwrap() - .set_collision_groups(InteractionGroups::new( - Group::GROUP_1, - Group::GROUP_1 | Group::GROUP_2, - )); - } - - pub(super) fn remove_ship( - &mut self, - res: &mut PhysStepResources, - colliderhandle: ColliderHandle, - ) { - let ship = match self.ships.get(&colliderhandle) { - None => return, - Some(s) => s, - }; - - self.rigid_body_set.remove( - ship.rigid_body, - &mut res.wrapper.im, - &mut self.collider_set, - &mut res.wrapper.ij, - &mut res.wrapper.mj, - true, - ); - self.ships.remove(&colliderhandle).unwrap(); - self.ship_behaviors.remove(&colliderhandle); - } - - pub(super) fn collide_projectile_ship( - &mut self, - res: &mut PhysStepResources, - projectile_h: ColliderHandle, - ship_h: ColliderHandle, - ) { - let projectile = self.projectiles.get(&projectile_h); - let ship = self.ships.get_mut(&ship_h); - if projectile.is_none() || ship.is_none() { - return; - } - let projectile = projectile.unwrap(); - let ship = ship.unwrap(); - - let f = res.ct.get_faction(projectile.faction); - let r = f.relationships.get(&ship.data.get_faction()).unwrap(); - let destory_projectile = match r { - Relationship::Hostile => match ship.data.get_state() { - ShipState::Flying { .. } => { - ship.data.apply_damage(projectile.content.damage); - true - } - ShipState::Collapsing { .. } => true, - _ => false, - }, - _ => false, - }; - - if destory_projectile { - let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); - let v = - pr.velocity_at_point(pr.center_of_mass()).normalize() * projectile.content.force; - let pos = *pr.translation(); - let _ = pr; - - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.apply_impulse_at_point(Vector2::new(v.x, v.y), Point2::new(pos.x, pos.y), true); - - // Borrow again, we can only have one at a time - let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); - let pos = *pr.translation(); - let angle = pr.rotation().angle(); - - match &projectile.content.impact_effect { - None => {} - Some(x) => { - let effect = res.ct.get_effect(*x); - let r = ship.rigid_body; - let sr = self.get_rigid_body(r).unwrap(); - let parent_velocity = pr.velocity_at_point(pr.center_of_mass()); - let target_velocity = - sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y)); - - res.particles.push(ParticleBuilder::from_content( - effect, - pos.into(), - -angle, - parent_velocity, - target_velocity, - )); - } - }; - - self.remove_projectile(res, projectile.collider); - } - } -} diff --git a/crates/system/src/phys/wrapper.rs b/crates/system/src/phys/wrapper.rs deleted file mode 100644 index 063070c..0000000 --- a/crates/system/src/phys/wrapper.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crossbeam::channel::Receiver; -use rapier2d::{ - dynamics::{ - CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, - RigidBodySet, - }, - geometry::{BroadPhase, ColliderSet, CollisionEvent, NarrowPhase}, - na::vector, - pipeline::{ChannelEventCollector, PhysicsPipeline}, -}; - -/// Wraps Rapier2d physics parameters -pub struct Wrapper { - pub(crate) ip: IntegrationParameters, - pub(crate) pp: PhysicsPipeline, - pub(crate) im: IslandManager, - pub(crate) bp: BroadPhase, - pub(crate) np: NarrowPhase, - pub(crate) ij: ImpulseJointSet, - pub(crate) mj: MultibodyJointSet, - pub(crate) ccd: CCDSolver, - - collision_handler: ChannelEventCollector, - - /// Collision event queue - /// this should be emptied after every frame. - pub collision_queue: Receiver, -} - -impl Wrapper { - /// Make a new physics wrapper - pub fn new() -> Self { - let (collision_send, collision_queue) = crossbeam::channel::unbounded(); - let (contact_force_send, _) = crossbeam::channel::unbounded(); - - Self { - ip: IntegrationParameters::default(), - pp: PhysicsPipeline::new(), - im: IslandManager::new(), - bp: BroadPhase::new(), - np: NarrowPhase::new(), - ij: ImpulseJointSet::new(), - mj: MultibodyJointSet::new(), - ccd: CCDSolver::new(), - collision_queue, - collision_handler: ChannelEventCollector::new(collision_send, contact_force_send), - } - } - - /// Step physics sim by `t` seconds - pub fn step( - &mut self, - t: f32, - rigid_body_set: &mut RigidBodySet, - collider_set: &mut ColliderSet, - ) { - self.ip.dt = t; - self.pp.step( - &vector![0.0, 0.0], - &self.ip, - &mut self.im, - &mut self.bp, - &mut self.np, - rigid_body_set, - collider_set, - &mut self.ij, - &mut self.mj, - &mut self.ccd, - None, - &(), - &mut self.collision_handler, - ); - } -}