From a779011b9048cdda04887c77e20ee8936f332a7a Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Tue, 25 Aug 2026 00:32:16 -0700 Subject: vfx pipeline --- src/content/entities/entity_bullet_emitter.rs | 17 +- src/content/mod.rs | 1 + src/content/vfx/mod.rs | 29 ++++ src/input.rs | 2 +- src/main.rs | 14 +- src/renderer/mod.rs | 222 ++++++++++++++++++++++++-- src/shader/instance.wgsl | 6 +- src/shader/particle.wgsl | 11 +- src/shader/vfx_line.wgsl | 76 +++++++++ src/sim/sim_manager/mod.rs | 35 +++- src/vfx/mod.rs | 58 +++++++ 11 files changed, 440 insertions(+), 31 deletions(-) create mode 100644 src/content/vfx/mod.rs create mode 100644 src/shader/vfx_line.wgsl create mode 100644 src/vfx/mod.rs diff --git a/src/content/entities/entity_bullet_emitter.rs b/src/content/entities/entity_bullet_emitter.rs index c014e8c..3788dd9 100644 --- a/src/content/entities/entity_bullet_emitter.rs +++ b/src/content/entities/entity_bullet_emitter.rs @@ -2,7 +2,7 @@ use glam::{IVec2, Vec2}; use rapier2d::dynamics::RigidBodyBuilder; use crate::{ - content::materials::MaterialId, + content::{materials::MaterialId, vfx::VfxMaterialId}, input::Input, sim::{ cell::Cell, @@ -10,6 +10,7 @@ use crate::{ lib::force::apply_bullet, sim_manager::SimCtx, }, + vfx::VfxLine, }; struct BulletEmitterEntityBehaviour { @@ -27,11 +28,21 @@ impl EntityBehaviour for BulletEmitterEntityBehaviour { if let Some(target) = self.target { let pos = update_ctx.entity_data.transform(ctx).unwrap().0; let dir = (target - pos).normalize(); + let from = pos + (dir * 4.0); self.shot_timer -= delta_time; if self.shot_timer <= 0.0 { - apply_bullet(ctx, pos + (dir * 4.0), target, 70); - self.shot_timer = 0.1; + apply_bullet(ctx, from, target, 70); + // 0.1 seconds to travel 200 cells + let lifetime = (target - from).length() / 1000.0; + ctx.vfx_writer.write_vfx_line(VfxLine::new( + from, + target, + 1.3, + VfxMaterialId::Tracer, + lifetime, + )); + self.shot_timer = 0.2; } self.life -= delta_time; diff --git a/src/content/mod.rs b/src/content/mod.rs index 2044048..ee2f47c 100644 --- a/src/content/mod.rs +++ b/src/content/mod.rs @@ -1,2 +1,3 @@ pub mod entities; pub mod materials; +pub mod vfx; diff --git a/src/content/vfx/mod.rs b/src/content/vfx/mod.rs new file mode 100644 index 0000000..8843bf1 --- /dev/null +++ b/src/content/vfx/mod.rs @@ -0,0 +1,29 @@ +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum VfxMaterialId { + Tracer = 0, +} + +pub struct VfxMaterialDef { + pub name: &'static str, + pub color: (u8, u8, u8, u8), + pub length: f32, +} + +static MATERIALS: [VfxMaterialDef; 1] = [VfxMaterialDef { + name: "Tracer", + color: (0xFF, 0xAA, 0xAA, 0xFF), + length: 15.0, +}]; + +impl VfxMaterialId { + pub const ALL: [VfxMaterialId; 1] = [VfxMaterialId::Tracer]; + #[inline] + pub fn def(self) -> &'static VfxMaterialDef { + &MATERIALS[self as usize] + } + + pub fn from_index(idx: u8) -> VfxMaterialId { + VfxMaterialId::ALL[idx as usize] + } +} diff --git a/src/input.rs b/src/input.rs index 7fe6c79..09a0436 100644 --- a/src/input.rs +++ b/src/input.rs @@ -9,7 +9,7 @@ use crate::{camera::Camera, sim::cell_manager::manager::CellManager}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Input { // movement - Up, + Up = 0, Left, Down, Right, diff --git a/src/main.rs b/src/main.rs index ccca2db..6e6c9d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod content; mod input; mod renderer; mod sim; +mod vfx; use futures::executor; use std::{collections::VecDeque, sync::Arc, time::Instant}; @@ -30,6 +31,7 @@ use crate::{ }, renderer::RendererState, sim::{rb_manager::DebugRenderMode, sim_manager::SimManager}, + vfx::VfxManager, }; pub type Error = Box; @@ -58,6 +60,7 @@ struct App { // game input_manager: InputManager, camera: Option, + vfx_manager: Option, sim_manager: Option, // renderer @@ -77,7 +80,9 @@ impl App { camera.handle_camera_input(&self.input_manager, delta_time) } - if let Some(sim) = &mut self.sim_manager { + if let Some(sim) = &mut self.sim_manager + && let Some(vfx_manager) = &mut self.vfx_manager + { // --TEST SPAWNING-- if self.input_manager.pressed(Input::Action1) { sim.create_entity(entity_cube_def( @@ -97,7 +102,7 @@ impl App { )); } - sim.update(&self.config, &self.input_manager, delta_time); + sim.update(vfx_manager, &self.config, &self.input_manager, delta_time); self.input_manager.reset_for_frame(); } } @@ -113,6 +118,7 @@ impl Default for App { camera: None, + vfx_manager: Some(VfxManager::new()), sim_manager: Some(SimManager::new()), last_render: Instant::now(), @@ -206,15 +212,19 @@ impl ApplicationHandler for App { if let Some(renderer_state) = &mut self.renderer_state && let Some(sim) = &mut self.sim_manager + && let Some(vfx) = &mut self.vfx_manager && let Some(camera) = &mut self.camera { renderer_state.render( sim, + vfx, camera, &mut self.config, &self.diagnostics, &self.input_manager, ); + + vfx.after_render(delta_time); } } _ => {} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index f21c794..c1b156e 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -9,21 +9,22 @@ use crate::{ Config, Diagnostics, InputManager, camera::Camera, config::{CELLS_IN_CHUNK, CHUNK_SIZE}, - content::materials::MaterialId, + content::{materials::MaterialId, vfx::VfxMaterialId}, renderer::ui::draw_egui, sim::{ - cell_manager::manager::CellManager, - entity::EntityId, - rb_manager::debug_render::DebugVertex, - sim_manager::{SimCtx, SimManager}, + cell_manager::manager::CellManager, entity::EntityId, + rb_manager::debug_render::DebugVertex, sim_manager::SimManager, }, + vfx::VfxManager, }; // TODO derive from shared chunk config const CHUNK_SLOTS: usize = 11 * 200; -const RB_ENTITY_SLOTS: usize = 64; -const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS; +const ENTITY_SLOTS: usize = 64; +const CELL_SLOTS: usize = CHUNK_SLOTS + ENTITY_SLOTS; const PARTICLE_SLOTS: usize = 10_000; +// TODO make this dynamic +const VFX_LINE_SLOTS: usize = 10_000; const INITIAL_DEBUG_VERTEX_CAPACITY: usize = 4096; @@ -46,6 +47,25 @@ struct RendererParticle { life: f32, } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct RendererVfxMaterial { + color: [f32; 4], + length: f32, + _padding: [u32; 3], +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct RendererVfxLine { + a: [f32; 2], + b: [f32; 2], + width: f32, + material: u32, + alive_for: f32, + lifetime: f32, +} + fn srgb_to_linear(channel: u8) -> f32 { let channel = channel as f32 / 255.0; if channel <= 0.04045 { @@ -84,7 +104,7 @@ pub struct RendererState { // common camera_uniform_buffer: wgpu::Buffer, - // world pixels + // instances instance_pipeline: wgpu::RenderPipeline, instance_buffer: wgpu::Buffer, cell_buffer: wgpu::Buffer, @@ -95,6 +115,11 @@ pub struct RendererState { particle_buffer: wgpu::Buffer, particle_bind_group: wgpu::BindGroup, + // vfx + vfx_line_pipeline: wgpu::RenderPipeline, + vfx_line_buffer: wgpu::Buffer, + vfx_line_bind_group: wgpu::BindGroup, + // physics debug lines debug_pipeline: wgpu::RenderPipeline, debug_bind_group: wgpu::BindGroup, @@ -193,6 +218,39 @@ impl RendererState { mapped_at_creation: false, }); + queue.write_buffer(&palette_buffer, 0, bytemuck::cast_slice(&palette)); + + // --- VFX materials --- + let mut vfx_material_palette = [RendererVfxMaterial { + color: [0.0f32; 4], + length: 0.0, + _padding: [0; 3], + }; VfxMaterialId::ALL.len()]; + for material in VfxMaterialId::ALL { + let def = material.def(); + let i = material as usize; + vfx_material_palette[i].color = [ + srgb_to_linear(def.color.0), + srgb_to_linear(def.color.1), + srgb_to_linear(def.color.2), + def.color.3 as f32 / 255.0, + ]; + vfx_material_palette[i].length = def.length; + } + + let vfx_material_palette_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("VFX Material Palette buffer"), + size: size_of_val(&vfx_material_palette) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + queue.write_buffer( + &vfx_material_palette_buffer, + 0, + bytemuck::cast_slice(&vfx_material_palette), + ); + // --- INSTANCES --- let instance_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -298,8 +356,6 @@ impl RendererState { mapped_at_creation: false, }); - queue.write_buffer(&palette_buffer, 0, bytemuck::cast_slice(&palette)); - let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Instance buffer"), size: (CELL_SLOTS * size_of::()) as u64, @@ -450,6 +506,120 @@ impl RendererState { ], }); + // --- VFX_LINES --- + let vfx_line_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[ + // camera uniform + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 0, + count: None, + visibility: wgpu::ShaderStages::VERTEX, + }, + // material palette + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 1, + count: None, + // don't need vertex here + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + }, + // instance buffer + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 2, + count: None, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + }, + ], + }); + + let vfx_line_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("VFX Line shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("../shader/vfx_line.wgsl").into()), + }); + + let vfx_line_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + immediate_size: 0, + bind_group_layouts: &[Some(&vfx_line_bind_group_layout)], + }); + + let vfx_line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&vfx_line_pipeline_layout), + vertex: wgpu::VertexState { + module: &vfx_line_shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &vfx_line_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let vfx_line_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("VFX line buffer"), + size: (VFX_LINE_SLOTS * size_of::()) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let vfx_line_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &vfx_line_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: camera_uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: vfx_material_palette_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: vfx_line_buffer.as_entire_binding(), + }, + ], + }); + // --- DEBUG --- let debug_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -554,6 +724,10 @@ impl RendererState { particle_buffer, particle_bind_group, + vfx_line_pipeline, + vfx_line_buffer, + vfx_line_bind_group, + debug_pipeline, debug_bind_group, debug_vertex_buffer, @@ -575,6 +749,7 @@ impl RendererState { pub fn render( &mut self, sim: &mut SimManager, + vfx_manager: &VfxManager, camera: &mut Camera, config: &mut Config, diagnostics: &Diagnostics, @@ -784,6 +959,28 @@ impl RendererState { .write_buffer(&self.particle_buffer, 0, bytemuck::cast_slice(&particles)); } + let mut vfx_lines: Vec = Vec::new(); + + { + puffin::profile_scope!("Build VFX lines"); + // TODO could cull this to visible, maybe it's slower? + vfx_lines = vfx_manager + .lines + .iter() + .map(|p| RendererVfxLine { + a: p.a.to_array(), + b: p.b.to_array(), + material: p.material as u32, + width: p.width, + alive_for: p.alive_for, + lifetime: p.lifetime, + }) + .collect(); + + self.queue + .write_buffer(&self.vfx_line_buffer, 0, bytemuck::cast_slice(&vfx_lines)); + } + let debug_vertex_count = if config.debug_render { puffin::profile_scope!("Build physics debug lines"); let vertices = sim.rb_manager.debug_render(config.debug_render_mode); @@ -854,6 +1051,11 @@ impl RendererState { render_pass.set_bind_group(0, &self.particle_bind_group, &[]); render_pass.draw(0..4, 0..particles.len() as u32); + // vfx + render_pass.set_pipeline(&self.vfx_line_pipeline); + render_pass.set_bind_group(0, &self.vfx_line_bind_group, &[]); + render_pass.draw(0..4, 0..vfx_lines.len() as u32); + // debug if debug_vertex_count > 0 { render_pass.set_pipeline(&self.debug_pipeline); diff --git a/src/shader/instance.wgsl b/src/shader/instance.wgsl index fb1e0b1..a549801 100644 --- a/src/shader/instance.wgsl +++ b/src/shader/instance.wgsl @@ -17,8 +17,8 @@ struct Instance { @group(0) @binding(3) var cells: array; struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) world: vec2, + @builtin(position) clip_position: vec4f, + @location(0) world: vec2f, @location(1) @interpolate(flat) instance: u32, }; @@ -46,7 +46,7 @@ fn vs_main( } @fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { +fn fs_main(in: VertexOutput) -> @location(0) vec4f { let instance = instances[in.instance]; let d = in.world - instance.centre; diff --git a/src/shader/particle.wgsl b/src/shader/particle.wgsl index 3dd61a9..227468b 100644 --- a/src/shader/particle.wgsl +++ b/src/shader/particle.wgsl @@ -14,8 +14,8 @@ struct Particle { @group(0) @binding(2) var particles: array; struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) world: vec2, + @builtin(position) clip_position: vec4f, + @location(0) world: vec2f, @location(1) @interpolate(flat) instance: u32, @location(2) @interpolate(flat) material: u32, @location(3) @interpolate(flat) life: f32, @@ -29,7 +29,7 @@ fn vs_main( let particle = particles[n]; // (0,0),(1,0),(0,1),(1,1) - let corner = vec2(f32(i & 1u), f32((i >> 1u) & 1u)); + let corner = vec2f(f32(i & 1u), f32((i >> 1u) & 1u)); let world = particle.position + corner; var out: VertexOutput; @@ -43,9 +43,10 @@ fn vs_main( } @fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { +fn fs_main(in: VertexOutput) -> @location(0) vec4f { let particle = particles[in.instance]; // TODO can pack more in here and shift let c = palette[in.material]; - return vec4(c.r, c.g, c.b, c.a * smoothstep(0.0, 1, in.life)); + // if life is less than 1, smoothly reduce alpha to 0 + return vec4f(c.r, c.g, c.b, c.a * smoothstep(0.0, 1, in.life)); } diff --git a/src/shader/vfx_line.wgsl b/src/shader/vfx_line.wgsl new file mode 100644 index 0000000..bef7645 --- /dev/null +++ b/src/shader/vfx_line.wgsl @@ -0,0 +1,76 @@ +struct Camera { + scale: vec2f, + centre: vec2f, +}; + +struct MaterialDef { + color: vec4f, + // the length of the path segment for t + length: f32, +}; + +struct VfxLine { + a: vec2f, + b: vec2f, + width: f32, + material: u32, + alive_for: f32, + lifetime: f32, +} + +@group(0) @binding(0) var camera: Camera; +@group(0) @binding(1) var materials: array; +@group(0) @binding(2) var vfx_lines: array; + +struct VertexOutput { + @builtin(position) clip_position: vec4f, + @location(0) @interpolate(flat) color: vec4f, + // the length of the path segment for t + @location(1) @interpolate(flat) length: f32, + @location(2) @interpolate(flat) cur_centre: f32, + @location(3) dist: f32, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) i: u32, + @builtin(instance_index) n: u32, +) -> VertexOutput { + let vfx_line = vfx_lines[n]; + + let d = vfx_line.b - vfx_line.a; + // (-y, x) is a 90deg ccw rotation + let p = normalize(vec2f(-d.y, d.x)) * (vfx_line.width * 0.5); + // 0=a,1=b + let cv = f32(i & 1u); + // direction to offset the orthogonal width + let co = f32(i >> 1u) * 2.0 - 1.0; + // select a/b, add offset + let world = mix(vfx_line.a, vfx_line.b, cv) + p * co; + var out: VertexOutput; + out.clip_position = vec4f((world - camera.centre) * camera.scale, 0.0, 1.0); + + // material + let material = materials[vfx_line.material]; + out.color = material.color; + out.length = material.length; + + // computed + let l = length(d); + out.cur_centre = (l + material.length) * (vfx_line.alive_for / vfx_line.lifetime) - (material.length / 2); + + // interpolated + out.dist = cv * l; + + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4f { + // distance on line of the centre of the current (w.r.t. time) line segment + // can do this in vertex + // our distance along the line vs the current centre + let dist_from_centre = abs(in.dist - in.cur_centre); + let c = in.color; + return vec4f(c.r, c.g, c.b, c.a * (1 - smoothstep(0.0, in.length / 2, dist_from_centre))); +} diff --git a/src/sim/sim_manager/mod.rs b/src/sim/sim_manager/mod.rs index 9ae66be..8cb5eb3 100644 --- a/src/sim/sim_manager/mod.rs +++ b/src/sim/sim_manager/mod.rs @@ -18,6 +18,7 @@ use crate::{ process_entity_update_result, read_back_entities_from_world, write_entities_to_world, }, }, + vfx::VfxWriter, }; mod utils; @@ -43,6 +44,7 @@ pub struct SimManager { } pub struct SimCtx<'a> { + pub vfx_writer: &'a mut dyn VfxWriter, pub input_manager: &'a InputManager, pub cell_manager: &'a mut CellManager, pub rb_manager: &'a mut RbManager, @@ -130,7 +132,13 @@ impl SimManager { } } - fn cell_update(&mut self, config: &Config, input_manager: &InputManager, delta_time: f32) { + fn cell_update( + &mut self, + vfx_writer: &mut dyn VfxWriter, + config: &Config, + input_manager: &InputManager, + delta_time: f32, + ) { // before we tick, write all the entities into the sim world let written_entities_scope = write_entities_to_world(self); @@ -187,6 +195,7 @@ impl SimManager { for id in entity_ids { let entity = self.entities.get_mut(&id); let mut ctx = SimCtx { + vfx_writer, input_manager, cell_manager: &mut self.cell_manager, rb_manager: &mut self.rb_manager, @@ -203,11 +212,17 @@ impl SimManager { read_back_entities_from_world(self, written_entities_scope); } - fn physics_update(&mut self, input_manager: &InputManager, delta_time: f32) { + fn physics_update( + &mut self, + vfx_writer: &mut dyn VfxWriter, + input_manager: &InputManager, + delta_time: f32, + ) { let entity_ids: Vec = self.entities.keys().cloned().collect(); for id in entity_ids { let entity = self.entities.get_mut(&id); let mut ctx = SimCtx { + vfx_writer, input_manager, cell_manager: &mut self.cell_manager, rb_manager: &mut self.rb_manager, @@ -243,7 +258,13 @@ impl SimManager { .tick(&mut self.cell_manager, delta_time); } - pub fn update(&mut self, config: &Config, input_manager: &InputManager, delta_time: f32) { + pub fn update( + &mut self, + vfx_writer: &mut dyn VfxWriter, + config: &Config, + input_manager: &InputManager, + delta_time: f32, + ) { // handle sim controls if input_manager.pressed(Input::Pause) { self.paused = !self.paused; @@ -268,14 +289,14 @@ impl SimManager { self.last_cell_update = now; if self.paused && self.ignore_pause_next_tick { - self.cell_update(config, input_manager, delta_time); + self.cell_update(vfx_writer, config, input_manager, delta_time); } else if !self.paused { self.cell_updates_due += secs_since_last_cell_update / expected_secs_since_last_cell_update; let mut cell_updates_done = 0; // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock while self.cell_updates_due >= 1.0 && cell_updates_done < 3 { - self.cell_update(config, input_manager, delta_time); + self.cell_update(vfx_writer, config, input_manager, delta_time); self.cell_updates_due -= 1.0; cell_updates_done += 1; } @@ -288,14 +309,14 @@ impl SimManager { self.last_physics_update = now; if self.paused && self.ignore_pause_next_tick { - self.physics_update(input_manager, PHYSICS_DELTA_TIME); + self.physics_update(vfx_writer, input_manager, PHYSICS_DELTA_TIME); } else if !self.paused { self.physics_updates_due += secs_since_last_physics_update / expected_secs_since_last_physics_update; let mut updates_done = 0; // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock while self.physics_updates_due >= 1.0 && updates_done < 3 { - self.physics_update(input_manager, PHYSICS_DELTA_TIME); + self.physics_update(vfx_writer, input_manager, PHYSICS_DELTA_TIME); self.physics_updates_due -= 1.0; updates_done += 1; } diff --git a/src/vfx/mod.rs b/src/vfx/mod.rs new file mode 100644 index 0000000..3a7d1a5 --- /dev/null +++ b/src/vfx/mod.rs @@ -0,0 +1,58 @@ +use glam::Vec2; + +use crate::content::vfx::VfxMaterialId; + +pub struct VfxLine { + pub a: Vec2, + pub b: Vec2, + pub width: f32, + pub material: VfxMaterialId, + pub alive_for: f32, + pub lifetime: f32, +} + +impl VfxLine { + pub fn new(a: Vec2, b: Vec2, width: f32, material: VfxMaterialId, lifetime: f32) -> Self { + VfxLine { + a, + b, + width, + material, + alive_for: 0.0, + lifetime, + } + } +} + +pub trait VfxWriter { + fn write_vfx_line(&mut self, line: VfxLine) -> (); +} + +pub struct VfxManager { + pub lines: Vec, +} + +impl VfxManager { + pub fn after_render(&mut self, delta_time: f32) { + let mut i = 0; + while i < self.lines.len() { + let l = &mut self.lines[i]; + l.alive_for += delta_time; + if l.alive_for > l.lifetime { + self.lines.swap_remove(i); + continue; + } + i += 1; + } + } + + pub fn new() -> Self { + VfxManager { lines: Vec::new() } + } +} + +impl VfxWriter for VfxManager { + fn write_vfx_line(&mut self, line: VfxLine) -> () { + self.lines.push(line); + } +} -- cgit v1.3.1