Added ship collapse sequence

master
Mark 2024-01-05 18:04:30 -08:00
parent 46313b4880
commit 27c8bf6093
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
12 changed files with 416 additions and 45 deletions

2
assets

@ -1 +1 @@
Subproject commit 38fd6766762ce90bb699f98e30e46b17c4eda50c
Subproject commit 74ddbde9e1cec1418c17844bc7336324ece88d15

View File

@ -2,13 +2,19 @@
sprite = "particle::explosion::small"
lifetime = "inherit"
inherit_velocity = "target"
size = 3.0
size = 8.0
[effect."large explosion"]
sprite = "particle::explosion::large"
lifetime = "inherit"
inherit_velocity = "target"
size = 25.0
[effect."huge explosion"]
sprite = "particle::explosion::huge"
lifetime = "inherit"
inherit_velocity = "target"
size = 3.0
size = 50.0
[effect."blaster expire"]
sprite = "particle::blaster"
@ -25,5 +31,4 @@ size = 3.0
# effect probabilities & variants
# multiple particles in one effect
# fade
# better physics
# document: effect vs particle

View File

@ -6,6 +6,9 @@ hull = 200
linear_drag = 0.2
angular_drag = 0.2
# TODO: disable
# TODO: damage effects
space.outfit = 200
space.engine = 50
space.weapon = 50
@ -13,6 +16,19 @@ space.weapon = 50
engines = [{ x = 0.0, y = -1.05, size = 50.0 }]
guns = [{ x = 0.0, y = 1 }, { x = 0.1, y = 0.80 }, { x = -0.1, y = 0.80 }]
# Length of death sequence, in seconds
collapse.length = 5.0
# Effects to create during the collapse sequence.
# On average, `count` will be spawned over the sequence,
# with a distribution of (x^2 + 0.1)
collapse.effects = [
{ effect = "small explosion", count = 30 },
{ effect = "large explosion", count = 5 },
]
collision = [
#[rustfmt:skip],
[0.53921, 1.0000],
@ -37,3 +53,44 @@ collision = [
[-0.53921, 0.29343],
[-0.53921, 1.0000],
]
# Scripted explosion
[[ship."Gypsum".collapse.event]]
time = 5.0
effects = [
#[rustfmt:skip],
{ effect = "small explosion", count = 8 },
{ effect = "large explosion", count = 5 },
{ effect = "huge explosion", count = 1, pos = [0, 0] },
{ effect = "huge explosion", count = 4 },
]
# Scripted explosion
[[ship."Gypsum".collapse.event]]
time = 0.0
effects = [
#[rustfmt:skip],
{ effect = "small explosion", count = 3 },
{ effect = "large explosion", count = 1 },
]
# Play a sprite reel (or change sprite)
#[[ship."Gypsum".death.collapse.event]]
#time = 10.0
#reel = "gypsum post-death"
# Create debris
#[[ship."Gypsum".death.collapse.event]]
#time = "end"
#physics = "inherit"
# OR (relative to original rigidbody)
#physics.position = [0, 0]
#physics.velocity = [0, 0]
#physics.angle = 0
#physics.angvel = 0
#debris = "debris"
# Burning, interactable, destructible debris
#[debris]
#effects = [{ type = "small explosion", count = 10 }]

View File

@ -21,10 +21,7 @@ use walkdir::WalkDir;
pub use handle::{
EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle,
};
pub use part::{
Effect, EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace,
Projectile, ProjectileCollider, Relationship, RepeatMode, Ship, Sprite, System,
};
pub use part::*;
mod syntax {
use anyhow::{bail, Context, Result};

View File

@ -48,8 +48,10 @@ pub enum ProjectileCollider {
Ball(BallCollider),
}
/// A simple ball-shaped collider, centered at the object's position
#[derive(Debug, Deserialize, Clone)]
pub struct BallCollider {
/// The radius of this ball
pub radius: f32,
}

View File

@ -1,19 +1,22 @@
//! Content parts
pub mod effect;
pub mod faction;
pub mod gun;
pub mod outfit;
pub mod outfitspace;
pub mod ship;
pub mod sprite;
pub mod system;
pub(crate) mod effect;
pub(crate) mod faction;
pub(crate) mod gun;
pub(crate) mod outfit;
pub(crate) mod outfitspace;
pub(crate) mod ship;
pub(crate) mod sprite;
pub(crate) mod system;
pub use effect::{Effect, ImpactInheritVelocity};
pub use faction::{Faction, Relationship};
pub use gun::{Gun, Projectile, ProjectileCollider};
pub use outfit::Outfit;
pub use outfitspace::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship};
pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
ShipCollapse,
};
pub use sprite::{RepeatMode, Sprite};
pub use system::{Object, System};

