Reorganized system sim

master
Mark 2024-01-23 15:52:43 -08:00
parent e24c0edb8f
commit 51d9aa3911
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
22 changed files with 1398 additions and 1269 deletions

View File

@ -1,13 +1,12 @@
//! This module provides a physics-based simulation of one system. //! This module provides a physics-based simulation of one system.
pub mod controller;
pub mod objects; pub mod objects;
mod particlebuilder; pub mod physimage;
mod physsim;
mod physwrapper;
mod stepresources; 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 stepresources::*;
pub use systemsim::{PhysSim, PhysSimShipHandle};
pub use wrapper::Wrapper;

View File

@ -1,45 +1,46 @@
use galactica_content::{Effect, SpriteHandle}; use galactica_content::{Content, EffectHandle, SpriteAutomaton};
use nalgebra::{Point2, Rotation2, Vector2}; use nalgebra::{Rotation2, Vector2};
use rand::Rng; use rand::Rng;
use rapier2d::dynamics::{RigidBodyBuilder, RigidBodyHandle, RigidBodyType};
use crate::phys::{PhysStepResources, PhysWrapper};
/// Instructions to create a new particle /// Instructions to create a new particle
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct ParticleBuilder { pub struct PhysEffect {
/// The sprite to use for this particle /// The sprite to use for this particle
pub sprite: SpriteHandle, pub anim: SpriteAutomaton,
/// This object's center, in world coordinates.
pub pos: Point2<f32>,
/// This particle's velocity, in world coordinates /// This particle's velocity, in world coordinates
pub velocity: Vector2<f32>, pub rigid_body: RigidBodyHandle,
/// This particle's angle, in radians
pub angle: f32,
/// This particle's angular velocity (rad/sec)
pub angvel: f32,
/// This particle's lifetime, in seconds /// This particle's lifetime, in seconds
pub lifetime: f32, lifetime: f32,
/// The size of this particle, /// The size of this particle,
/// given as height in world units. /// given as height in world units.
pub size: f32, 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, pub fade: f32,
/// If true, this effect has been destroyed,
/// and needs to be removed from the game.
is_destroyed: bool,
} }
impl ParticleBuilder { impl PhysEffect {
/// Create a ParticleBuilder from an Effect /// Create a new particle inside `Wrapper`
pub fn from_content( pub fn new(
effect: &Effect, ct: &Content,
pos: Point2<f32>, wrapper: &mut PhysWrapper,
effect: EffectHandle,
pos: Vector2<f32>,
parent_angle: f32, parent_angle: f32,
parent_velocity: Vector2<f32>, parent_velocity: Vector2<f32>,
target_velocity: Vector2<f32>, target_velocity: Vector2<f32>,
) -> Self { ) -> Self {
let effect = ct.get_effect(effect);
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let velocity = { let velocity = {
@ -65,13 +66,17 @@ impl ParticleBuilder {
parent_angle + effect.angle + rng.gen_range(-effect.angle_rng..=effect.angle_rng) parent_angle + effect.angle + rng.gen_range(-effect.angle_rng..=effect.angle_rng)
}; };
ParticleBuilder { println!("{:?}", effect.angvel_rng);
sprite: effect.sprite,
pos,
velocity,
angle, let rb = RigidBodyBuilder::new(RigidBodyType::KinematicVelocityBased)
angvel, .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 lifetime: 0f32
.max(effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng)), .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)), 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)), 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
}
} }

View File

@ -1,7 +1,9 @@
//! This module contains game objects that may interact with the physics engine. //! This module contains game objects that may interact with the physics engine.
mod collapse;
mod effect;
mod projectile; mod projectile;
mod ship; mod ship;
pub use effect::*;
pub use projectile::PhysProjectile; pub use projectile::PhysProjectile;
pub use ship::{PhysSimShip, ShipControls}; pub use ship::*;

View File

