Compare commits

...

2 Commits

Author SHA1 Message Date
Mark f05f2fbc45
Added animated particles 2024-01-03 07:46:27 -08:00
Mark a892e4e763
Improved particles 2024-01-03 06:37:02 -08:00
21 changed files with 431 additions and 186 deletions

View File

@ -1,6 +1,7 @@
## Specific Jobs ## Specific Jobs
- Finish particles - Define projectile colliders & particles
- Projectile colliders & particles - Particle variation
- Animated sprites
- UI: health, shield, fuel, heat, energy bars - UI: health, shield, fuel, heat, energy bars
- UI: text arranger - UI: text arranger
- Sound system - Sound system

View File

@ -10,7 +10,7 @@ rate_rng = 0.1
projectile.sprite_texture = "projectile::blaster" projectile.sprite_texture = "projectile::blaster"
# Height of projectile in game units # Height of projectile in game units
projectile.size = 10 projectile.size = 6
projectile.size_rng = 0.0 projectile.size_rng = 0.0
# Speed of projectile, in game units/second # Speed of projectile, in game units/second
projectile.speed = 300 projectile.speed = 300

View File

@ -1,38 +1,45 @@
[texture."starfield"] [texture."starfield"]
path = "starfield.png" file = "starfield.png"
[texture."star::star"] [texture."star::star"]
path = "star/B-09.png" file = "star/B-09.png"
[texture."flare::ion"] [texture."flare::ion"]
path = "flare/1.png" file = "flare/1.png"
[texture."planet::earth"] [texture."planet::earth"]
path = "planet/earth.png" file = "planet/earth.png"
[texture."planet::luna"] [texture."planet::luna"]
path = "planet/luna.png" file = "planet/luna.png"
[texture."projectile::blaster"] [texture."projectile::blaster"]
path = "projectile/blaster.png" file = "projectile/blaster.png"
[texture."ship::gypsum"] [texture."ship::gypsum"]
path = "ship/gypsum.png" file = "ship/gypsum.png"
[texture."ui::radar"] [texture."ui::radar"]
path = "ui/radar.png" file = "ui/radar.png"
[texture."ui::shipblip"] [texture."ui::shipblip"]
path = "ui/ship-blip.png" file = "ui/ship-blip.png"
[texture."ui::planetblip"] [texture."ui::planetblip"]
path = "ui/planet-blip.png" file = "ui/planet-blip.png"
[texture."ui::radarframe"] [texture."ui::radarframe"]
path = "ui/radarframe.png" file = "ui/radarframe.png"
[texture."ui::centerarrow"] [texture."ui::centerarrow"]
path = "ui/center-arrow.png" file = "ui/center-arrow.png"
[texture."particle::blaster"] [texture."particle::blaster"]
path = "particle/blaster-01.png" duration = 0.15
repeat = "once"
frames = [
"particle/blaster-01.png",
"particle/blaster-02.png",
"particle/blaster-03.png",
"particle/blaster-04.png",
]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "galactica-behavior" name = "galactica-behavior"
description = "AI behaviors for Galaictica" description = "AI behaviors for Galactica"
categories = { workspace = true } categories = { workspace = true }
keywords = { workspace = true } keywords = { workspace = true }
version = { workspace = true } version = { workspace = true }

View File