View File

@ -1,13 +1,13 @@
use std::collections::HashMap;
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use cgmath::Point2;
use nalgebra::{point, Point};
use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitSpace};
use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace};
pub(crate) mod syntax {
use crate::part::outfitspace;
use crate::part::{effect::syntax::EffectReference, outfitspace};
use serde::Deserialize;
// Raw serde syntax structs.
@ -25,6 +25,7 @@ pub(crate) mod syntax {
pub angular_drag: f32,
pub linear_drag: f32,
pub space: outfitspace::syntax::OutfitSpace,
pub collapse: Option<Collapse>,
}
#[derive(Debug, Deserialize)]
@ -39,6 +40,34 @@ pub(crate) mod syntax {
pub x: f32,
pub y: f32,
}
// TODO:
// plural or not? document!
#[derive(Debug, Deserialize)]
pub struct Collapse {
pub length: f32,
pub effects: Vec<CollapseEffectSpawner>,
pub event: Vec<CollapseEvent>,
}
#[derive(Debug, Deserialize)]
pub struct CollapseEffectSpawner {
pub effect: EffectReference,
pub count: f32,
pub pos: Option<[f32; 2]>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CollapseEvent {
Effect(EffectCollapseEvent),
}
#[derive(Debug, Deserialize)]
pub struct EffectCollapseEvent {
pub time: f32,
pub effects: Vec<CollapseEffectSpawner>,
}
}
// Processed data structs.
@ -86,6 +115,9 @@ pub struct Ship {
/// Outfit space in this ship
pub space: OutfitSpace,
/// Ship collapse sequence
pub collapse: ShipCollapse,
}
/// Collision shape for this ship
@ -116,16 +148,60 @@ pub struct GunPoint {
pub pos: Point2<f32>,
}
/// Parameters for a ship's collapse sequence
#[derive(Debug, Clone)]
pub struct ShipCollapse {
/// Collapse sequence length, in seconds
pub length: f32,
/// Effects to create during collapse
pub effects: Vec<CollapseEffectSpawner>,
/// Scripted events during ship collapse
pub events: Vec<CollapseEvent>,
}
/// A scripted event during a ship collapse sequence
#[derive(Debug, Clone)]
pub struct CollapseEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
/// How many effects to create
pub count: f32,
/// Where to create these effects.
/// Position is random if None.
pub pos: Option<Point2<f32>>,
}
/// A scripted event during a ship collapse sequence
#[derive(Debug, Clone)]
pub enum CollapseEvent {
/// A scripted effect during a ship collapse sequence
Effect(EffectCollapseEvent),
}
/// A scripted effect during a ship collapse sequence
#[derive(Debug, Clone)]
pub struct EffectCollapseEvent {
/// When to trigger this event
pub time: f32,
/// The effect to create
pub effects: Vec<CollapseEffectSpawner>,
}
impl crate::Build for Ship {
type InputSyntaxType = HashMap<String, syntax::Ship>;
fn build(
ship: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext,
content: &mut Content,
build_context: &mut ContentBuildContext,
ct: &mut Content,
) -> Result<()> {
for (ship_name, ship) in ship {
let handle = match content.sprite_index.get(&ship.sprite) {
let handle = match ct.sprite_index.get(&ship.sprite) {
None => bail!(
"In ship `{}`: sprite `{}` doesn't exist",
ship_name,
@ -135,10 +211,67 @@ impl crate::Build for Ship {
};
let size = ship.size;
let aspect = content.get_sprite(handle).aspect;
let aspect = ct.get_sprite(handle).aspect;
content.ships.push(Self {
let collapse = if let Some(c) = ship.collapse {
let mut effects = Vec::new();
for e in c.effects {
effects.push(CollapseEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.with_context(|| format!("while loading ship `{}`", ship_name))?,
count: e.count,
pos: e.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect,
y: p[1] * size / 2.0,
}),
});
}
let mut events = Vec::new();
for e in c.event {
match e {
syntax::CollapseEvent::Effect(e) => {
let mut effects = Vec::new();
for g in e.effects {
effects.push(CollapseEffectSpawner {
effect: g.effect.to_handle(build_context, ct).with_context(
|| format!("while loading ship `{}`", ship_name),
)?,
count: g.count,
pos: g.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect,
y: p[1] * size / 2.0,
}),
})
}
events.push(CollapseEvent::Effect(EffectCollapseEvent {
time: e.time,
effects,
}))
}
}
}
ShipCollapse {
length: c.length,
effects,
events,
}
} else {
// Default collapse sequence
ShipCollapse {
length: 0.0,
effects: vec![],
events: vec![],
}
};
ct.ships.push(Self {
aspect,
collapse,
name: ship_name,
sprite: handle,
mass: ship.mass,

View File

@ -41,6 +41,7 @@ impl Game {
o1.add(&ct, content::OutfitHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 });
let s = object::Ship::new(
&ct,
@ -57,14 +58,14 @@ impl Game {
// This method of specifying factions is non-deterministic,
// but that's ok since this is for debug.
// TODO: fix
content::FactionHandle { index: 0 },
content::FactionHandle { index: 1 },
object::OutfitSet::new(ss),
);
let h2 = physics.add_ship(&ct, s, Point2 { x: 300.0, y: 300.0 });
let mut o1 = object::OutfitSet::new(ss);
o1.add(&ct, content::OutfitHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 });
//o1.add_gun(&ct, content::GunHandle { index: 0 });
let s = object::Ship::new(
&ct,

View File

@ -30,8 +30,7 @@ impl Ship {
}
}
/// Has this ship been destroyed?
pub fn is_destroyed(&self) -> bool {
pub fn is_dead(&self) -> bool {
self.hull <= 0.0
}
@ -42,8 +41,10 @@ impl Ship {
let r = f.relationships.get(&p.faction).unwrap();
match r {
content::Relationship::Hostile => {
// TODO: implement death and spawning, and enable damage
//s.hull -= p.damage;
if self.is_dead() {
return true;
}
self.hull -= p.content.damage;
return true;
}
_ => return false,
@ -51,6 +52,10 @@ impl Ship {
}
pub fn fire_guns(&mut self) -> Vec<(Projectile, content::GunPoint)> {
if self.is_dead() {
return vec![];
}
self.outfits
.iter_guns_points()
.filter(|(g, _)| g.cooldown <= 0.0)

View File

@ -2,6 +2,7 @@ use anyhow::Result;
use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3};
use galactica_constants;
use rand::seq::SliceRandom;
use std::{iter, rc::Rc};
use wgpu;
use winit::{self, dpi::LogicalSize, window::Window};
@ -624,6 +625,7 @@ impl GPUState {
);
// Write all new particles to GPU buffer
state.new_particles.shuffle(&mut rand::thread_rng());
for i in state.new_particles.iter() {
self.queue.write_buffer(
&self.vertex_buffers.particle.instances,

View File

@ -1,12 +1,13 @@
use cgmath::{Deg, InnerSpace, Vector2};
use nalgebra::vector;
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2, Zero};
use nalgebra::{point, vector};
use rapier2d::dynamics::RigidBody;
use rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider};
use crate::{util, ShipPhysicsHandle};
use galactica_content as content;
use galactica_gameobject as object;
use galactica_render::ObjectSprite;
use galactica_render::{ObjectSprite, ParticleBuilder};
pub struct ShipControls {
pub left: bool,
@ -26,6 +27,141 @@ impl ShipControls {
}
}
struct ShipCollapseSequence {
total_length: f32,
time_elapsed: f32,
rng: ThreadRng,
}
impl ShipCollapseSequence {
fn new(total_length: f32) -> Self {
Self {
total_length: total_length,
time_elapsed: 0.0,
rng: rand::thread_rng(),
}
}
fn is_done(&self) -> bool {
self.time_elapsed >= self.total_length
}
fn random_in_ship(
&mut self,
ship_content: &content::Ship,
collider: &Collider,
) -> Vector2<f32> {
// 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 {
y = self.rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
x = self.rng.gen_range(-1.0..=1.0) * ship_content.size * ship_content.sprite.aspect
/ 2.0;
a = collider.shape().contains_local_point(&point![x, y]);
}
Vector2 { x, y }
}
fn step(
&mut self,
ship: &object::Ship,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
rigid_body: &mut RigidBody,
collider: &mut Collider,
t: f32,
) {
let h = ship.handle;
let ship_content = ct.get_ship(h);
let ship_pos = util::rigidbody_position(rigid_body);
let ship_rot = util::rigidbody_rotation(rigid_body);
let ship_ang = ship_rot.angle(Vector2 { x: 1.0, y: 0.0 });
// The fraction of this collapse sequence that has been played
let frac_done = self.time_elapsed / self.total_length;
// Trigger collapse events
for event in &ship_content.collapse.events {
match event {
content::CollapseEvent::Effect(event) => {
if (event.time > self.time_elapsed && event.time <= self.time_elapsed + t)
|| (event.time == 0.0 && self.time_elapsed == 0.0)
{
for spawner in &event.effects {
let effect = ct.get_effect(spawner.effect);
for _ in 0..spawner.count as usize {
let pos = if let Some(pos) = spawner.pos {
pos.to_vec()
} else {
self.random_in_ship(ship_content, collider)
};
// Position, adjusted for ship rotation
let pos =
Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos;
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos: ship_pos + pos,
velocity: Vector2::zero(),
angle: Deg::zero(),
lifetime: effect.lifetime,
size: effect.size,
});
}
}
}
}
}
}
// Create collapse effects
for spawner in &ship_content.collapse.effects {
let effect = 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| {
let f = 0.2;
let y = if x < (1.0 - f) {
let x = x / (1.0 - f);
(x * x + 0.2) * 1.8 - f
} else {
1.0
};
return y;
};
let p_add = (t / self.total_length) * pdf(frac_done) * spawner.count;
if self.rng.gen_range(0.0..=1.0) <= p_add {
let pos = if let Some(pos) = spawner.pos {
pos.to_vec()
} else {
self.random_in_ship(ship_content, collider)
};
// Position, adjusted for ship rotation
let pos = Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos;
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos: ship_pos + pos,
velocity: Vector2::zero(),
angle: Deg::zero(),
lifetime: effect.lifetime,
size: effect.size,
});
}
}
self.time_elapsed += t;
}
}
/// A ship instance in the physics system
pub struct ShipWorldObject {
/// This ship's physics handle
@ -36,20 +172,46 @@ pub struct ShipWorldObject {
/// This ship's controls
pub controls: ShipControls,
collapse_sequence: ShipCollapseSequence,
}
impl ShipWorldObject {
/// Make a new ship
pub fn new(ship: object::Ship, physics_handle: ShipPhysicsHandle) -> Self {
pub fn new(
ct: &content::Content,
ship: object::Ship,
physics_handle: ShipPhysicsHandle,
) -> Self {
let ship_content = ct.get_ship(ship.handle);
ShipWorldObject {
physics_handle,
ship,
controls: ShipControls::new(),
collapse_sequence: ShipCollapseSequence::new(ship_content.collapse.length),
}
}
/// Should this ship should be removed from the world?
pub fn remove_from_world(&self) -> bool {
return self.ship.is_dead() && self.collapse_sequence.is_done();
}
/// Step this ship's state by t seconds
pub fn step(&mut self, r: &mut RigidBody, t: f32) {
pub fn step(
&mut self,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
r: &mut RigidBody,
c: &mut Collider,
t: f32,
) {
if self.ship.is_dead() {
return self
.collapse_sequence
.step(&self.ship, ct, particles, r, c, t);
}
let ship_rot = util::rigidbody_rotation(r);
let engine_force = ship_rot * t;

View File

@ -253,7 +253,8 @@ impl<'a> World {
);
let h = ShipPhysicsHandle(r, c);
self.ships.insert(c, objects::ShipWorldObject::new(ship, h));
self.ships
.insert(c, objects::ShipWorldObject::new(ct, ship, h));
return h;
}
@ -264,16 +265,19 @@ impl<'a> World {
let mut projectiles = Vec::new();
let mut to_remove = Vec::new();
for (_, s) in &mut self.ships {
if s.ship.is_destroyed() {
to_remove.push(s.physics_handle);
continue;
}
let r = &mut self.wrapper.rigid_body_set[s.physics_handle.0];
s.step(r, t);
let c = &mut self.wrapper.collider_set[s.physics_handle.1];
// TODO: unified step info struct
s.step(ct, particles, r, c, t);
if s.controls.guns {
projectiles.push((s.physics_handle, s.ship.fire_guns()));
}
if s.remove_from_world() {
to_remove.push(s.physics_handle);
continue;
}
}
for (s, p) in projectiles {
self.add_projectiles(s, p);