@ -1,7 +1,12 @@
use galactica_content::{AnimationState, Content, FactionHandle, Projectile, SpriteAutomaton}; use galactica_content::{AnimationState, Content, FactionHandle, Projectile, SpriteAutomaton};
use nalgebra::Vector2;
use rand::Rng; use rand::Rng;
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle}; use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
use crate::phys::{physsim::NewObjects, PhysStepResources, PhysWrapper};
use super::PhysEffect;
/// A single projectile in this simulation /// A single projectile in this simulation
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PhysProjectile { pub struct PhysProjectile {
@ -12,7 +17,7 @@ pub struct PhysProjectile {
anim: SpriteAutomaton, anim: SpriteAutomaton,
/// The remaining lifetime of this projectile, in seconds /// The remaining lifetime of this projectile, in seconds
pub lifetime: f32, lifetime: f32,
/// The faction this projectile belongs to /// The faction this projectile belongs to
pub faction: FactionHandle, pub faction: FactionHandle,
@ -25,6 +30,10 @@ pub struct PhysProjectile {
/// This projectile's size variation /// This projectile's size variation
pub size_rng: f32, pub size_rng: f32,
/// If true, this projectile has been destroyed
/// and is waiting to be deallocated.
is_destroyed: bool,
} }
impl PhysProjectile { impl PhysProjectile {
@ -47,18 +56,88 @@ impl PhysProjectile {
lifetime, lifetime,
faction, faction,
size_rng: rng.gen_range(-size_rng..=size_rng), size_rng: rng.gen_range(-size_rng..=size_rng),
is_destroyed: false,
} }
} }
/// Process this projectile's state after `t` seconds /// Process this projectile's state after `t` seconds
pub fn tick(&mut self, ct: &Content, t: f32) { pub(in crate::phys) fn step(
self.lifetime -= t; &mut self,
self.anim.step(ct, t) 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? /// Destroy this projectile without creating an expire effect
pub fn is_expired(&self) -> bool { pub(in crate::phys) fn destroy_silent(
return self.lifetime < 0.0; &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
} }
} }

View File

@ -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<ShipCollapseSequence>,
}
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<OutfitHandle> {
// 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<Item = OutfitHandle>) {
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<Item = &(EnginePoint, SpriteAutomaton)> {
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
}
}

View File

