From d354a885436bbceb573626624760a66710bddb29 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 3 Feb 2024 11:24:17 -0800 Subject: [PATCH] Improved scenes & event handling --- content/ui/flying.rhai | 19 +- content/ui/landed.rhai | 50 +- content/ui/outfitter.rhai | 61 ++- crates/galactica/src/main.rs | 63 ++- crates/playeragent/src/inputstatus.rs | 1 + crates/playeragent/src/playeragent.rs | 1 + crates/render/src/gpustate.rs | 480 +++++++++++++++++- crates/render/src/lib.rs | 2 - crates/render/src/renderinput.rs | 7 +- crates/render/src/renderscene/landed.rs | 117 ----- crates/render/src/renderscene/mod.rs | 32 -- crates/render/src/renderscene/system/mod.rs | 4 - crates/render/src/renderscene/system/phys.rs | 321 ------------ .../render/src/renderscene/system/system.rs | 144 ------ crates/render/src/ui/api/config.rs | 28 + crates/render/src/ui/api/event.rs | 42 ++ crates/render/src/ui/api/mod.rs | 9 + crates/render/src/ui/api/sceneaction.rs | 12 +- crates/render/src/ui/api/sprite.rs | 1 + crates/render/src/ui/api/state.rs | 73 ++- crates/render/src/ui/event.rs | 8 + crates/render/src/ui/manager.rs | 308 ++++++++--- crates/render/src/ui/mod.rs | 4 +- crates/render/src/ui/mouseevent.rs | 24 - crates/render/src/ui/uielement.rs | 1 + crates/render/src/ui/uiscene.rs | 16 - crates/render/src/ui/util/sprite.rs | 18 +- 27 files changed, 997 insertions(+), 849 deletions(-) delete mode 100644 crates/render/src/renderscene/landed.rs delete mode 100644 crates/render/src/renderscene/mod.rs delete mode 100644 crates/render/src/renderscene/system/mod.rs delete mode 100644 crates/render/src/renderscene/system/phys.rs delete mode 100644 crates/render/src/renderscene/system/system.rs create mode 100644 crates/render/src/ui/api/config.rs create mode 100644 crates/render/src/ui/api/event.rs create mode 100644 crates/render/src/ui/event.rs delete mode 100644 crates/render/src/ui/mouseevent.rs delete mode 100644 crates/render/src/ui/uiscene.rs diff --git a/content/ui/flying.rhai b/content/ui/flying.rhai index a38bcc6..5962157 100644 --- a/content/ui/flying.rhai +++ b/content/ui/flying.rhai @@ -1,6 +1,11 @@ -fn init(state) { - +fn config() { + let config = SceneConfig(); + config.show_starfield(true); + config.show_phys(true); + return config +} +fn init(state) { let ring = SpriteBuilder( "ring", "ui::status", @@ -40,5 +45,11 @@ fn init(state) { ]; } -fn hover(element, hover_state) {} -fn click(element, click_state) {} \ No newline at end of file +fn event(state, event) { + if type_of(event) == "PlayerShipStateEvent" { + if state.player_ship().is_landed() { + return SceneAction::GoTo("landed"); + } + return; + } +} \ No newline at end of file diff --git a/content/ui/landed.rhai b/content/ui/landed.rhai index 3b4b1e8..202fa99 100644 --- a/content/ui/landed.rhai +++ b/content/ui/landed.rhai @@ -1,3 +1,9 @@ +fn config() { + let config = SceneConfig(); + config.show_starfield(true); + config.show_phys(false); + return config +} fn init(state) { let frame = SpriteBuilder( @@ -12,7 +18,7 @@ fn init(state) { let landscape = SpriteBuilder( "landscape", - state.planet_landscape, + "ui::landscape::test", Rect( -180.0, 142.0, 274.0, 135.0, SpriteAnchor::NorthWest, @@ -40,7 +46,7 @@ fn init(state) { SpriteAnchor::Center ) ); - title.set_text(state.planet_name); + title.set_text("Title"); return [ button, @@ -50,22 +56,36 @@ fn init(state) { ]; } -fn hover(element, hover_state) { - if element.has_name("button") { - if hover_state { - element.take_edge("on:top", 0.1); - } else { - element.take_edge("off:top", 0.1); +fn event(state, event) { + if type_of(event) == "MouseHoverEvent" { + let element = event.element(); + if element.has_name("button") { + if event.is_enter() { + element.take_edge("on:top", 0.1); + } else { + element.take_edge("off:top", 0.1); + } } + return; } -} - -fn click(element, click_state) { - if !click_state { - return SceneAction::None; + + + if type_of(event) == "MouseClickEvent" { + if !event.is_down() { + return SceneAction::None; + } + + let element = event.element(); + if element.has_name("button") { + return SceneAction::GoTo("outfitter"); + } + return; } - if element.has_name("button") { - return SceneAction::SceneOutfitter; + if type_of(event) == "PlayerShipStateEvent" { + if !state.player_ship().is_landed() { + return SceneAction::GoTo("flying"); + } + return; } } \ No newline at end of file diff --git a/content/ui/outfitter.rhai b/content/ui/outfitter.rhai index b66d312..c778c47 100644 --- a/content/ui/outfitter.rhai +++ b/content/ui/outfitter.rhai @@ -1,3 +1,9 @@ +fn config() { + let config = SceneConfig(); + config.show_starfield(true); + config.show_phys(false); + return config +} fn init(state) { let se_box = SpriteBuilder( @@ -19,7 +25,7 @@ fn init(state) { SpriteAnchor::SouthWest ) ); - exit_text.set_text(state.planet_name); + exit_text.set_text("Earth"); let exit_button = SpriteBuilder( "exit_button", @@ -64,7 +70,7 @@ fn init(state) { SpriteAnchor::NorthWest ) ); - ship_name.set_text(state.planet_name); + ship_name.set_text("Earth"); let ship_type = TextBoxBuilder( "ship_type", @@ -75,7 +81,7 @@ fn init(state) { SpriteAnchor::NorthWest ) ); - ship_type.set_text(state.planet_name); + ship_type.set_text("Earth"); let ship_stats = TextBoxBuilder( "ship_stats", @@ -86,7 +92,7 @@ fn init(state) { SpriteAnchor::NorthWest, ) ); - ship_stats.set_text(state.planet_name); + ship_stats.set_text("Earth"); @@ -123,7 +129,7 @@ fn init(state) { SpriteAnchor::NorthEast, ) ); - outfit_name.set_text(state.planet_name); + outfit_name.set_text("Earth"); let outfit_desc = TextBoxBuilder( "outfit_desc", @@ -134,7 +140,7 @@ fn init(state) { SpriteAnchor::NorthEast, ) ); - outfit_desc.set_text(state.planet_name); + outfit_desc.set_text("Earth"); let outfit_stats = TextBoxBuilder( "outfit_stats", @@ -145,7 +151,7 @@ fn init(state) { SpriteAnchor::NorthEast, ) ); - outfit_stats.set_text(state.planet_name); + outfit_stats.set_text("Earth"); return [ ship_bg, @@ -166,22 +172,37 @@ fn init(state) { ]; } -fn hover(element, hover_state) { - if element.has_name("exit_button") { - if hover_state { - element.take_edge("on:top", 0.1); - } else { - element.take_edge("off:top", 0.1); + +fn event(state, event) { + if type_of(event) == "MouseHoverEvent" { + let element = event.element(); + if element.has_name("exit_button") { + if event.is_enter() { + element.take_edge("on:top", 0.1); + } else { + element.take_edge("off:top", 0.1); + } } + return; } -} - -fn click(element, click_state) { - if !click_state { - return SceneAction::None; + + + if type_of(event) == "MouseClickEvent" { + if !event.is_down() { + return SceneAction::None; + } + + let element = event.element(); + if element.has_name("exit_button") { + return SceneAction::GoTo("landed"); + } + return; } - if element.has_name("exit_button") { - return SceneAction::SceneLanded; + if type_of(event) == "PlayerShipStateEvent" { + if !state.player_ship().is_landed() { + return SceneAction::GoTo("flying"); + } + return; } } \ No newline at end of file diff --git a/crates/galactica/src/main.rs b/crates/galactica/src/main.rs index be41771..9581f5d 100644 --- a/crates/galactica/src/main.rs +++ b/crates/galactica/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Result}; use clap::Parser; use galactica_content::{Content, SystemHandle}; use galactica_playeragent::{PlayerAgent, PlayerStatus}; -use galactica_render::{RenderInput, RenderScenes}; +use galactica_render::RenderInput; use galactica_system::{ data::ShipState, phys::{PhysImage, PhysSimShipHandle}, @@ -122,21 +122,19 @@ fn try_main() -> Result<()> { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, content.clone()))?; - gpu.set_scene(RenderScenes::System); gpu.init(&content); // TODO: don't clone content let mut game = game::Game::new(content.clone()); let p = game.make_player(); - let mut player = PlayerAgent::new(p.0); - player.set_camera_aspect( + let mut player = Rc::new(PlayerAgent::new(p.0)); + Rc::get_mut(&mut player).unwrap().set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, ); - let mut phys_img = PhysImage::new(); + let mut phys_img = Rc::new(PhysImage::new()); let mut last_run = Instant::now(); - let mut was_landed = false; event_loop.run(move |event, _, control_flow| { match event { @@ -146,8 +144,8 @@ fn try_main() -> Result<()> { camera_zoom: player.camera.zoom, current_time: game.get_current_time(), ct: content.clone(), - phys_img: &phys_img, - player: &player, + phys_img: phys_img.clone(), + player: player.clone(), time_since_last_run: last_run.elapsed().as_secs_f32(), current_system: SystemHandle { index: 0 }, timing: game.get_timing().clone(), @@ -164,9 +162,9 @@ fn try_main() -> Result<()> { } Event::MainEventsCleared => { - game.update_player_controls(&mut player); + game.update_player_controls(Rc::get_mut(&mut player).unwrap()); game.step(&phys_img); - game.update_image(&mut phys_img); + game.update_image(Rc::get_mut(&mut phys_img).unwrap()); // TODO: clean up let player_status = { @@ -177,21 +175,9 @@ fn try_main() -> Result<()> { ShipState::Landing { .. } | ShipState::UnLanding { .. } | ShipState::Collapsing { .. } - | ShipState::Flying { .. } => { - if was_landed { - was_landed = false; - gpu.set_scene(RenderScenes::System); - } - - Some(*o.rigidbody.translation()) - } + | ShipState::Flying { .. } => Some(*o.rigidbody.translation()), ShipState::Landed { target } => { - if !was_landed { - was_landed = true; - gpu.set_scene(RenderScenes::Landed); - } - let b = content.get_system_object(*target); Some(Vector2::new(b.pos.x, b.pos.y)) } @@ -207,9 +193,10 @@ fn try_main() -> Result<()> { }; // This must be updated BEFORE rendering! - player.step(&content, player_status); - - player.input.clear_inputs(); + Rc::get_mut(&mut player) + .unwrap() + .step(&content, player_status); + Rc::get_mut(&mut player).unwrap().input.clear_inputs(); gpu.window().request_redraw(); } @@ -230,27 +217,39 @@ fn try_main() -> Result<()> { }, .. } => { - player.input.process_key(state, key); + Rc::get_mut(&mut player) + .unwrap() + .input + .process_key(state, key); } WindowEvent::CursorMoved { position, .. } => { - player.input.process_mouse(position); + Rc::get_mut(&mut player) + .unwrap() + .input + .process_mouse(position); } WindowEvent::MouseInput { state, button, .. } => { - player.input.process_click(state, button); + Rc::get_mut(&mut player) + .unwrap() + .input + .process_click(state, button); } WindowEvent::MouseWheel { delta, phase, .. } => { - player.input.process_scroll(delta, phase); + Rc::get_mut(&mut player) + .unwrap() + .input + .process_scroll(delta, phase); } WindowEvent::Resized(_) => { gpu.resize(&content); - player.set_camera_aspect( + Rc::get_mut(&mut player).unwrap().set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, ); } WindowEvent::ScaleFactorChanged { .. } => { gpu.resize(&content); - player.set_camera_aspect( + Rc::get_mut(&mut player).unwrap().set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, ); diff --git a/crates/playeragent/src/inputstatus.rs b/crates/playeragent/src/inputstatus.rs index 4159b7d..d3b8945 100644 --- a/crates/playeragent/src/inputstatus.rs +++ b/crates/playeragent/src/inputstatus.rs @@ -3,6 +3,7 @@ use winit::{ event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}, }; +#[derive(Debug)] pub struct InputStatus { // Parameters scroll_speed: f32, diff --git a/crates/playeragent/src/playeragent.rs b/crates/playeragent/src/playeragent.rs index bc89b7c..fcfa168 100644 --- a/crates/playeragent/src/playeragent.rs +++ b/crates/playeragent/src/playeragent.rs @@ -25,6 +25,7 @@ impl PlayerSelection { } } +#[derive(Debug)] pub struct PlayerAgent { /// Which ship this player is controlling pub ship: Option, diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index d691ab5..fd6964f 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -1,22 +1,24 @@ -use std::rc::Rc; +use std::{iter, rc::Rc}; use anyhow::Result; use bytemuck; use galactica_content::Content; -use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer}; -use log::debug; +use galactica_system::data::ShipState; +use galactica_util::to_radians; +use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer}; +use nalgebra::{Point2, Point3}; use wgpu; use winit; use crate::{ - globaluniform::{GlobalDataContent, GlobalUniform}, + globaluniform::{GlobalDataContent, GlobalUniform, ObjectData}, pipeline::PipelineBuilder, - renderscene::{LandedScene, RenderScene, SystemScene}, shaderprocessor::preprocess_shader, starfield::Starfield, texturearray::TextureArray, - ui::{UiManager, UiScene}, - RenderInput, RenderScenes, RenderState, VertexBuffers, + ui::UiManager, + vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance}, + RenderInput, RenderState, VertexBuffers, }; /// A high-level GPU wrapper. Reads game state (via RenderInput), produces pretty pictures. @@ -32,7 +34,6 @@ pub struct GPUState { pub(crate) texture_array: TextureArray, pub(crate) state: RenderState, pub(crate) ui: UiManager, - pub(crate) scene: RenderScenes, } impl GPUState { @@ -238,7 +239,6 @@ impl GPUState { starfield_pipeline, ui_pipeline, radialbar_pipeline, - scene: RenderScenes::Landed, state, }); } @@ -250,18 +250,6 @@ impl GPUState { &self.state.window } - /// Change the current scenection - pub fn set_scene(&mut self, scene: RenderScenes) { - debug!("switching to {:?}", scene); - - match scene { - RenderScenes::Landed => self.ui.set_scene(&mut self.state, UiScene::Landed).unwrap(), - RenderScenes::System => self.ui.set_scene(&mut self.state, UiScene::Flying).unwrap(), - }; - - self.scene = scene; - } - /// Update window size. /// This should be called whenever our window is resized. pub fn resize(&mut self, ct: &Content) { @@ -290,6 +278,8 @@ impl GPUState { /// Main render function. Draws sprites on a window. pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> { + let input = Rc::new(input); + // Update global values self.state.queue.write_buffer( &self.state.global_uniform.data_buffer, @@ -313,11 +303,453 @@ impl GPUState { self.state.frame_reset(); - match self.scene { - RenderScenes::System => SystemScene::render(self, &input).unwrap(), - RenderScenes::Landed => LandedScene::render(self, &input).unwrap(), + let output = self.surface.get_current_texture()?; + let view = output.texture.create_view(&Default::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("render encoder"), + }); + + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("render pass"), + + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + if self.ui.get_config().show_phys { + // Create sprite instances + + // Game coordinates (relative to camera) of ne and sw corners of screen. + // Used to skip off-screen sprites. + let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom; + let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom; + + // Order matters, it determines what is drawn on top. + // The order inside ships and projectiles doesn't matter, + // but ships should always be under projectiles. + self.push_system(&input, (clip_ne, clip_sw)); + self.push_ships(&input, (clip_ne, clip_sw)); + self.push_projectiles(&input, (clip_ne, clip_sw)); + self.push_effects(&input, (clip_ne, clip_sw)); } + self.ui.draw(input.clone(), &mut self.state).unwrap(); + + // These should match the indices in each shader, + // and should each have a corresponding bind group layout. + render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); + render_pass.set_bind_group(1, &self.state.global_uniform.bind_group, &[]); + + if self.ui.get_config().show_starfield { + // Starfield pipeline + self.state + .vertex_buffers + .get_starfield() + .set_in_pass(&mut render_pass); + render_pass.set_pipeline(&self.starfield_pipeline); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..self.state.get_starfield_counter(), + ); + } + + if self.ui.get_config().show_phys { + // Sprite pipeline + self.state + .vertex_buffers + .get_object() + .set_in_pass(&mut render_pass); + render_pass.set_pipeline(&self.object_pipeline); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..self.state.get_object_counter(), + ); + } + + // Ui pipeline + self.state + .vertex_buffers + .get_ui() + .set_in_pass(&mut render_pass); + render_pass.set_pipeline(&self.ui_pipeline); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..self.state.get_ui_counter(), + ); + + // Radial progress bars + // TODO: do we need to do this every time? + self.state + .vertex_buffers + .get_radialbar() + .set_in_pass(&mut render_pass); + render_pass.set_pipeline(&self.radialbar_pipeline); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..self.state.get_radialbar_counter(), + ); + + let textareas = self.ui.get_textareas(&input, &self.state); + self.state + .text_renderer + .prepare( + &self.device, + &self.state.queue, + &mut self.state.text_font_system, + &mut self.state.text_atlas, + Resolution { + width: self.state.window_size.width, + height: self.state.window_size.height, + }, + textareas, + &mut self.state.text_cache, + ) + .unwrap(); + + self.state + .text_renderer + .render(&self.state.text_atlas, &mut render_pass) + .unwrap(); + + // begin_render_pass borrows encoder mutably, + // so we need to drop it before calling finish. + drop(render_pass); + + self.state.queue.submit(iter::once(encoder.finish())); + output.present(); + return Ok(()); } } + +impl GPUState { + fn push_ships( + &mut self, + input: &RenderInput, + // NE and SW corners of screen + screen_clip: (Point2, Point2), + ) { + for s in input.phys_img.iter_ships() { + let ship_pos; + let ship_ang; + let ship_cnt; + match s.ship.get_data().get_state() { + ShipState::Dead | ShipState::Landed { .. } => continue, + + ShipState::Collapsing { .. } | ShipState::Flying { .. } => { + let r = &s.rigidbody; + let pos = *r.translation(); + ship_pos = Point3::new(pos.x, pos.y, 1.0); + let ship_rot = r.rotation(); + ship_ang = ship_rot.angle(); + ship_cnt = input.ct.get_ship(s.ship.get_data().get_content()); + } + + ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => { + let r = &s.rigidbody; + let pos = *r.translation(); + ship_pos = Point3::new(pos.x, pos.y, *current_z); + let ship_rot = r.rotation(); + ship_ang = ship_rot.angle(); + ship_cnt = input.ct.get_ship(s.ship.get_data().get_content()); + } + } + + // Position adjusted for parallax + // TODO: adjust parallax for zoom? + // 1.0 is z-coordinate, which is constant for ships + let pos: Point2 = + (Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z; + + // Game dimensions of this sprite post-scale. + // Post-scale width or height, whichever is larger. + // This is in game units. + // + // We take the maximum to account for rotated sprites. + let m = + (ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0); + + // Don't draw sprites that are off the screen + if pos.x < screen_clip.0.x - m + || pos.y > screen_clip.0.y + m + || pos.x > screen_clip.1.x + m + || pos.y < screen_clip.1.y - m + { + continue; + } + + let idx = self.state.get_object_counter(); + // Write this object's location data + self.state.queue.write_buffer( + &self.state.global_uniform.object_buffer, + ObjectData::SIZE * idx as u64, + bytemuck::cast_slice(&[ObjectData { + xpos: ship_pos.x, + ypos: ship_pos.y, + zpos: ship_pos.z, + angle: ship_ang, + size: ship_cnt.size, + parent: 0, + is_child: 0, + _padding: Default::default(), + }]), + ); + + // Push this object's instance + let anim_state = s.ship.get_anim_state(); + self.state.push_object_buffer(ObjectInstance { + texture_index: anim_state.texture_index(), + texture_fade: anim_state.fade, + object_index: idx as u32, + color: [1.0, 1.0, 1.0, 1.0], + }); + + if { + let is_flying = match s.ship.get_data().get_state() { + ShipState::Flying { .. } + | ShipState::UnLanding { .. } + | ShipState::Landing { .. } => true, + _ => false, + }; + is_flying + } { + for (engine_point, anim) in s.ship.iter_engine_anim() { + self.state.queue.write_buffer( + &self.state.global_uniform.object_buffer, + ObjectData::SIZE * self.state.get_object_counter() as u64, + bytemuck::cast_slice(&[ObjectData { + // Note that we adjust the y-coordinate for half-height, + // not the x-coordinate, even though our ships point east + // at 0 degrees. This is because this is placed pre-rotation, + // and the parent rotation adjustment in our object shader + // automatically accounts for this. + xpos: engine_point.pos.x, + ypos: engine_point.pos.y - engine_point.size / 2.0, + zpos: 1.0, + // We still need an adjustment here, though, + // since engine sprites point north (with exhaust towards the south) + angle: to_radians(90.0), + size: engine_point.size, + parent: idx as u32, + is_child: 1, + _padding: Default::default(), + }]), + ); + + let anim_state = anim.get_texture_idx(); + self.state.push_object_buffer(ObjectInstance { + texture_index: anim_state.texture_index(), + texture_fade: anim_state.fade, + object_index: self.state.get_object_counter() as u32, + color: [1.0, 1.0, 1.0, 1.0], + }); + } + } + } + } + + fn push_projectiles( + &mut self, + input: &RenderInput, + // NE and SW corners of screen + screen_clip: (Point2, Point2), + ) { + for p in input.phys_img.iter_projectiles() { + let r = &p.rigidbody; + let proj_pos = *r.translation(); + let proj_rot = r.rotation(); + let proj_ang = proj_rot.angle(); + let proj_cnt = &p.projectile.content; // TODO: don't clone this? + + // Position adjusted for parallax + // TODO: adjust parallax for zoom? + // 1.0 is z-coordinate, which is constant for projectiles + let pos = (proj_pos - input.camera_pos) / 1.0; + + // Game dimensions of this sprite post-scale. + // Post-scale width or height, whichever is larger. + // This is in game units. + // + // We take the maximum to account for rotated sprites. + let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0); + + // Don't draw sprites that are off the screen + if pos.x < screen_clip.0.x - m + || pos.y > screen_clip.0.y + m + || pos.x > screen_clip.1.x + m + || pos.y < screen_clip.1.y - m + { + continue; + } + + let idx = self.state.get_object_counter(); + // Write this object's location data + self.state.queue.write_buffer( + &self.state.global_uniform.object_buffer, + ObjectData::SIZE * idx as u64, + bytemuck::cast_slice(&[ObjectData { + xpos: proj_pos.x, + ypos: proj_pos.y, + zpos: 1.0, + angle: proj_ang, + size: 0f32.max(proj_cnt.size + p.projectile.size_rng), + parent: 0, + is_child: 0, + _padding: Default::default(), + }]), + ); + + let anim_state = p.projectile.get_anim_state(); + self.state.push_object_buffer(ObjectInstance { + texture_index: anim_state.texture_index(), + texture_fade: anim_state.fade, + object_index: idx as u32, + color: [1.0, 1.0, 1.0, 1.0], + }); + } + } + + fn push_system( + &mut self, + input: &RenderInput, + // NE and SW corners of screen + screen_clip: (Point2, Point2), + ) { + let system = input.ct.get_system(input.current_system); + + for o in &system.objects { + // Position adjusted for parallax + let pos: Point2 = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z; + + // Game dimensions of this sprite post-scale. + // Post-scale width or height, whichever is larger. + // This is in game units. + // + // We take the maximum to account for rotated sprites. + let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0); + + // Don't draw sprites that are off the screen + if pos.x < screen_clip.0.x - m + || pos.y > screen_clip.0.y + m + || pos.x > screen_clip.1.x + m + || pos.y < screen_clip.1.y - m + { + continue; + } + + let idx = self.state.get_object_counter(); + // Write this object's location data + self.state.queue.write_buffer( + &self.state.global_uniform.object_buffer, + ObjectData::SIZE * idx as u64, + bytemuck::cast_slice(&[ObjectData { + xpos: o.pos.x, + ypos: o.pos.y, + zpos: o.pos.z, + angle: o.angle, + size: o.size, + parent: 0, + is_child: 0, + _padding: Default::default(), + }]), + ); + + let sprite = input.ct.get_sprite(o.sprite); + let texture_a = sprite.get_first_frame(); // ANIMATE + + // Push this object's instance + self.state.push_object_buffer(ObjectInstance { + texture_index: [texture_a, texture_a], + texture_fade: 1.0, + object_index: idx as u32, + color: [1.0, 1.0, 1.0, 1.0], + }); + } + } + + fn push_effects( + &mut self, + input: &RenderInput, + // NE and SW corners of screen + screen_clip: (Point2, Point2), + ) { + for p in input.phys_img.iter_effects() { + let r = &p.rigidbody; + let pos = *r.translation(); + let rot = r.rotation(); + let ang = rot.angle(); + + // Position adjusted for parallax + // TODO: adjust parallax for zoom? + // 1.0 is z-coordinate, which is constant for projectiles + let adjusted_pos = (pos - input.camera_pos) / 1.0; + + // Game dimensions of this sprite post-scale. + // Post-scale width or height, whichever is larger. + // This is in game units. + // + // We take the maximum to account for rotated sprites. + let m = (p.effect.size / 1.0) + * input + .ct + .get_sprite(p.effect.anim.get_sprite()) + .aspect + .max(1.0); + + // Don't draw sprites that are off the screen + if adjusted_pos.x < screen_clip.0.x - m + || adjusted_pos.y > screen_clip.0.y + m + || adjusted_pos.x > screen_clip.1.x + m + || adjusted_pos.y < screen_clip.1.y - m + { + continue; + } + + let idx = self.state.get_object_counter(); + // Write this object's location data + self.state.queue.write_buffer( + &self.state.global_uniform.object_buffer, + ObjectData::SIZE * idx as u64, + bytemuck::cast_slice(&[ObjectData { + xpos: pos.x, + ypos: pos.y, + zpos: 1.0, + angle: ang, + size: p.effect.size, + parent: 0, + is_child: 0, + _padding: Default::default(), + }]), + ); + + let anim_state = p.effect.anim.get_texture_idx(); + self.state.push_object_buffer(ObjectInstance { + texture_index: anim_state.texture_index(), + texture_fade: anim_state.fade, + object_index: idx as u32, + color: [1.0, 1.0, 1.0, p.get_fade()], + }); + } + } +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 3cd8af3..7c65bcc 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -12,7 +12,6 @@ mod gpustate; mod pipeline; mod positionanchor; mod renderinput; -mod renderscene; mod renderstate; mod shaderprocessor; mod starfield; @@ -23,7 +22,6 @@ mod vertexbuffer; pub use gpustate::GPUState; pub use positionanchor::PositionAnchor; pub use renderinput::RenderInput; -pub use renderscene::RenderScenes; use renderstate::*; use nalgebra::Matrix4; diff --git a/crates/render/src/renderinput.rs b/crates/render/src/renderinput.rs index c0bd69e..19d9db6 100644 --- a/crates/render/src/renderinput.rs +++ b/crates/render/src/renderinput.rs @@ -7,12 +7,13 @@ use galactica_util::timing::Timing; use nalgebra::Vector2; /// Bundles parameters passed to a single call to GPUState::render -pub struct RenderInput<'a> { +#[derive(Debug)] +pub struct RenderInput { /// Camera position, in world units pub camera_pos: Vector2, /// Player ship data - pub player: &'a PlayerAgent, + pub player: Rc, /// The system we're currently in pub current_system: SystemHandle, @@ -21,7 +22,7 @@ pub struct RenderInput<'a> { pub camera_zoom: f32, /// The world state to render - pub phys_img: &'a PhysImage, + pub phys_img: Rc, // TODO: handle overflow. is it a problem? /// The current time, in seconds diff --git a/crates/render/src/renderscene/landed.rs b/crates/render/src/renderscene/landed.rs deleted file mode 100644 index 7e2a0ac..0000000 --- a/crates/render/src/renderscene/landed.rs +++ /dev/null @@ -1,117 +0,0 @@ -use anyhow::Result; -use glyphon::Resolution; -use std::iter; - -use super::RenderScene; -use crate::vertexbuffer::consts::SPRITE_INDICES; - -pub struct LandedScene {} - -impl RenderScene for LandedScene { - fn render(g: &mut crate::GPUState, input: &crate::RenderInput) -> Result<()> { - let output = g.surface.get_current_texture()?; - let view = output.texture.create_view(&Default::default()); - - let mut encoder = g - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render encoder"), - }); - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render pass"), - - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - - // Create sprite instances - g.ui.draw(&input, &mut g.state)?; - - // These should match the indices in each shader, - // and should each have a corresponding bind group layout. - render_pass.set_bind_group(0, &g.texture_array.bind_group, &[]); - render_pass.set_bind_group(1, &g.state.global_uniform.bind_group, &[]); - - // Starfield pipeline - g.state - .vertex_buffers - .get_starfield() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.starfield_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_starfield_counter(), - ); - - // Ui pipeline - g.state - .vertex_buffers - .get_ui() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.ui_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_ui_counter(), - ); - - // Radial progress bars - g.state - .vertex_buffers - .get_radialbar() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.radialbar_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_radialbar_counter(), - ); - - let textareas = g.ui.get_textareas(input, &g.state); - g.state - .text_renderer - .prepare( - &g.device, - &g.state.queue, - &mut g.state.text_font_system, - &mut g.state.text_atlas, - Resolution { - width: g.state.window_size.width, - height: g.state.window_size.height, - }, - textareas, - &mut g.state.text_cache, - ) - .unwrap(); - - g.state - .text_renderer - .render(&g.state.text_atlas, &mut render_pass) - .unwrap(); - - // begin_render_pass borrows encoder mutably, - // so we need to drop it before calling finish. - drop(render_pass); - - g.state.queue.submit(iter::once(encoder.finish())); - output.present(); - - return Ok(()); - } -} diff --git a/crates/render/src/renderscene/mod.rs b/crates/render/src/renderscene/mod.rs deleted file mode 100644 index cb30de5..0000000 --- a/crates/render/src/renderscene/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod landed; -mod system; - -use std::fmt::Debug; - -pub use landed::LandedScene; -pub use system::SystemScene; - -use crate::{GPUState, RenderInput}; -use anyhow::Result; - -pub trait RenderScene { - fn render(g: &mut GPUState, input: &RenderInput) -> Result<()>; -} - -/// What render routine to run -pub enum RenderScenes { - /// Draw the system we're in - System, - - /// Draw the landed UI - Landed, -} - -impl Debug for RenderScenes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Landed => write!(f, "RenderScenes::Landed"), - Self::System => write!(f, "RenderScenes::System"), - } - } -} diff --git a/crates/render/src/renderscene/system/mod.rs b/crates/render/src/renderscene/system/mod.rs deleted file mode 100644 index 466f2f0..0000000 --- a/crates/render/src/renderscene/system/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod phys; -mod system; - -pub use system::SystemScene; diff --git a/crates/render/src/renderscene/system/phys.rs b/crates/render/src/renderscene/system/phys.rs deleted file mode 100644 index 10a5e03..0000000 --- a/crates/render/src/renderscene/system/phys.rs +++ /dev/null @@ -1,321 +0,0 @@ -use bytemuck; -use galactica_system::data::ShipState; -use galactica_util::to_radians; -use nalgebra::{Point2, Point3}; - -use crate::{ - globaluniform::ObjectData, vertexbuffer::types::ObjectInstance, GPUState, RenderInput, -}; - -use super::SystemScene; - -impl SystemScene { - pub(super) fn push_ships( - g: &mut GPUState, - input: &RenderInput, - // NE and SW corners of screen - screen_clip: (Point2, Point2), - ) { - for s in input.phys_img.iter_ships() { - let ship_pos; - let ship_ang; - let ship_cnt; - match s.ship.get_data().get_state() { - ShipState::Dead | ShipState::Landed { .. } => continue, - - ShipState::Collapsing { .. } | ShipState::Flying { .. } => { - let r = &s.rigidbody; - let pos = *r.translation(); - ship_pos = Point3::new(pos.x, pos.y, 1.0); - let ship_rot = r.rotation(); - ship_ang = ship_rot.angle(); - ship_cnt = input.ct.get_ship(s.ship.get_data().get_content()); - } - - ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => { - let r = &s.rigidbody; - let pos = *r.translation(); - ship_pos = Point3::new(pos.x, pos.y, *current_z); - let ship_rot = r.rotation(); - ship_ang = ship_rot.angle(); - ship_cnt = input.ct.get_ship(s.ship.get_data().get_content()); - } - } - - // Position adjusted for parallax - // TODO: adjust parallax for zoom? - // 1.0 is z-coordinate, which is constant for ships - let pos: Point2 = - (Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z; - - // Game dimensions of this sprite post-scale. - // Post-scale width or height, whichever is larger. - // This is in game units. - // - // We take the maximum to account for rotated sprites. - let m = - (ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0); - - // Don't draw sprites that are off the screen - if pos.x < screen_clip.0.x - m - || pos.y > screen_clip.0.y + m - || pos.x > screen_clip.1.x + m - || pos.y < screen_clip.1.y - m - { - continue; - } - - let idx = g.state.get_object_counter(); - // Write this object's location data - g.state.queue.write_buffer( - &g.state.global_uniform.object_buffer, - ObjectData::SIZE * idx as u64, - bytemuck::cast_slice(&[ObjectData { - xpos: ship_pos.x, - ypos: ship_pos.y, - zpos: ship_pos.z, - angle: ship_ang, - size: ship_cnt.size, - parent: 0, - is_child: 0, - _padding: Default::default(), - }]), - ); - - // Push this object's instance - let anim_state = s.ship.get_anim_state(); - g.state.push_object_buffer(ObjectInstance { - texture_index: anim_state.texture_index(), - texture_fade: anim_state.fade, - object_index: idx as u32, - color: [1.0, 1.0, 1.0, 1.0], - }); - - if { - let is_flying = match s.ship.get_data().get_state() { - ShipState::Flying { .. } - | ShipState::UnLanding { .. } - | ShipState::Landing { .. } => true, - _ => false, - }; - is_flying - } { - for (engine_point, anim) in s.ship.iter_engine_anim() { - g.state.queue.write_buffer( - &g.state.global_uniform.object_buffer, - ObjectData::SIZE * g.state.get_object_counter() as u64, - bytemuck::cast_slice(&[ObjectData { - // Note that we adjust the y-coordinate for half-height, - // not the x-coordinate, even though our ships point east - // at 0 degrees. This is because this is placed pre-rotation, - // and the parent rotation adjustment in our object shader - // automatically accounts for this. - xpos: engine_point.pos.x, - ypos: engine_point.pos.y - engine_point.size / 2.0, - zpos: 1.0, - // We still need an adjustment here, though, - // since engine sprites point north (with exhaust towards the south) - angle: to_radians(90.0), - size: engine_point.size, - parent: idx as u32, - is_child: 1, - _padding: Default::default(), - }]), - ); - - let anim_state = anim.get_texture_idx(); - g.state.push_object_buffer(ObjectInstance { - texture_index: anim_state.texture_index(), - texture_fade: anim_state.fade, - object_index: g.state.get_object_counter() as u32, - color: [1.0, 1.0, 1.0, 1.0], - }); - } - } - } - } - - pub(super) fn push_projectiles( - g: &mut GPUState, - input: &RenderInput, - // NE and SW corners of screen - screen_clip: (Point2, Point2), - ) { - for p in input.phys_img.iter_projectiles() { - let r = &p.rigidbody; - let proj_pos = *r.translation(); - let proj_rot = r.rotation(); - let proj_ang = proj_rot.angle(); - let proj_cnt = &p.projectile.content; // TODO: don't clone this? - - // Position adjusted for parallax - // TODO: adjust parallax for zoom? - // 1.0 is z-coordinate, which is constant for projectiles - let pos = (proj_pos - input.camera_pos) / 1.0; - - // Game dimensions of this sprite post-scale. - // Post-scale width or height, whichever is larger. - // This is in game units. - // - // We take the maximum to account for rotated sprites. - let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0); - - // Don't draw sprites that are off the screen - if pos.x < screen_clip.0.x - m - || pos.y > screen_clip.0.y + m - || pos.x > screen_clip.1.x + m - || pos.y < screen_clip.1.y - m - { - continue; - } - - let idx = g.state.get_object_counter(); - // Write this object's location data - g.state.queue.write_buffer( - &g.state.global_uniform.object_buffer, - ObjectData::SIZE * idx as u64, - bytemuck::cast_slice(&[ObjectData { - xpos: proj_pos.x, - ypos: proj_pos.y, - zpos: 1.0, - angle: proj_ang, - size: 0f32.max(proj_cnt.size + p.projectile.size_rng), - parent: 0, - is_child: 0, - _padding: Default::default(), - }]), - ); - - let anim_state = p.projectile.get_anim_state(); - g.state.push_object_buffer(ObjectInstance { - texture_index: anim_state.texture_index(), - texture_fade: anim_state.fade, - object_index: idx as u32, - color: [1.0, 1.0, 1.0, 1.0], - }); - } - } - - pub(super) fn push_system( - g: &mut GPUState, - input: &RenderInput, - // NE and SW corners of screen - screen_clip: (Point2, Point2), - ) { - let system = input.ct.get_system(input.current_system); - - for o in &system.objects { - // Position adjusted for parallax - let pos: Point2 = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z; - - // Game dimensions of this sprite post-scale. - // Post-scale width or height, whichever is larger. - // This is in game units. - // - // We take the maximum to account for rotated sprites. - let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0); - - // Don't draw sprites that are off the screen - if pos.x < screen_clip.0.x - m - || pos.y > screen_clip.0.y + m - || pos.x > screen_clip.1.x + m - || pos.y < screen_clip.1.y - m - { - continue; - } - - let idx = g.state.get_object_counter(); - // Write this object's location data - g.state.queue.write_buffer( - &g.state.global_uniform.object_buffer, - ObjectData::SIZE * idx as u64, - bytemuck::cast_slice(&[ObjectData { - xpos: o.pos.x, - ypos: o.pos.y, - zpos: o.pos.z, - angle: o.angle, - size: o.size, - parent: 0, - is_child: 0, - _padding: Default::default(), - }]), - ); - - let sprite = input.ct.get_sprite(o.sprite); - let texture_a = sprite.get_first_frame(); // ANIMATE - - // Push this object's instance - g.state.push_object_buffer(ObjectInstance { - texture_index: [texture_a, texture_a], - texture_fade: 1.0, - object_index: idx as u32, - color: [1.0, 1.0, 1.0, 1.0], - }); - } - } - - pub(super) fn push_effects( - g: &mut GPUState, - input: &RenderInput, - // NE and SW corners of screen - screen_clip: (Point2, Point2), - ) { - for p in input.phys_img.iter_effects() { - let r = &p.rigidbody; - let pos = *r.translation(); - let rot = r.rotation(); - let ang = rot.angle(); - - // Position adjusted for parallax - // TODO: adjust parallax for zoom? - // 1.0 is z-coordinate, which is constant for projectiles - let adjusted_pos = (pos - input.camera_pos) / 1.0; - - // Game dimensions of this sprite post-scale. - // Post-scale width or height, whichever is larger. - // This is in game units. - // - // We take the maximum to account for rotated sprites. - let m = (p.effect.size / 1.0) - * input - .ct - .get_sprite(p.effect.anim.get_sprite()) - .aspect - .max(1.0); - - // Don't draw sprites that are off the screen - if adjusted_pos.x < screen_clip.0.x - m - || adjusted_pos.y > screen_clip.0.y + m - || adjusted_pos.x > screen_clip.1.x + m - || adjusted_pos.y < screen_clip.1.y - m - { - continue; - } - - let idx = g.state.get_object_counter(); - // Write this object's location data - g.state.queue.write_buffer( - &g.state.global_uniform.object_buffer, - ObjectData::SIZE * idx as u64, - bytemuck::cast_slice(&[ObjectData { - xpos: pos.x, - ypos: pos.y, - zpos: 1.0, - angle: ang, - size: p.effect.size, - parent: 0, - is_child: 0, - _padding: Default::default(), - }]), - ); - - let anim_state = p.effect.anim.get_texture_idx(); - g.state.push_object_buffer(ObjectInstance { - texture_index: anim_state.texture_index(), - texture_fade: anim_state.fade, - object_index: idx as u32, - color: [1.0, 1.0, 1.0, p.get_fade()], - }); - } - } -} diff --git a/crates/render/src/renderscene/system/system.rs b/crates/render/src/renderscene/system/system.rs deleted file mode 100644 index d02343e..0000000 --- a/crates/render/src/renderscene/system/system.rs +++ /dev/null @@ -1,144 +0,0 @@ -use anyhow::Result; -use glyphon::Resolution; -use nalgebra::Point2; -use std::iter; - -use super::super::RenderScene; -use crate::vertexbuffer::consts::SPRITE_INDICES; - -pub struct SystemScene {} - -impl RenderScene for SystemScene { - fn render(g: &mut crate::GPUState, input: &crate::RenderInput) -> Result<()> { - let output = g.surface.get_current_texture()?; - let view = output.texture.create_view(&Default::default()); - - let mut encoder = g - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render encoder"), - }); - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render pass"), - - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - - // Create sprite instances - - // Game coordinates (relative to camera) of ne and sw corners of screen. - // Used to skip off-screen sprites. - let clip_ne = Point2::new(-g.state.window_aspect, 1.0) * input.camera_zoom; - let clip_sw = Point2::new(g.state.window_aspect, -1.0) * input.camera_zoom; - - // Order matters, it determines what is drawn on top. - // The order inside ships and projectiles doesn't matter, - // but ships should always be under projectiles. - Self::push_system(g, &input, (clip_ne, clip_sw)); - Self::push_ships(g, &input, (clip_ne, clip_sw)); - Self::push_projectiles(g, &input, (clip_ne, clip_sw)); - Self::push_effects(g, &input, (clip_ne, clip_sw)); - g.ui.draw(&input, &mut g.state)?; - - // These should match the indices in each shader, - // and should each have a corresponding bind group layout. - render_pass.set_bind_group(0, &g.texture_array.bind_group, &[]); - render_pass.set_bind_group(1, &g.state.global_uniform.bind_group, &[]); - - // Starfield pipeline - g.state - .vertex_buffers - .get_starfield() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.starfield_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_starfield_counter(), - ); - - // Sprite pipeline - g.state - .vertex_buffers - .get_object() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.object_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_object_counter(), - ); - - // Ui pipeline - g.state - .vertex_buffers - .get_ui() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.ui_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_ui_counter(), - ); - - // Radial progress bars - // TODO: do we need to do this every time? - g.state - .vertex_buffers - .get_radialbar() - .set_in_pass(&mut render_pass); - render_pass.set_pipeline(&g.radialbar_pipeline); - render_pass.draw_indexed( - 0..SPRITE_INDICES.len() as u32, - 0, - 0..g.state.get_radialbar_counter(), - ); - - let textareas = g.ui.get_textareas(input, &g.state); - g.state - .text_renderer - .prepare( - &g.device, - &g.state.queue, - &mut g.state.text_font_system, - &mut g.state.text_atlas, - Resolution { - width: g.state.window_size.width, - height: g.state.window_size.height, - }, - textareas, - &mut g.state.text_cache, - ) - .unwrap(); - - g.state - .text_renderer - .render(&g.state.text_atlas, &mut render_pass) - .unwrap(); - - // begin_render_pass borrows encoder mutably, - // so we need to drop it before calling finish. - drop(render_pass); - - g.state.queue.submit(iter::once(encoder.finish())); - output.present(); - - return Ok(()); - } -} diff --git a/crates/render/src/ui/api/config.rs b/crates/render/src/ui/api/config.rs new file mode 100644 index 0000000..786c173 --- /dev/null +++ b/crates/render/src/ui/api/config.rs @@ -0,0 +1,28 @@ +use rhai::{CustomType, TypeBuilder}; + +#[derive(Debug, Clone)] +pub struct SceneConfig { + pub show_phys: bool, + pub show_starfield: bool, +} + +impl SceneConfig { + pub fn new() -> Self { + Self { + show_phys: false, + show_starfield: false, + } + } +} + +impl CustomType for SceneConfig { + fn build(mut builder: TypeBuilder) { + builder + .with_name("SceneConfig") + .with_fn("SceneConfig", Self::new) + .with_fn("show_phys", |s: &mut Self, x: bool| s.show_phys = x) + .with_fn("show_starfield", |s: &mut Self, x: bool| { + s.show_starfield = x + }); + } +} diff --git a/crates/render/src/ui/api/event.rs b/crates/render/src/ui/api/event.rs new file mode 100644 index 0000000..f112cec --- /dev/null +++ b/crates/render/src/ui/api/event.rs @@ -0,0 +1,42 @@ +use rhai::{CustomType, TypeBuilder}; + +use super::SpriteElement; + +#[derive(Debug, Clone)] +pub struct MouseClickEvent { + pub down: bool, + pub element: SpriteElement, +} + +impl CustomType for MouseClickEvent { + fn build(mut builder: TypeBuilder) { + builder + .with_name("MouseClickEvent") + .with_fn("is_down", |s: &mut Self| s.down) + .with_fn("element", |s: &mut Self| s.element.clone()); + } +} + +#[derive(Debug, Clone)] +pub struct MouseHoverEvent { + pub enter: bool, + pub element: SpriteElement, +} + +impl CustomType for MouseHoverEvent { + fn build(mut builder: TypeBuilder) { + builder + .with_name("MouseHoverEvent") + .with_fn("is_enter", |s: &mut Self| s.enter) + .with_fn("element", |s: &mut Self| s.element.clone()); + } +} + +#[derive(Debug, Clone)] +pub struct PlayerShipStateEvent {} + +impl CustomType for PlayerShipStateEvent { + fn build(mut builder: TypeBuilder) { + builder.with_name("PlayerShipStateEvent"); + } +} diff --git a/crates/render/src/ui/api/mod.rs b/crates/render/src/ui/api/mod.rs index 6d58048..e6078e8 100644 --- a/crates/render/src/ui/api/mod.rs +++ b/crates/render/src/ui/api/mod.rs @@ -1,4 +1,6 @@ mod color; +mod config; +mod event; mod radialbuilder; mod rect; mod sceneaction; @@ -9,6 +11,8 @@ mod textboxbuilder; mod util; pub use color::*; +pub use config::*; +pub use event::*; pub use radialbuilder::*; pub use rect::*; pub use sceneaction::*; @@ -23,12 +27,17 @@ use rhai::{exported_module, Engine}; pub fn register_into_engine(engine: &mut Engine) { engine .build_type::() + .build_type::() .build_type::() .build_type::() .build_type::() .build_type::() + .build_type::() .build_type::() .build_type::() + .build_type::() + .build_type::() + .build_type::() .register_type_with_name::("SpriteAnchor") .register_static_module("SpriteAnchor", exported_module!(spriteanchor_mod).into()) .register_type_with_name::("TextBoxFont") diff --git a/crates/render/src/ui/api/sceneaction.rs b/crates/render/src/ui/api/sceneaction.rs index 0bd3809..46d4892 100644 --- a/crates/render/src/ui/api/sceneaction.rs +++ b/crates/render/src/ui/api/sceneaction.rs @@ -3,8 +3,7 @@ use rhai::plugin::*; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum SceneAction { None, - SceneOutfitter, - SceneLanded, + GoTo(String), } #[export_module] @@ -12,9 +11,8 @@ pub mod sceneaction_mod { #[allow(non_upper_case_globals)] pub const None: SceneAction = SceneAction::None; - #[allow(non_upper_case_globals)] - pub const SceneOutfitter: SceneAction = SceneAction::SceneOutfitter; - - #[allow(non_upper_case_globals)] - pub const SceneLanded: SceneAction = SceneAction::SceneLanded; + #[allow(non_snake_case)] + pub fn GoTo(scene: String) -> SceneAction { + SceneAction::GoTo(scene) + } } diff --git a/crates/render/src/ui/api/sprite.rs b/crates/render/src/ui/api/sprite.rs index d005390..a06fba2 100644 --- a/crates/render/src/ui/api/sprite.rs +++ b/crates/render/src/ui/api/sprite.rs @@ -11,6 +11,7 @@ pub struct SpriteElement { pub ct: Rc, } +// TODO: remove this unsafe impl Send for SpriteElement {} unsafe impl Sync for SpriteElement {} diff --git a/crates/render/src/ui/api/state.rs b/crates/render/src/ui/api/state.rs index 4d8cf71..75696f6 100644 --- a/crates/render/src/ui/api/state.rs +++ b/crates/render/src/ui/api/state.rs @@ -1,7 +1,72 @@ +use galactica_system::{data, phys::PhysSimShipHandle}; use rhai::{CustomType, TypeBuilder}; +use std::rc::Rc; -#[derive(Debug, Clone, CustomType)] -pub struct State { - pub planet_landscape: String, - pub planet_name: String, +use crate::RenderInput; + +#[derive(Debug, Clone)] +pub struct ShipState { + ship: Option, + input: Rc, +} + +// TODO: remove this +unsafe impl Send for ShipState {} +unsafe impl Sync for ShipState {} + +impl ShipState { + pub fn get_state(&mut self) -> &data::ShipState { + let ship = self + .input + .phys_img + .get_ship(self.ship.as_ref().unwrap()) + .unwrap(); + ship.ship.get_data().get_state() + } +} + +impl CustomType for ShipState { + fn build(mut builder: TypeBuilder) { + builder + .with_name("ShipState") + .with_fn("is_some", |s: &mut Self| s.ship.is_some()) + .with_fn("is_dead", |s: &mut Self| s.get_state().is_dead()) + .with_fn("is_landed", |s: &mut Self| s.get_state().is_landed()) + .with_fn("is_landing", |s: &mut Self| s.get_state().is_landing()) + .with_fn("is_flying", |s: &mut Self| s.get_state().is_flying()) + .with_fn("is_unlanding", |s: &mut Self| s.get_state().is_unlanding()) + .with_fn("is_collapsing", |s: &mut Self| { + s.get_state().is_collapsing() + }); + } +} + +#[derive(Debug, Clone)] +pub struct State { + input: Rc, +} + +// TODO: remove this +unsafe impl Send for State {} +unsafe impl Sync for State {} + +impl State { + pub fn new(input: Rc) -> Self { + Self { input } + } + + pub fn player_ship(&mut self) -> ShipState { + ShipState { + input: self.input.clone(), + ship: self.input.player.ship.map(|x| PhysSimShipHandle(x)), + } + } +} + +impl CustomType for State { + fn build(mut builder: TypeBuilder) { + builder + .with_name("State") + .with_fn("player_ship", Self::player_ship); + } } diff --git a/crates/render/src/ui/event.rs b/crates/render/src/ui/event.rs new file mode 100644 index 0000000..66ca332 --- /dev/null +++ b/crates/render/src/ui/event.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone)] +pub enum Event { + None, + MouseClick, + MouseRelease, + MouseHover, + MouseUnhover, +} diff --git a/crates/render/src/ui/manager.rs b/crates/render/src/ui/manager.rs index bf756fb..6343c4a 100644 --- a/crates/render/src/ui/manager.rs +++ b/crates/render/src/ui/manager.rs @@ -1,15 +1,19 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use galactica_content::Content; +use galactica_system::phys::PhysSimShipHandle; use glyphon::TextArea; use log::{debug, error, trace}; -use rhai::{Array, Dynamic, Engine, Scope, AST}; -use std::{collections::HashSet, rc::Rc}; +use rhai::{Array, Dynamic, Engine, Scope}; +use std::{collections::HashSet, num::NonZeroU32, rc::Rc}; use super::{ - api::{self, SceneAction, SpriteElement, TextBoxBuilder}, - mouseevent::MouseEvent, + api::{ + self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent, SceneAction, SceneConfig, + SpriteElement, TextBoxBuilder, + }, + event::Event, util::{FpsIndicator, RadialBar, TextBox}, - UiElement, UiScene, + UiElement, }; use crate::{ ui::{ @@ -20,12 +24,14 @@ use crate::{ }; pub(crate) struct UiManager { - current_scene: UiScene, + current_scene: Option, + current_scene_config: SceneConfig, engine: Engine, scope: Scope<'static>, elements: Vec, ct: Rc, + last_player_state: u32, show_timings: bool, fps_indicator: FpsIndicator, } @@ -39,43 +45,70 @@ impl UiManager { Self { ct, - current_scene: UiScene::Flying, + current_scene: None, + current_scene_config: SceneConfig::new(), engine, scope, elements: Vec::new(), show_timings: true, fps_indicator: FpsIndicator::new(state), + last_player_state: 0, } } - pub fn get_scene_ast(ct: &Content, scene: UiScene) -> &AST { - match scene { - UiScene::Landed => &ct.get_config().ui_landed_scene, - UiScene::Flying => &ct.get_config().ui_flying_scene, - UiScene::Outfitter => &ct.get_config().ui_outfitter_scene, - } + pub fn get_config(&self) -> &SceneConfig { + &self.current_scene_config } /// Change the current scene - pub fn set_scene(&mut self, state: &mut RenderState, scene: UiScene) -> Result<()> { + pub fn set_scene( + &mut self, + state: &mut RenderState, + input: Rc, + scene: String, + ) -> Result<()> { + if !self.ct.get_config().ui_scenes.contains_key(&scene) { + error!("tried to switch to ui scene `{scene}`, which doesn't exist"); + return Ok(()); + } + debug!("switching to {:?}", scene); - self.current_scene = scene; + self.current_scene = Some(scene); + self.current_scene_config = self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "config", + (), + ) + .with_context(|| format!("while handling `config()`")) + .with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?; self.scope.clear(); self.elements.clear(); let mut used_names = HashSet::new(); - trace!("running init for `{}`", self.current_scene.to_string()); - - let builders: Array = self.engine.call_fn( - &mut self.scope, - Self::get_scene_ast(&self.ct, self.current_scene), - "init", - (State { - planet_landscape: "ui::landscape::test".to_string(), - planet_name: "Earth".to_string(), - },), - )?; + let builders: Array = self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "init", + (State::new(input.clone()),), + ) + .with_context(|| format!("while running `init()`")) + .with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?; trace!("found {:?} builders", builders.len()); @@ -85,7 +118,7 @@ impl UiManager { if used_names.contains(s.name.as_str()) { error!( "UI scene `{}` re-uses element name `{}`", - self.current_scene.to_string(), + self.current_scene.as_ref().unwrap(), s.name ); } else { @@ -103,7 +136,7 @@ impl UiManager { if used_names.contains(r.name.as_str()) { error!( "UI scene `{}` re-uses element name `{}`", - self.current_scene.to_string(), + self.current_scene.as_ref().unwrap(), r.name ); } else { @@ -122,7 +155,7 @@ impl UiManager { if used_names.contains(t.name.as_str()) { error!( "UI scene `{}` re-uses element name `{}`", - self.current_scene.to_string(), + self.current_scene.as_ref().unwrap(), t.name ); } else { @@ -148,49 +181,183 @@ impl UiManager { } /// Draw all ui elements - pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) -> Result<()> { - let mut iter = self.elements.iter(); - - if self.show_timings { - self.fps_indicator.step(input, state); + pub fn draw(&mut self, input: Rc, state: &mut RenderState) -> Result<()> { + if self.current_scene.is_none() { + // Initialize start scene if we haven't yet + self.set_scene( + state, + input.clone(), + self.ct.get_config().start_ui_scene.clone(), + )?; } - let action: SceneAction = loop { - let e = match iter.next() { - Some(e) => e, - None => break SceneAction::None, - }; + if self.show_timings { + self.fps_indicator.step(&input, state); + } + // Check for player ship state changes + let player = input.player.ship; + let send_event = { + if let Some(player) = player { + let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap(); + if self.last_player_state == 0 + || NonZeroU32::new(self.last_player_state).unwrap() + != ship.ship.get_data().get_state().as_int() + { + self.last_player_state = ship.ship.get_data().get_state().as_int().into(); + true + } else { + false + } + } else { + self.last_player_state = 0; + true + } + }; + + let mut actions = Vec::new(); + if send_event { + let action: Dynamic = self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "event", + (State::new(input.clone()), PlayerShipStateEvent {}), + ) + .with_context(|| format!("while handling player state change event")) + .with_context(|| { + format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) + })?; + + if let Some(action) = action.try_cast::() { + match action { + SceneAction::None => {} + _ => { + actions.push(action); + } + } + } + } + + for e in &self.elements { let action: Dynamic = { if let Some(e) = e.sprite() { let mut x = (*e).borrow_mut(); - let m = x.check_mouse(input, state); - x.step(input, state); - x.push_to_buffer(input, state); + let event = x.check_mouse(&input, state); + x.step(&input, state); + x.push_to_buffer(&input, state); drop(x); // we MUST drop here, since script calls mutate the sprite RefCell - match m { - MouseEvent::None => Dynamic::from(SceneAction::None), + match event { + Event::None => Dynamic::from(SceneAction::None), - MouseEvent::Release | MouseEvent::Click => self.engine.call_fn( - &mut self.scope, - Self::get_scene_ast(&self.ct, self.current_scene), - "click", - (SpriteElement::new(self.ct.clone(), e.clone()), m.is_click()), - )?, + Event::MouseClick => self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "event", + ( + State::new(input.clone()), + MouseClickEvent { + down: true, + element: SpriteElement::new(self.ct.clone(), e.clone()), + }, + ), + ) + .with_context(|| format!("while handling click event")) + .with_context(|| { + format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) + })?, - MouseEvent::Leave | MouseEvent::Enter => self.engine.call_fn( - &mut self.scope, - Self::get_scene_ast(&self.ct, self.current_scene), - "hover", - (SpriteElement::new(self.ct.clone(), e.clone()), m.is_enter()), - )?, + Event::MouseRelease => self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "event", + ( + State::new(input.clone()), + MouseClickEvent { + down: false, + element: SpriteElement::new(self.ct.clone(), e.clone()), + }, + ), + ) + .with_context(|| format!("while handling release event")) + .with_context(|| { + format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) + })?, + + Event::MouseHover => self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "event", + ( + State::new(input.clone()), + MouseHoverEvent { + enter: true, + element: SpriteElement::new(self.ct.clone(), e.clone()), + }, + ), + ) + .with_context(|| format!("while handling hover event")) + .with_context(|| { + format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) + })?, + + Event::MouseUnhover => self + .engine + .call_fn( + &mut self.scope, + &self + .ct + .get_config() + .ui_scenes + .get(self.current_scene.as_ref().unwrap()) + .unwrap(), + "event", + ( + State::new(input.clone()), + MouseHoverEvent { + enter: false, + element: SpriteElement::new(self.ct.clone(), e.clone()), + }, + ), + ) + .with_context(|| format!("while handling unhover event")) + .with_context(|| { + format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) + })?, } } else if let Some(e) = e.radialbar() { let mut x = (*e).borrow_mut(); - x.step(input, state); - x.push_to_buffer(input, state); + x.step(&input, state); + x.push_to_buffer(&input, state); Dynamic::from(SceneAction::None) } else { Dynamic::from(SceneAction::None) @@ -201,18 +368,20 @@ impl UiManager { match action { SceneAction::None => {} _ => { - break action; + actions.push(action); } } } - }; + } - drop(iter); - - match action { - SceneAction::None => {} - SceneAction::SceneOutfitter => self.set_scene(state, UiScene::Outfitter)?, - SceneAction::SceneLanded => self.set_scene(state, UiScene::Landed)?, + for a in actions { + match a { + SceneAction::None => {} + SceneAction::GoTo(s) => { + self.set_scene(state, input.clone(), s.clone())?; + break; + } + } } return Ok(()); @@ -227,6 +396,11 @@ impl<'a> UiManager { state: &RenderState, ) -> Vec> { let mut v = Vec::with_capacity(32); + + if self.current_scene.is_none() { + return v; + } + if self.show_timings { v.push(self.fps_indicator.get_textarea(state, input)) } diff --git a/crates/render/src/ui/mod.rs b/crates/render/src/ui/mod.rs index 520a3e5..193d98b 100644 --- a/crates/render/src/ui/mod.rs +++ b/crates/render/src/ui/mod.rs @@ -1,10 +1,8 @@ mod api; +mod event; mod manager; -mod mouseevent; mod uielement; -mod uiscene; mod util; pub(crate) use manager::UiManager; pub(crate) use uielement::UiElement; -pub(crate) use uiscene::UiScene; diff --git a/crates/render/src/ui/mouseevent.rs b/crates/render/src/ui/mouseevent.rs deleted file mode 100644 index 997fdcc..0000000 --- a/crates/render/src/ui/mouseevent.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Debug, Copy, Clone)] -pub enum MouseEvent { - Click, - Release, - Enter, - Leave, - None, -} - -impl MouseEvent { - pub fn is_enter(&self) -> bool { - match self { - Self::Enter => true, - _ => false, - } - } - - pub fn is_click(&self) -> bool { - match self { - Self::Click => true, - _ => false, - } - } -} diff --git a/crates/render/src/ui/uielement.rs b/crates/render/src/ui/uielement.rs index c5cd805..1e054db 100644 --- a/crates/render/src/ui/uielement.rs +++ b/crates/render/src/ui/uielement.rs @@ -2,6 +2,7 @@ use std::{cell::RefCell, rc::Rc}; use super::util::{RadialBar, Sprite, TextBox}; +#[derive(Debug)] pub enum UiElement { Sprite(Rc>), RadialBar(Rc>), diff --git a/crates/render/src/ui/uiscene.rs b/crates/render/src/ui/uiscene.rs deleted file mode 100644 index 19b62d6..0000000 --- a/crates/render/src/ui/uiscene.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Debug, Copy, Clone)] -pub(crate) enum UiScene { - Landed, - Flying, - Outfitter, -} - -impl ToString for UiScene { - fn to_string(&self) -> String { - match self { - Self::Flying => "flying".to_string(), - Self::Landed => "landed".to_string(), - Self::Outfitter => "outfitter".to_string(), - } - } -} diff --git a/crates/render/src/ui/util/sprite.rs b/crates/render/src/ui/util/sprite.rs index 89605ac..b999641 100644 --- a/crates/render/src/ui/util/sprite.rs +++ b/crates/render/src/ui/util/sprite.rs @@ -2,9 +2,7 @@ use galactica_content::{Content, SpriteAutomaton, SpriteHandle}; use galactica_util::to_radians; use super::super::api::Rect; -use crate::{ - ui::mouseevent::MouseEvent, vertexbuffer::types::UiInstance, RenderInput, RenderState, -}; +use crate::{ui::event::Event, vertexbuffer::types::UiInstance, RenderInput, RenderState}; #[derive(Debug, Clone)] pub struct Sprite { @@ -73,7 +71,7 @@ impl Sprite { }); } - pub fn check_mouse(&mut self, input: &RenderInput, state: &mut RenderState) -> MouseEvent { + pub fn check_mouse(&mut self, input: &RenderInput, state: &mut RenderState) -> Event { let r = self.rect.to_centered(state, input.ct.get_config().ui_scale); if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() { @@ -86,18 +84,18 @@ impl Sprite { && input.player.input.pressed_leftclick() { self.has_click = true; - return MouseEvent::Click; + return Event::MouseClick; } if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() { self.has_click = false; - return MouseEvent::Release; + return Event::MouseRelease; } // Release mouse when cursor leaves box if self.has_click && !self.has_mouse { self.has_click = false; - return MouseEvent::Release; + return Event::MouseRelease; } if r.contains_mouse(input, state) && !self.has_mouse { @@ -107,16 +105,16 @@ impl Sprite { self.waiting_for_release = true; } self.has_mouse = true; - return MouseEvent::Enter; + return Event::MouseHover; } if !r.contains_mouse(input, state) && self.has_mouse { self.waiting_for_release = false; self.has_mouse = false; - return MouseEvent::Leave; + return Event::MouseUnhover; } - return MouseEvent::None; + return Event::None; } pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) {