@ -26,7 +26,7 @@ impl ShipBehavior for Point {
s.controls.guns = false; s.controls.guns = false;
s.controls.thrust = false; s.controls.thrust = false;
let (my_s, my_r) = physics.get_ship_body(&self.handle).unwrap(); let (my_s, my_r) = physics.get_ship_body(self.handle).unwrap();
let my_position = util::rigidbody_position(my_r); let my_position = util::rigidbody_position(my_r);
let my_rotation = util::rigidbody_rotation(my_r); let my_rotation = util::rigidbody_rotation(my_r);
let my_angvel = my_r.angvel(); let my_angvel = my_r.angvel();

View File

@ -19,8 +19,8 @@ use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle}; pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
pub use part::{ pub use part::{
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, Ship, EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, RepeatMode,
System, Texture, Ship, System, Texture,
}; };
mod syntax { mod syntax {

View File

@ -14,4 +14,4 @@ pub use outfit::Outfit;
pub use shared::OutfitSpace; pub use shared::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship}; pub use ship::{EnginePoint, GunPoint, Ship};
pub use system::{Object, System}; pub use system::{Object, System};
pub use texture::Texture; pub use texture::{RepeatMode, Texture};

View File

@ -1,20 +1,60 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use image::io::Reader; use image::io::Reader;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::{handle::TextureHandle, Content}; use crate::{handle::TextureHandle, Content};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize;
use std::path::PathBuf; use std::path::PathBuf;
use serde::Deserialize; use super::RepeatMode;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Texture { #[serde(untagged)]
pub path: PathBuf, pub enum Texture {
Static(StaticTexture),
Frames(FramesTexture),
}
#[derive(Debug, Deserialize)]
pub struct StaticTexture {
pub file: PathBuf,
}
// TODO: loop mode
#[derive(Debug, Deserialize)]
pub struct FramesTexture {
pub frames: Vec<PathBuf>,
pub duration: f32,
pub repeat: RepeatMode,
}
}
/// How to replay a texture's animation
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum RepeatMode {
/// Play this animation once, and stop at the last frame
#[serde(rename = "once")]
Once,
/// After the first frame, jump to the last frame
#[serde(rename = "repeat")]
Repeat,
}
impl RepeatMode {
/// Represent this repeatmode as an integer
/// Used to pass this enum into shaders
pub fn as_int(&self) -> u32 {
match self {
Self::Once => 0,
Self::Repeat => 1,
}
} }
} }
@ -27,8 +67,16 @@ pub struct Texture {
/// The handle for this texture /// The handle for this texture
pub handle: TextureHandle, pub handle: TextureHandle,
/// The path to this texture's image file /// The frames of this texture
pub path: PathBuf, /// (static textures have one frame)
pub frames: Vec<PathBuf>,
/// The speed of this texture's animation
/// (static textures have zero fps)
pub fps: f32,
/// How to replay this texture's animation
pub repeat: RepeatMode,
} }
impl crate::Build for Texture { impl crate::Build for Texture {
@ -36,19 +84,21 @@ impl crate::Build for Texture {
fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> {
for (texture_name, t) in texture { for (texture_name, t) in texture {
let path = ct.texture_root.join(t.path); match t {
let reader = Reader::open(&path).with_context(|| { syntax::Texture::Static(t) => {
let file = ct.texture_root.join(t.file);
let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read texture `{}` from file `{}`", "Failed to read texture `{}` from file `{}`",
texture_name, texture_name,
path.display() file.display()
) )
})?; })?;
let dim = reader.into_dimensions().with_context(|| { let dim = reader.into_dimensions().with_context(|| {
format!( format!(
"Failed to dimensions of texture `{}` from file `{}`", "Failed to get dimensions of texture `{}` from file `{}`",
texture_name, texture_name,
path.display() file.display()
) )
})?; })?;
@ -67,12 +117,73 @@ impl crate::Build for Texture {
} }
ct.texture_index.insert(texture_name.clone(), h); ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self { ct.textures.push(Self {
name: texture_name, name: texture_name,
path, frames: vec![file],
fps: 0.0,
handle: h, handle: h,
repeat: RepeatMode::Once,
}); });
} }
syntax::Texture::Frames(t) => {
let mut dim = None;
for f in &t.frames {
let file = ct.texture_root.join(f);
let reader = Reader::open(&file).with_context(|| {
format!(
"Failed to read texture `{}` from file `{}`",
texture_name,
file.display()
)
})?;
let d = reader.into_dimensions().with_context(|| {
format!(
"Failed to get dimensions of texture `{}` from file `{}`",
texture_name,
file.display()
)
})?;
match dim {
None => dim = Some(d),
Some(e) => {
if d != e {
bail!(
"Failed to load frames of texture `{}`. Frames have different sizes `{}`",
texture_name,
file.display()
)
}
}
}
}
let h = TextureHandle {
index: ct.textures.len(),
aspect: dim.unwrap().0 as f32 / dim.unwrap().1 as f32,
};
if texture_name == ct.starfield_texture_name {
unreachable!("Starfield texture may not be animated")
}
let fps = t.duration / t.frames.len() as f32;
ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self {
name: texture_name,
frames: t
.frames
.into_iter()
.map(|f| ct.texture_root.join(f))
.collect(),
fps,
handle: h,
repeat: t.repeat,
});
}
}
}
if ct.starfield_handle.is_none() { if ct.starfield_handle.is_none() {
bail!( bail!(

View File

@ -10,29 +10,26 @@ use galactica_behavior::{behavior, ShipBehavior};
use galactica_constants; use galactica_constants;
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::{ObjectSprite, ParticleBuilder, UiSprite}; use galactica_render::{FrameState, ObjectSprite, ParticleBuilder, UiSprite};
use galactica_ui as ui; use galactica_ui as ui;
use galactica_world::{util, ShipPhysicsHandle, World}; use galactica_world::{util, ShipPhysicsHandle, World};
pub struct Game { pub struct Game {
pub input: InputStatus, input: InputStatus,
pub last_update: Instant, last_update: Instant,
pub player: ShipPhysicsHandle, player: ShipPhysicsHandle,
paused: bool, paused: bool,
pub time_scale: f32, time_scale: f32,
start_instant: Instant,
camera: Camera,
system: object::System,
shipbehaviors: Vec<Box<dyn ShipBehavior>>, shipbehaviors: Vec<Box<dyn ShipBehavior>>,
playerbehavior: behavior::Player, playerbehavior: behavior::Player,
content: content::Content, content: content::Content,
pub system: object::System,
pub camera: Camera,
world: World, world: World,
pub start_instant: Instant, new_particles: Vec<ParticleBuilder>,
// TODO: clean this up
pub new_particles: Vec<ParticleBuilder>,
} }
impl Game { impl Game {
@ -112,6 +109,10 @@ impl Game {
} }
} }
pub fn set_camera_aspect(&mut self, v: f32) {
self.camera.aspect = v
}
pub fn process_key(&mut self, state: &ElementState, key: &VirtualKeyCode) { pub fn process_key(&mut self, state: &ElementState, key: &VirtualKeyCode) {
self.input.process_key(state, key) self.input.process_key(state, key)
} }
@ -173,6 +174,17 @@ impl Game {
self.last_update = Instant::now(); self.last_update = Instant::now();
} }
pub fn get_frame_state(&mut self) -> FrameState {
FrameState {
camera_pos: self.camera.pos,
camera_zoom: self.camera.zoom,
object_sprites: self.get_object_sprites(),
ui_sprites: self.get_ui_sprites(),
new_particles: &mut self.new_particles,
current_time: self.start_instant.elapsed().as_secs_f32(),
}
}
pub fn get_object_sprites(&self) -> Vec<ObjectSprite> { pub fn get_object_sprites(&self) -> Vec<ObjectSprite> {
let mut sprites: Vec<ObjectSprite> = Vec::new(); let mut sprites: Vec<ObjectSprite> = Vec::new();
@ -195,7 +207,7 @@ impl Game {
pub fn get_ui_sprites(&self) -> Vec<UiSprite> { pub fn get_ui_sprites(&self) -> Vec<UiSprite> {
return ui::build_radar( return ui::build_radar(
&self.content, &self.content,
&self.player, self.player,
&self.world, &self.world,
&self.system, &self.system,
self.camera.zoom, self.camera.zoom,

View File

@ -27,22 +27,12 @@ fn main() -> Result<()> {
let mut game = game::Game::new(content); let mut game = game::Game::new(content);
gpu.update_starfield_buffer(); gpu.update_starfield_buffer();
game.camera.aspect = gpu.window_size.width as f32 / gpu.window_size.height as f32; game.set_camera_aspect(gpu.window_size.width as f32 / gpu.window_size.height as f32);
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
match event { match event {
Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => {
match gpu.render( match gpu.render(game.get_frame_state()) {
game.camera.pos,
game.camera.zoom,
&game.get_object_sprites(),
&game.get_ui_sprites(),
// TODO: clean this up, single game data struct
// Game in another crate?
// Shipbehavior needs game state too...
&mut game.new_particles,
game.start_instant,
) {
Ok(_) => {} Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(), Err(wgpu::SurfaceError::Lost) => gpu.resize(),
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
@ -81,13 +71,15 @@ fn main() -> Result<()> {
} }
WindowEvent::Resized(_) => { WindowEvent::Resized(_) => {
gpu.resize(); gpu.resize();
game.camera.aspect = game.set_camera_aspect(
gpu.window_size.width as f32 / gpu.window_size.height as f32; gpu.window_size.width as f32 / gpu.window_size.height as f32,
);
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
gpu.resize(); gpu.resize();
game.camera.aspect = game.set_camera_aspect(
gpu.window_size.width as f32 / gpu.window_size.height as f32; gpu.window_size.width as f32 / gpu.window_size.height as f32,
);
} }
_ => {} _ => {}
}, },

View File

@ -72,8 +72,7 @@ impl OutfitStatSum {
#[derive(Debug)] #[derive(Debug)]
pub struct OutfitSet { pub struct OutfitSet {
/// The total sum of the stats this set of outfits provides /// The total sum of the stats this set of outfits provides
/// TODO: this shouldn't be pub stats: OutfitStatSum,
pub stats: OutfitStatSum,
//pub total_space: content::OutfitSpace, //pub total_space: content::OutfitSpace,
available_space: content::OutfitSpace, available_space: content::OutfitSpace,
@ -110,6 +109,10 @@ impl<'a> OutfitSet {
} }
} }
pub fn stat_sum(&self) -> &OutfitStatSum {
&self.stats
}
/// Add an outfit to this ship. /// Add an outfit to this ship.
/// Returns true on success, and false on failure /// Returns true on success, and false on failure
/// TODO: failure reason enum /// TODO: failure reason enum

View File

@ -1,8 +1,13 @@
struct InstanceInput { struct InstanceInput {
@location(2) position: vec3<f32>, @location(2) position: vec2<f32>,
@location(3) size: f32, @location(3) velocity: vec2<f32>,
@location(4) expires: f32, @location(4) rotation_0: vec2<f32>,
@location(5) texture_index: u32, @location(5) rotation_1: vec2<f32>,
@location(6) size: f32,
@location(7) created: f32,
@location(8) expires: f32,
@location(9) texture_index_len_rep: vec3<u32>,
@location(10) texture_aspect_fps: vec2<f32>,
}; };
struct VertexInput { struct VertexInput {
@ -38,7 +43,9 @@ var texture_array: binding_array<texture_2d<f32>>;
var sampler_array: binding_array<sampler>; var sampler_array: binding_array<sampler>;
fn fmod(x: f32, m: f32) -> f32 {
return x - floor(x / m) * m;
}
@vertex @vertex
fn vertex_main( fn vertex_main(
@ -48,28 +55,55 @@ fn vertex_main(
var out: VertexOutput; var out: VertexOutput;
out.texture_coords = vertex.texture_coords; out.texture_coords = vertex.texture_coords;
out.texture_index = instance.texture_index;
if instance.expires < global.current_time.x { if instance.expires < global.current_time.x {
out.texture_index = instance.texture_index_len_rep.x;
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0); out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out; return out;
} }
let age = global.current_time.x - instance.created;
var frame: u32 = u32(0);
if instance.texture_index_len_rep.z == u32(1) {
// Repeat
frame = u32(fmod(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y)
));
} else {
// Once
frame = u32(min(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y) - 1.0
));
}
out.texture_index = instance.texture_index_len_rep.x + frame;
let rotation = mat2x2(instance.rotation_0, instance.rotation_1);
var scale: f32 = instance.size / global.camera_zoom.x; var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y); var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
pos = pos * vec2<f32>( pos = pos * vec2<f32>(
1.0 * scale / global.window_aspect.x, instance.texture_aspect_fps.x * scale / global.window_aspect.x,
scale scale
); );
pos = rotation * pos;
var ipos: vec2<f32> = (
vec2(instance.position.x, instance.position.y)
+ (instance.velocity * age)
- global.camera_position
);
var ipos: vec2<f32> = vec2(instance.position.x, instance.position.y) - global.camera_position;
pos = pos + vec2<f32>( pos = pos + vec2<f32>(
ipos.x / (global.camera_zoom.x/2.0) / global.window_aspect.x, ipos.x / (global.camera_zoom.x/2.0) / global.window_aspect.x,
ipos.y / (global.camera_zoom.x/2.0) ipos.y / (global.camera_zoom.x/2.0)
); );
out.position = vec4<f32>(pos, 0.0, 1.0) * instance.position.z; out.position = vec4<f32>(pos, 0.0, 1.0);
return out; return out;
} }

View File

@ -0,0 +1,26 @@
use cgmath::Point2;
use crate::{ObjectSprite, ParticleBuilder, UiSprite};
/// Bundles parameters passed to a single call to GPUState::render
pub struct FrameState<'a> {
/// Camera position, in world units
pub camera_pos: Point2<f32>,
/// Height of screen, in world units
pub camera_zoom: f32,
/// World object sprites
pub object_sprites: Vec<ObjectSprite>,
/// UI sprites
pub ui_sprites: Vec<UiSprite>,
/// Particles to create during this frame.
/// this array will be cleared.
pub new_particles: &'a mut Vec<ParticleBuilder>,
// TODO: handle overflow
/// The current time, in seconds
pub current_time: f32,
}

View File

@ -1,8 +1,8 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3}; use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3};
use galactica_constants; use galactica_constants;
use std::{iter, rc::Rc, time::Instant}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
use winit::{self, dpi::LogicalSize, window::Window}; use winit::{self, dpi::LogicalSize, window::Window};
@ -10,7 +10,7 @@ use crate::{
content, content,
globaldata::{GlobalData, GlobalDataContent}, globaldata::{GlobalData, GlobalDataContent},
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
sprite::{ObjectSubSprite, ParticleBuilder}, sprite::ObjectSubSprite,
starfield::Starfield, starfield::Starfield,
texturearray::TextureArray, texturearray::TextureArray,
vertexbuffer::{ vertexbuffer::{
@ -18,7 +18,7 @@ use crate::{
types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance}, types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance},
BufferObject, VertexBuffer, BufferObject, VertexBuffer,
}, },
ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX, FrameState, ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX,
}; };
/// A high-level GPU wrapper. Consumes game state, /// A high-level GPU wrapper. Consumes game state,
@ -456,28 +456,22 @@ impl GPUState {
/// Make an instance for all the game's sprites /// Make an instance for all the game's sprites
/// (Objects and UI) /// (Objects and UI)
/// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded. /// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded.
fn update_sprite_instances( fn update_sprite_instances(&self, framestate: FrameState) -> (usize, usize) {
&self,
camera_zoom: f32,
camera_pos: Point2<f32>,
objects: &Vec<ObjectSprite>,
ui: &Vec<UiSprite>,
) -> (usize, usize) {
let mut object_instances: Vec<ObjectInstance> = Vec::new(); let mut object_instances: Vec<ObjectInstance> = Vec::new();
// Game coordinates (relative to camera) of ne and sw corners of screen. // Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites. // Used to skip off-screen sprites.
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * camera_zoom; let clip_ne = Point2::from((-self.window_aspect, 1.0)) * framestate.camera_zoom;
let clip_sw = Point2::from((self.window_aspect, -1.0)) * camera_zoom; let clip_sw = Point2::from((self.window_aspect, -1.0)) * framestate.camera_zoom;
for s in objects { for s in framestate.object_sprites {
self.push_object_sprite( self.push_object_sprite(
camera_zoom, framestate.camera_zoom,
camera_pos, framestate.camera_pos,
&mut object_instances, &mut object_instances,
clip_ne, clip_ne,
clip_sw, clip_sw,
s, &s,
); );
} }
@ -495,8 +489,8 @@ impl GPUState {
let mut ui_instances: Vec<UiInstance> = Vec::new(); let mut ui_instances: Vec<UiInstance> = Vec::new();
for s in ui { for s in framestate.ui_sprites {
self.push_ui_sprite(&mut ui_instances, s); self.push_ui_sprite(&mut ui_instances, &s);
} }
if ui_instances.len() as u64 > galactica_constants::UI_SPRITE_INSTANCE_LIMIT { if ui_instances.len() as u64 > galactica_constants::UI_SPRITE_INSTANCE_LIMIT {
@ -525,17 +519,7 @@ impl GPUState {
} }
/// Main render function. Draws sprites on a window. /// Main render function. Draws sprites on a window.
pub fn render( pub fn render(&mut self, framestate: FrameState) -> Result<(), wgpu::SurfaceError> {
&mut self,
camera_pos: Point2<f32>,
camera_zoom: f32,
// TODO: clean this up, pass one struct
object_sprites: &Vec<ObjectSprite>,
ui_sprites: &Vec<UiSprite>,
new_particles: &mut Vec<ParticleBuilder>,
start_instant: Instant,
) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?; let output = self.surface.get_current_texture()?;
let view = output let view = output
.texture .texture
@ -568,16 +552,13 @@ impl GPUState {
timestamp_writes: None, timestamp_writes: None,
}); });
// TODO: handle overflow
let time_now = start_instant.elapsed().as_secs_f32();
// Update global values // Update global values
self.queue.write_buffer( self.queue.write_buffer(
&self.global_data.buffer, &self.global_data.buffer,
0, 0,
bytemuck::cast_slice(&[GlobalDataContent { bytemuck::cast_slice(&[GlobalDataContent {
camera_position: camera_pos.into(), camera_position: framestate.camera_pos.into(),
camera_zoom: [camera_zoom, 0.0], camera_zoom: [framestate.camera_zoom, 0.0],
camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX], camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX],
window_size: [ window_size: [
self.window_size.width as f32, self.window_size.width as f32,
@ -590,20 +571,25 @@ impl GPUState {
galactica_constants::STARFIELD_SIZE_MIN, galactica_constants::STARFIELD_SIZE_MIN,
galactica_constants::STARFIELD_SIZE_MAX, galactica_constants::STARFIELD_SIZE_MAX,
], ],
current_time: [time_now, 0.0], current_time: [framestate.current_time, 0.0],
}]), }]),
); );
for i in new_particles.iter() { // Write all new particles to GPU buffer
for i in framestate.new_particles.iter() {
let texture = self.texture_array.get_texture(i.texture); let texture = self.texture_array.get_texture(i.texture);
self.queue.write_buffer( self.queue.write_buffer(
&self.vertex_buffers.particle.instances, &self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_array_head, ParticleInstance::SIZE * self.vertex_buffers.particle_array_head,
bytemuck::cast_slice(&[ParticleInstance { bytemuck::cast_slice(&[ParticleInstance {
position: [i.pos.x, i.pos.y, 1.0], position: [i.pos.x, i.pos.y],
velocity: i.velocity.into(),
rotation: Matrix2::from_angle(i.angle).into(),
size: i.size, size: i.size,
texture_index: texture.index, texture_index_len_rep: [texture.index, texture.len, texture.repeat],
expires: time_now + i.lifetime, texture_aspect_fps: [texture.aspect, texture.fps],
created: framestate.current_time,
expires: framestate.current_time + i.lifetime,
}]), }]),
); );
self.vertex_buffers.particle_array_head += 1; self.vertex_buffers.particle_array_head += 1;
@ -613,11 +599,10 @@ impl GPUState {
self.vertex_buffers.particle_array_head = 0; self.vertex_buffers.particle_array_head = 0;
} }
} }
new_particles.clear(); framestate.new_particles.clear();
// Create sprite instances // Create sprite instances
let (n_object, n_ui) = let (n_object, n_ui) = self.update_sprite_instances(framestate);
self.update_sprite_instances(camera_zoom, camera_pos, object_sprites, ui_sprites);
// These should match the indices in each shader, // These should match the indices in each shader,
// and should each have a corresponding bind group layout. // and should each have a corresponding bind group layout.

View File

@ -7,6 +7,7 @@
//! and the only one external code should interact with. //! and the only one external code should interact with.
//! (Excluding data structs, like [`ObjectSprite`]) //! (Excluding data structs, like [`ObjectSprite`])
mod framestate;
mod globaldata; mod globaldata;
mod gpustate; mod gpustate;
mod pipeline; mod pipeline;
@ -15,6 +16,7 @@ mod starfield;
mod texturearray; mod texturearray;
mod vertexbuffer; mod vertexbuffer;
pub use framestate::FrameState;
use galactica_content as content; use galactica_content as content;
pub use gpustate::GPUState; pub use gpustate::GPUState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite}; pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite};

View File

@ -1,14 +1,19 @@
use crate::content; use crate::content;
use cgmath::{Deg, Point2, Point3}; use cgmath::{Deg, Point2, Point3, Vector2};
/// Instructions to create a new particle /// Instructions to create a new particle
pub struct ParticleBuilder { pub struct ParticleBuilder {
/// The texture to use for this particle /// The texture to use for this particle
pub texture: content::TextureHandle, pub texture: content::TextureHandle,
// TODO: rotation, velocity
/// This object's center, in world coordinates. /// This object's center, in world coordinates.
pub pos: Point3<f32>, pub pos: Point2<f32>,
/// This particle's velocity, in world coordinates
pub velocity: Vector2<f32>,
/// This particle's angle, in world coordinates
pub angle: Deg<f32>,
/// This particle's lifetime, in seconds /// This particle's lifetime, in seconds
pub lifetime: f32, pub lifetime: f32,

View File

@ -70,7 +70,10 @@ impl RawTexture {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Texture { pub struct Texture {
pub index: u32, // Index in texture array pub index: u32, // Index in texture array
pub len: u32, // Number of frames
pub fps: f32, // Frames per second
pub aspect: f32, // width / height pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture
} }
pub struct TextureArray { pub struct TextureArray {
@ -98,18 +101,23 @@ impl TextureArray {
let mut textures = HashMap::new(); let mut textures = HashMap::new();
for t in &ct.textures { for t in &ct.textures {
let mut f = File::open(&t.path)?; let index = texture_data.len() as u32;
for f in &t.frames {
let mut f = File::open(&f)?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?; f.read_to_end(&mut bytes)?;
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
}
textures.insert( textures.insert(
t.handle, t.handle,
Texture { Texture {
index: texture_data.len() as u32, index,
aspect: t.handle.aspect, aspect: t.handle.aspect,
fps: t.fps,
len: t.frames.len() as u32,
repeat: t.repeat.as_int(),
}, },
); );
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
} }
let sampler = device.create_sampler(&wgpu::SamplerDescriptor { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {

View File

@ -200,22 +200,32 @@ impl BufferObject for UiInstance {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ParticleInstance { pub struct ParticleInstance {
/// World position of this particle /// World position of this particle
pub position: [f32; 3], pub position: [f32; 2],
/// Velocity of this sprite, in world coordinates
pub velocity: [f32; 2],
/// Rotation matrix for this sprite, in world coordinates
pub rotation: [[f32; 2]; 2],
// TODO: docs: particle frames must all have the same size // TODO: docs: particle frames must all have the same size
// TODO: is transparency trimmed? That's not ideal for animated particles! // TODO: is transparency trimmed? That's not ideal for animated particles!
/// The height of this particle, in world units /// The height of this particle, in world units
pub size: f32, pub size: f32,
// TODO: rotation, velocity vector
// TODO: animated sprites // TODO: animated sprites
// TODO: texture aspect ratio // TODO: texture aspect ratio
/// The time, in seconds, at which this particle was created.
/// Time is kept by a variable in the global uniform.
pub created: f32,
/// The time, in seconds, at which this particle expires. /// The time, in seconds, at which this particle expires.
/// Time is kept by a variable in the global uniform. /// Time is kept by a variable in the global uniform.
pub expires: f32, pub expires: f32,
/// What texture to use for this particle /// What texture to use for this particle
pub texture_index: u32, pub texture_index_len_rep: [u32; 3],
pub texture_aspect_fps: [f32; 2],
} }
impl BufferObject for ParticleInstance { impl BufferObject for ParticleInstance {
@ -228,25 +238,54 @@ impl BufferObject for ParticleInstance {
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: 0, offset: 0,
shader_location: 2, shader_location: 2,
format: wgpu::VertexFormat::Float32x3, format: wgpu::VertexFormat::Float32x2,
},
// Velocity
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32x2,
},
// Rotation
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 4,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
shader_location: 5,
format: wgpu::VertexFormat::Float32x2,
}, },
// Size // Size
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 3, shader_location: 6,
format: wgpu::VertexFormat::Float32,
},
// Created
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Expires // Expires
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
shader_location: 4, shader_location: 8,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Texture // Texture index / len / repeat
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
shader_location: 5, shader_location: 9,
format: wgpu::VertexFormat::Uint32, format: wgpu::VertexFormat::Uint32x3,
},
// Texture aspect / fps
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
shader_location: 10,
format: wgpu::VertexFormat::Float32x2,
}, },
], ],
} }

View File

@ -4,10 +4,10 @@ use galactica_gameobject as object;
use galactica_render::{AnchoredUiPosition, UiSprite}; use galactica_render::{AnchoredUiPosition, UiSprite};
use galactica_world::{util, ShipPhysicsHandle, World}; use galactica_world::{util, ShipPhysicsHandle, World};
// TODO: camera as one unit // TODO: args as one unit
pub fn build_radar( pub fn build_radar(
ct: &content::Content, ct: &content::Content,
player: &ShipPhysicsHandle, player: ShipPhysicsHandle,
physics: &World, physics: &World,
system: &object::System, system: &object::System,
camera_zoom: f32, camera_zoom: f32,

View File

@ -27,7 +27,6 @@ impl ShipControls {
} }
/// A ship instance in the physics system /// A ship instance in the physics system
/// TODO: Decouple ship data from physics
pub struct ShipWorldObject { pub struct ShipWorldObject {
/// TODO /// TODO
pub physics_handle: ShipPhysicsHandle, pub physics_handle: ShipPhysicsHandle,
@ -56,17 +55,18 @@ impl ShipWorldObject {
if self.controls.thrust { if self.controls.thrust {
r.apply_impulse( r.apply_impulse(
vector![engine_force.x, engine_force.y] * self.ship.outfits.stats.engine_thrust, vector![engine_force.x, engine_force.y]
* self.ship.outfits.stat_sum().engine_thrust,
true, true,
); );
} }
if self.controls.right { if self.controls.right {
r.apply_torque_impulse(self.ship.outfits.stats.steer_power * -100.0 * t, true); r.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * -100.0 * t, true);
} }
if self.controls.left { if self.controls.left {
r.apply_torque_impulse(self.ship.outfits.stats.steer_power * 100.0 * t, true); r.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * 100.0 * t, true);
} }
for i in self.ship.outfits.iter_guns() { for i in self.ship.outfits.iter_guns() {

View File

@ -1,4 +1,4 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Point3, Rad, Vector2}; use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2};
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use nalgebra::vector; use nalgebra::vector;
use rand::Rng; use rand::Rng;
@ -57,12 +57,12 @@ impl<'a> World {
/// Add a projectile fired from a ship /// Add a projectile fired from a ship
fn add_projectiles( fn add_projectiles(
&mut self, &mut self,
s: &ShipPhysicsHandle, s: ShipPhysicsHandle,
p: Vec<(object::Projectile, content::GunPoint)>, p: Vec<(object::Projectile, content::GunPoint)>,
) { ) {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
for (projectile, point) in p { for (projectile, point) in p {
let r = self.get_ship_body(&s).unwrap().1; let r = self.get_ship_body(s).unwrap().1;
let ship_pos = util::rigidbody_position(r); let ship_pos = util::rigidbody_position(r);
let ship_rot = util::rigidbody_rotation(r); let ship_rot = util::rigidbody_rotation(r);
@ -187,7 +187,7 @@ impl<'a> World {
} }
} }
for (s, p) in projectiles { for (s, p) in projectiles {
self.add_projectiles(&s, p); self.add_projectiles(s, p);
} }
for s in to_remove { for s in to_remove {
self.remove_ship(s); self.remove_ship(s);
@ -211,20 +211,40 @@ impl<'a> World {
}; };
if let Some(p) = self.projectiles.get(a) { if let Some(p) = self.projectiles.get(a) {
if let Some(s) = self.ships.get_mut(b) { let hit = if let Some(s) = self.ships.get_mut(b) {
let hit = s.ship.handle_projectile_collision(ct, &p.projectile); s.ship.handle_projectile_collision(ct, &p.projectile)
} else {
false
};
// Stupid re-borrow trick.
// We can't have s be mutable inside this block.
if let Some(s) = self.ships.get(b) {
if hit { if hit {
let r = self.get_rigid_body(p.rigid_body); let pr = self.get_rigid_body(p.rigid_body);
let pos = util::rigidbody_position(r); let pos = util::rigidbody_position(pr);
let angle: Deg<f32> = util::rigidbody_rotation(pr)
.angle(Vector2 { x: 1.0, y: 0.0 })
.into();
// Match target ship velocity, so impact particles are "sticky".
// Particles will fly off if the ship is spinning fast, but I
// haven't found a good way to fix that.
let (_, sr) = self.get_ship_body(s.physics_handle).unwrap();
let velocity =
sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y));
let velocity = Vector2 {
x: velocity.x,
y: velocity.y,
};
particles.push(ParticleBuilder { particles.push(ParticleBuilder {
texture: ct.get_texture_handle("particle::blaster"), texture: ct.get_texture_handle("particle::blaster"),
pos: Point3 { pos: Point2 { x: pos.x, y: pos.y },
x: pos.x, velocity,
y: pos.y, angle: -angle,
z: 1.0, // TODO:remove z coordinate lifetime: 0.15,
}, size: 5.0,
lifetime: 0.1,
size: 10.0,
}); });
self.remove_projectile(*a); self.remove_projectile(*a);
} }
@ -259,7 +279,7 @@ impl<'a> World {
/// Get a ship and its rigidbody from a handle /// Get a ship and its rigidbody from a handle
pub fn get_ship_body( pub fn get_ship_body(
&self, &self,
s: &ShipPhysicsHandle, s: ShipPhysicsHandle,
) -> Option<(&objects::ShipWorldObject, &RigidBody)> { ) -> Option<(&objects::ShipWorldObject, &RigidBody)> {
// TODO: handle dead handles // TODO: handle dead handles
Some((self.ships.get(&s.1)?, self.wrapper.rigid_body_set.get(s.0)?)) Some((self.ships.get(&s.1)?, self.wrapper.rigid_body_set.get(s.0)?))