@ -1,13 +1,7 @@
use std::collections::HashMap;
use galactica_util::{clockwise_angle, to_radians}; use galactica_util::{clockwise_angle, to_radians};
use nalgebra::Vector2; use nalgebra::Vector2;
use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle};
use crate::phys::{ use crate::phys::{objects::ShipControls, PhysImage, PhysSimShipHandle, PhysStepResources};
objects::{PhysSimShip, ShipControls},
PhysStepResources,
};
// TODO: no wobble // TODO: no wobble
// TODO: slow down when near planet // TODO: slow down when near planet
@ -16,13 +10,11 @@ use crate::phys::{
/// Land this ship on the given object /// Land this ship on the given object
pub fn auto_landing( pub fn auto_landing(
_res: &PhysStepResources, _res: &PhysStepResources,
rigid_bodies: &RigidBodySet, img: &PhysImage,
ships: &HashMap<ColliderHandle, PhysSimShip>, this_ship: PhysSimShipHandle,
this_ship: ColliderHandle,
target_pos: Vector2<f32>, target_pos: Vector2<f32>,
) -> Option<ShipControls> { ) -> Option<ShipControls> {
let rigid_body_handle = ships.get(&this_ship).unwrap().rigid_body; let rigid_body = &img.get_ship(&this_ship).unwrap().rigidbody;
let rigid_body = rigid_bodies.get(rigid_body_handle).unwrap();
let my_pos = *rigid_body.translation(); let my_pos = *rigid_body.translation();
let my_rot = rigid_body.rotation() * Vector2::new(1.0, 0.0); let my_rot = rigid_body.rotation() * Vector2::new(1.0, 0.0);
let my_vel = rigid_body.linvel(); let my_vel = rigid_body.linvel();

View File

@ -1,10 +1,15 @@
use galactica_content::{CollapseEvent, Content, Ship}; use galactica_content::{CollapseEvent, Content, Ship};
use nalgebra::{Point2, Vector2}; use nalgebra::{Point2, Vector2};
use rand::{rngs::ThreadRng, Rng}; 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::{
use crate::data::ShipData; data::ShipData,
phys::{objects::PhysEffect, physsim::NewObjects, PhysStepResources, PhysWrapper},
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct ShipCollapseSequence { pub(super) struct ShipCollapseSequence {
@ -49,10 +54,14 @@ impl ShipCollapseSequence {
pub(super) fn step( pub(super) fn step(
&mut self, &mut self,
res: &mut PhysStepResources, res: &mut PhysStepResources,
wrapper: &mut PhysWrapper,
new: &mut NewObjects,
ship_data: &ShipData, ship_data: &ShipData,
rigid_body: &mut RigidBody, rigid_body_handle: RigidBodyHandle,
collider: &mut Collider, 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_content = res.ct.get_ship(ship_data.get_content());
let ship_pos = rigid_body.translation(); let ship_pos = rigid_body.translation();
let ship_rot = rigid_body.rotation(); let ship_rot = rigid_body.rotation();
@ -71,26 +80,26 @@ impl ShipCollapseSequence {
// ^^ Don't miss events scheduled at the very start of the sequence! // ^^ Don't miss events scheduled at the very start of the sequence!
{ {
for spawner in &event.effects { for spawner in &event.effects {
let effect = res.ct.get_effect(spawner.effect);
for _ in 0..spawner.count as usize { for _ in 0..spawner.count as usize {
let pos: Vector2<f32> = if let Some(pos) = spawner.pos { let pos: Vector2<f32> = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y) Vector2::new(pos.x, pos.y)
} else { } 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 pos = ship_pos + (ship_rot * pos);
let velocity = let velocity =
rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y)); rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y));
res.particles.push(ParticleBuilder::from_content( new.effects.push(PhysEffect::new(
effect, res.ct,
pos.into(), wrapper,
spawner.effect,
pos,
0.0, 0.0,
velocity, velocity,
Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0),
)) ));
} }
} }
} }
@ -100,8 +109,6 @@ impl ShipCollapseSequence {
// Create collapse effects // Create collapse effects
for spawner in &ship_content.collapse.effects { for spawner in &ship_content.collapse.effects {
let effect = res.ct.get_effect(spawner.effect);
// Probability of adding a particle this frame. // Probability of adding a particle this frame.
// The area of this function over [0, 1] should be 1. // The area of this function over [0, 1] should be 1.
let pdf = |x: f32| { let pdf = |x: f32| {
@ -121,23 +128,22 @@ impl ShipCollapseSequence {
let pos = if let Some(pos) = spawner.pos { let pos = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y) Vector2::new(pos.x, pos.y)
} else { } else {
self.random_in_ship(res.ct, ship_content, collider) self.random_in_ship(res.ct, ship_content, &collider)
}; };
// Position, adjusted for ship rotation // Position, adjusted for ship rotation
let pos = ship_pos + (ship_rot * pos); let pos = ship_pos + (ship_rot * pos);
let vel = rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y)); let vel = rigid_body.velocity_at_point(&Point2::new(pos.x, pos.y));
res.particles.push(ParticleBuilder { new.effects.push(PhysEffect::new(
sprite: effect.sprite, res.ct,
pos: pos.into(), wrapper,
velocity: vel, spawner.effect,
angle: 0.0, pos,
angvel: 0.0, 0.0,
lifetime: effect.lifetime, vel,
size: effect.size, Vector2::new(0.0, 0.0),
fade: 0.0, ));
});
} }
} }

View File

@ -1,19 +1,15 @@
//! Various implementations of [`crate::ShipBehavior`] //! Various implementations of [`crate::ShipBehavior`]
pub(crate) mod autopilot;
mod null; mod null;
mod point; mod point;
use null::*; use null::*;
use point::PointShipController; use point::PointShipController;
use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; use std::fmt::Debug;
use std::{collections::HashMap, fmt::Debug};
use super::{ use super::ShipControls;
objects::{PhysSimShip, ShipControls}, use crate::phys::{PhysImage, PhysSimShipHandle, PhysStepResources};
PhysStepResources,
};
/// Represents a ship controller /// Represents a ship controller
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -40,13 +36,12 @@ impl ShipController {
pub fn update_controls( pub fn update_controls(
&mut self, &mut self,
res: &PhysStepResources, res: &PhysStepResources,
rigid_bodies: &RigidBodySet, img: &PhysImage,
ships: &HashMap<ColliderHandle, PhysSimShip>, this_ship: PhysSimShipHandle,
this_ship: ColliderHandle,
) -> Option<ShipControls> { ) -> Option<ShipControls> {
match self { match self {
Self::Null(n) => n.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, rigid_bodies, ships, this_ship), Self::Point(p) => p.update_controls(res, img, this_ship),
} }
} }
} }
@ -61,8 +56,7 @@ where
fn update_controls( fn update_controls(
&mut self, &mut self,
res: &PhysStepResources, res: &PhysStepResources,
rigid_bodies: &RigidBodySet, img: &PhysImage,
ships: &HashMap<ColliderHandle, PhysSimShip>, this_ship: PhysSimShipHandle,
this_ship: ColliderHandle,
) -> Option<ShipControls>; ) -> Option<ShipControls>;
} }

View File

@ -1,13 +1,5 @@
use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle}; use super::{ShipControllerStruct, ShipControls};
use std::collections::HashMap; use crate::phys::{PhysImage, PhysSimShipHandle, PhysStepResources};
use super::{
super::{
objects::{PhysSimShip, ShipControls},
PhysStepResources,
},
ShipControllerStruct,
};
/// The Null controller is assigned to objects that are static or not controlled by the computer. /// 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. /// Most notably, the player's ship has a Null controller.
@ -25,9 +17,8 @@ impl ShipControllerStruct for NullShipController {
fn update_controls( fn update_controls(
&mut self, &mut self,
_res: &PhysStepResources, _res: &PhysStepResources,
_rigid_bodies: &RigidBodySet, _img: &PhysImage,
_ships: &HashMap<ColliderHandle, PhysSimShip>, _this_ship: PhysSimShipHandle,
_this_ship: ColliderHandle,
) -> Option<ShipControls> { ) -> Option<ShipControls> {
None None
} }

View File

@ -1,19 +1,15 @@
use galactica_content::Relationship; use galactica_content::Relationship;
use galactica_util::clockwise_angle; use galactica_util::clockwise_angle;
use nalgebra::Vector2; use nalgebra::Vector2;
use rapier2d::{dynamics::RigidBodySet, geometry::ColliderHandle};
use std::collections::HashMap;
use crate::data::ShipState; use crate::{
data::ShipState,
use super::{ phys::{PhysImage, PhysSimShipHandle},
super::{
objects::{PhysSimShip, ShipControls},
PhysStepResources,
},
ShipControllerStruct,
}; };
use super::{ShipControllerStruct, ShipControls};
use crate::phys::PhysStepResources;
/// "Point" ship controller. /// "Point" ship controller.
/// Point and shoot towards the nearest enemy. /// Point and shoot towards the nearest enemy.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -30,43 +26,47 @@ impl ShipControllerStruct for PointShipController {
fn update_controls( fn update_controls(
&mut self, &mut self,
res: &PhysStepResources, res: &PhysStepResources,
rigid_bodies: &RigidBodySet, img: &PhysImage,
ships: &HashMap<ColliderHandle, PhysSimShip>, this_ship: PhysSimShipHandle,
this_ship: ColliderHandle,
) -> Option<ShipControls> { ) -> Option<ShipControls> {
let mut controls = ShipControls::new(); let mut controls = ShipControls::new();
let my_ship = ships.get(&this_ship).unwrap(); let my_ship = match img.get_ship(&this_ship) {
let this_rigidbody = rigid_bodies.get(my_ship.rigid_body).unwrap(); None => return None,
Some(s) => s,
};
let this_rigidbody = &my_ship.rigidbody;
let my_position = this_rigidbody.translation(); let my_position = this_rigidbody.translation();
let my_rotation = this_rigidbody.rotation(); let my_rotation = this_rigidbody.rotation();
let my_angvel = this_rigidbody.angvel(); 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 // Iterate all possible targets
let mut hostile_ships = ships let mut hostile_ships = img.iter_ships().filter(
.values() // Target only flying ships we're hostile towards
.filter( |s| match my_faction
// Target only flying ships we're hostile towards .relationships
|s| match my_faction.relationships.get(&s.data.get_faction()).unwrap() { .get(&s.ship.data.get_faction())
Relationship::Hostile => match s.data.get_state() { .unwrap()
ShipState::Flying { .. } => true, {
_ => false, Relationship::Hostile => match s.ship.data.get_state() {
}, ShipState::Flying { .. } => true,
_ => false, _ => false,
}, },
) _ => false,
.map(|s| rigid_bodies.get(s.rigid_body).unwrap()); },
);
// Find the closest target // Find the closest target
let mut closest_enemy_position = match hostile_ships.next() { 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 None => return Some(controls), // Do nothing if no targets are available
}; };
let mut d = (my_position - closest_enemy_position).magnitude(); let mut d = (my_position - closest_enemy_position).magnitude();
for r in hostile_ships { for s in hostile_ships {
let p = r.translation(); let p = s.rigidbody.translation();
let new_d = (my_position - p).magnitude(); let new_d = (my_position - p).magnitude();
if new_d < d { if new_d < d {
d = new_d; d = new_d;

View File

@ -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,
}
}
}

View File

@ -0,0 +1,8 @@
mod autopilot;
mod collapse;
mod controller;
mod controls;
mod ship;
pub use controls::ShipControls;
pub use ship::PhysShip;

View File

@ -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<ShipCollapseSequence>,
/// 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<OutfitHandle>)> = 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<OutfitHandle> {
// 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<Item = OutfitHandle>) {
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<Item = &(EnginePoint, SpriteAutomaton)> {
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
}
}

View File

@ -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<PhysShipImage>,
/// Map ship handles to indices in ships
pub(crate) ship_map: HashMap<PhysSimShipHandle, usize>,
/// The projectiles in this frame
pub(crate) projectiles: Vec<PhysProjectileImage>,
/// The effects in this frame
pub(crate) effects: Vec<PhysEffectImage>,
}
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<Item = &PhysShipImage> {
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<Item = &PhysProjectileImage> {
self.projectiles.iter()
}
/// Iterate effects in this image
pub fn iter_effects(&self) -> impl Iterator<Item = &PhysEffectImage> {
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,
}

View File

@ -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<PhysProjectile>,
pub effects: Vec<PhysEffect>,
}
/// 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<PhysEffect>,
projectiles: HashMap<ColliderHandle, PhysProjectile>,
ships: HashMap<ColliderHandle, PhysShip>,
}
// 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<f32>,
) -> 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<Item = &PhysShip> + '_ {
self.ships.values()
}
/// Iterate over all ships in this physics system
pub fn iter_projectiles(&self) -> impl Iterator<Item = &PhysProjectile> + '_ {
self.projectiles.values()
}
/// Iterate over all particles in this physics system
pub fn iter_particles(&self) -> impl Iterator<Item = &PhysEffect> + '_ {
self.effects.iter()
}
}

View File

@ -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<CollisionEvent>,
}
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<RigidBody> {
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)
}
}

View File

@ -1,4 +1,3 @@
use super::{wrapper::Wrapper, ParticleBuilder};
use galactica_content::Content; use galactica_content::Content;
use galactica_util::timing::Timing; use galactica_util::timing::Timing;
@ -10,12 +9,6 @@ pub struct PhysStepResources<'a> {
/// Length of time step /// Length of time step
pub t: f32, pub t: f32,
/// Particles to create
pub particles: &'a mut Vec<ParticleBuilder>,
/// Record detailed frame timing breakdown /// Record detailed frame timing breakdown
pub timing: &'a mut Timing, pub timing: &'a mut Timing,
/// Physics computer
pub wrapper: &'a mut Wrapper,
} }

View File

@ -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<ColliderHandle, PhysProjectile>,
ships: HashMap<ColliderHandle, PhysSimShip>,
ship_behaviors: HashMap<ColliderHandle, ShipController>,
}
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<f32>,
) -> 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<Item = (&PhysSimShip, &RigidBody)> + '_ {
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<Item = &PhysSimShip> + '_ {
self.ships.values()
}
/// Iterate over all ships in this physics system
pub fn iter_projectiles(&self) -> impl Iterator<Item = &PhysProjectile> + '_ {
self.projectiles.values()
}
}

View File

@ -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<OutfitHandle>)> = 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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<CollisionEvent>,
}
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,
);
}
}