diff --git a/Cargo.toml b/Cargo.toml index ce79fb9..29de197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,5 @@ cgmath = "0.18.0" rand = "0.8.5" walkdir = "2.4.0" toml = "0.8.8" - # Glyphon's crates.io release doesn't support wgpu 0.18 yet glyphon = { git = "https://github.com/grovesNL/glyphon.git", branch = "main" } diff --git a/assets b/assets index 1674e86..9746acb 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1674e86c1edcbd119d94516950d2d274b46a19d4 +Subproject commit 9746acb16c6e2c5f6f232e8d1b53e27fb9ef486e diff --git a/content/config.toml b/content/config.toml new file mode 100644 index 0000000..07c8988 --- /dev/null +++ b/content/config.toml @@ -0,0 +1,38 @@ +[config] +# Per-game config values. +# These should rarely be changed. + +sprite_root = "render" + +fonts.files = [ + "fonts/PTAstraSans-Bold.ttf", + "fonts/PTAstraSans-BoldItalic.ttf", + "fonts/PTAstraSans-Italic.ttf", + "fonts/PTAstraSans-Regular.ttf", + "fonts/PTAstraSerif-Bold.ttf", + "fonts/PTAstraSerif-BoldItalic.ttf", + "fonts/PTAstraSerif-Italic.ttf", + "fonts/PTAstraSerif-Regular.ttf", + "fonts/PTMono-Regular.ttf", +] + +fonts.serif = "PT Astra Serif" +fonts.sans = "PT Astra Sans" +fonts.mono = "PT Mono" + +# Size range for starfield stars, in game units. +# This is scaled for zoom, but NOT for distance. +starfield.min_size = 0.2 +starfield.max_size = 1.8 + +# Z-axis (parallax) range for starfield stars +starfield.min_dist = 75.0 +starfield.max_dist = 200.0 +# Name of starfield sprite +starfield.sprite = "starfield" + + +# Zoom level bounds. +# Zoom is measured as "window height in game units." +zoom_min = 200.0 +zoom_max = 2000.0 diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index 4d63f2b..33a5fe8 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -7,7 +7,7 @@ mod handle; mod part; mod util; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use std::{ collections::HashMap, @@ -28,7 +28,10 @@ mod syntax { use serde::Deserialize; use std::{collections::HashMap, fmt::Display, hash::Hash}; - use crate::part::{effect, faction, outfit, ship, sprite, system}; + use crate::{ + config, + part::{effect, faction, outfit, ship, sprite, system}, + }; #[derive(Debug, Deserialize)] pub struct Root { @@ -38,6 +41,7 @@ mod syntax { pub sprite: Option>, pub faction: Option>, pub effect: Option>, + pub config: Option, } fn merge_hashmap( @@ -74,6 +78,7 @@ mod syntax { sprite: None, faction: None, effect: None, + config: None, } } @@ -89,6 +94,13 @@ mod syntax { .with_context(|| "while merging factions")?; merge_hashmap(&mut self.effect, other.effect) .with_context(|| "while merging effects")?; + if self.config.is_some() { + if other.config.is_some() { + bail!("invalid content dir, multiple config tables") + } + } else { + self.config = other.config; + } return Ok(()); } } @@ -124,12 +136,6 @@ impl ContentBuildContext { /// Represents static game content #[derive(Debug)] pub struct Content { - /* Configuration values */ - /// Root directory for image - image_root: PathBuf, - /// Name of starfield sprite - starfield_sprite_name: String, - /// Sprites pub sprites: Vec, /// Map strings to texture names. @@ -147,6 +153,7 @@ pub struct Content { systems: Vec, factions: Vec, effects: Vec, + config: Config, } // Loading methods @@ -159,12 +166,7 @@ impl Content { } /// Load content from a directory. - pub fn load_dir( - path: PathBuf, - texture_root: PathBuf, - atlas_index: PathBuf, - starfield_texture_name: String, - ) -> Result { + pub fn load_dir(path: PathBuf, asset_root: PathBuf, atlas_index: PathBuf) -> Result { let mut root = syntax::Root::new(); for e in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { @@ -202,6 +204,15 @@ impl Content { let mut build_context = ContentBuildContext::new(); let mut content = Self { + config: { + if let Some(c) = root.config { + c.build(&asset_root) + .with_context(|| "while parsing config table")? + } else { + bail!("failed loading content: no config table specified") + } + }, + sprite_atlas: atlas, systems: Vec::new(), ships: Vec::new(), @@ -212,8 +223,6 @@ impl Content { effects: Vec::new(), sprite_index: HashMap::new(), starfield_handle: None, - image_root: texture_root, - starfield_sprite_name: starfield_texture_name, }; // TODO: enforce sprite and image limits @@ -333,4 +342,9 @@ impl Content { pub fn get_effect(&self, h: EffectHandle) -> &Effect { return &self.effects[h.index]; } + + /// Get content configuration + pub fn get_config(&self) -> &Config { + return &self.config; + } } diff --git a/crates/content/src/part/config.rs b/crates/content/src/part/config.rs new file mode 100644 index 0000000..dc035a8 --- /dev/null +++ b/crates/content/src/part/config.rs @@ -0,0 +1,145 @@ +use std::path::PathBuf; + +pub(crate) mod syntax { + use anyhow::{bail, Result}; + use serde::Deserialize; + use std::path::{Path, PathBuf}; + + // Raw serde syntax structs. + // These are never seen by code outside this crate. + + #[derive(Debug, Deserialize)] + pub struct Config { + pub fonts: Fonts, + pub sprite_root: PathBuf, + pub starfield: Starfield, + pub zoom_min: f32, + pub zoom_max: f32, + } + + impl Config { + // TODO: clean up build trait + pub fn build(self, asset_root: &Path) -> Result { + for i in &self.fonts.files { + if !asset_root.join(i).exists() { + bail!("font file `{}` doesn't exist", i.display()); + } + } + + let starfield_density = 0.01; + let starfield_size = self.starfield.min_dist * self.zoom_max; + let starfield_count = (starfield_size * starfield_density) as i32; + + // 12, because that should be enough to tile any screen. + // Starfield squares are tiled to cover the viewport, adapting to any screen ratio. + // An insufficient limit will result in some tiles not being drawn + let starfield_instance_limit = 12 * starfield_count as u64; + + return Ok(super::Config { + sprite_root: asset_root.join(self.sprite_root), + font_files: self + .fonts + .files + .iter() + .map(|i| asset_root.join(i)) + .collect(), + + font_sans: self.fonts.sans, + font_serif: self.fonts.serif, + font_mono: self.fonts.mono, + //font_cursive: self.fonts.cursive, + //font_fantasy: self.fonts.fantasy, + starfield_max_dist: self.starfield.max_dist, + starfield_min_dist: self.starfield.min_dist, + starfield_max_size: self.starfield.max_size, + starfield_min_size: self.starfield.min_size, + starfield_sprite: self.starfield.sprite, + starfield_count, + starfield_density, + starfield_size, + starfield_instance_limit, + zoom_max: self.zoom_max, + zoom_min: self.zoom_min, + }); + } + } + + #[derive(Debug, Deserialize)] + pub struct Fonts { + pub files: Vec, + pub sans: String, + pub serif: String, + pub mono: String, + //pub cursive: String, + //pub fantasy: String, + } + + #[derive(Debug, Deserialize)] + pub struct Starfield { + pub min_size: f32, + pub max_size: f32, + pub min_dist: f32, + pub max_dist: f32, + pub sprite: String, + } +} + +/// Content configuration +#[derive(Debug, Clone)] +pub struct Config { + /// The directory where all images are stored. + /// Image paths are always interpreted relative to this path. + /// This is a subdirectory of the asset root. + pub sprite_root: PathBuf, + + /// List of font files to load + pub font_files: Vec, + + /// Sans Serif font family name + pub font_sans: String, + + /// Serif font family name + pub font_serif: String, + + /// Monospace font family name + pub font_mono: String, + //pub font_cursive: String, + //pub font_fantasy: String, + /// Min size of starfield sprite, in game units + pub starfield_min_size: f32, + + /// Max size of starfield sprite, in game units + pub starfield_max_size: f32, + + /// Minimum z-distance of starfield star, in game units + pub starfield_min_dist: f32, + + /// Maximum z-distance of starfield star, in game units + pub starfield_max_dist: f32, + + /// Name of starfield sprite + pub starfield_sprite: String, + + /// Size of a square starfield tile, in game units. + /// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units + /// will completely cover a (square) screen. + /// This should be big enough to cover the height of the screen at max zoom. + pub starfield_size: f32, + + /// Average number of stars per game unit + pub starfield_density: f32, + + /// Number of stars in one starfield tile + /// Must be positive + pub starfield_count: i32, + + // TODO: this shouldn't be here, it depends on graphics implementation + /// The maximum number of starfield sprites we can create + pub starfield_instance_limit: u64, + + /// Minimum zoom, in game units + pub zoom_min: f32, + + /// Maximum zoom,in game units + pub zoom_max: f32, +} diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index 5d6d21a..48e50d7 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -1,5 +1,6 @@ //! Content parts +pub(crate) mod config; pub(crate) mod effect; pub(crate) mod faction; pub(crate) mod outfit; @@ -8,6 +9,7 @@ pub(crate) mod ship; pub(crate) mod sprite; pub(crate) mod system; +pub use config::Config; pub use effect::Effect; pub use faction::{Faction, Relationship}; pub use outfit::{Gun, Outfit, Projectile, ProjectileCollider}; diff --git a/crates/content/src/part/sprite.rs b/crates/content/src/part/sprite.rs index 9bb788c..b6f1c73 100644 --- a/crates/content/src/part/sprite.rs +++ b/crates/content/src/part/sprite.rs @@ -120,7 +120,7 @@ impl crate::Build for Sprite { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { - let file = content.image_root.join(&t.file); + let file = content.config.sprite_root.join(&t.file); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", @@ -141,7 +141,7 @@ impl crate::Build for Sprite { aspect: dim.0 as f32 / dim.1 as f32, }; - if sprite_name == content.starfield_sprite_name { + if sprite_name == content.config.starfield_sprite { if content.starfield_handle.is_none() { content.starfield_handle = Some(h) } else { @@ -166,7 +166,7 @@ impl crate::Build for Sprite { syntax::Sprite::Frames(t) => { let mut dim = None; for f in &t.frames { - let file = content.image_root.join(f); + let file = content.config.sprite_root.join(f); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", @@ -200,7 +200,7 @@ impl crate::Build for Sprite { aspect: dim.0 as f32 / dim.1 as f32, }; - if sprite_name == content.starfield_sprite_name { + if sprite_name == content.config.starfield_sprite { unreachable!("Starfield texture may not be animated") } @@ -227,7 +227,7 @@ impl crate::Build for Sprite { if content.starfield_handle.is_none() { bail!( "Could not find a starfield texture (name: `{}`)", - content.starfield_sprite_name + content.config.starfield_sprite ) } diff --git a/crates/galactica/src/game.rs b/crates/galactica/src/game.rs index 1548f30..b914a01 100644 --- a/crates/galactica/src/game.rs +++ b/crates/galactica/src/game.rs @@ -1,9 +1,6 @@ use galactica_content::{Content, FactionHandle, OutfitHandle, ShipHandle, SystemHandle}; use galactica_galaxy::{ship::ShipPersonality, Galaxy, GxShipHandle}; -use galactica_util::{ - constants::{ZOOM_MAX, ZOOM_MIN}, - timing::Timing, -}; +use galactica_util::timing::Timing; use std::time::Instant; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; @@ -21,8 +18,9 @@ pub struct Game { start_instant: Instant, camera: Camera, - galaxy: Galaxy, - content: Content, + ct: Content, + gx: Galaxy, + systemsim: SystemSim, new_particles: Vec, @@ -86,8 +84,8 @@ impl Game { paused: false, time_scale: 1.0, systemsim: physics, - galaxy, - content: ct, + gx: galaxy, + ct, new_particles: Vec::new(), timing: Timing::new(), } @@ -123,7 +121,7 @@ impl Game { self.timing.start_frame(); self.timing.start_galaxy(); - self.galaxy.step(t); + self.gx.step(t); self.timing.mark_galaxy(); self.systemsim.step(StepResources { @@ -134,15 +132,16 @@ impl Game { thrust: self.input.key_thrust, guns: self.input.key_guns, }, - ct: &self.content, - gx: &mut self.galaxy, + ct: &self.ct, + gx: &mut self.gx, particles: &mut self.new_particles, timing: &mut self.timing, t, }); if self.input.v_scroll != 0.0 { - self.camera.zoom = (self.camera.zoom + self.input.v_scroll).clamp(ZOOM_MIN, ZOOM_MAX); + self.camera.zoom = (self.camera.zoom + self.input.v_scroll) + .clamp(self.ct.get_config().zoom_min, self.ct.get_config().zoom_max); self.input.v_scroll = 0.0; } @@ -161,13 +160,17 @@ impl Game { camera_pos: self.camera.pos, camera_zoom: self.camera.zoom, current_time: self.start_instant.elapsed().as_secs_f32(), - content: &self.content, + ct: &self.ct, systemsim: &self.systemsim, // TODO: maybe system should be stored here? particles: &mut self.new_particles, player_data: self.player, - data: &self.galaxy, + gx: &self.gx, current_system: SystemHandle { index: 0 }, timing: &mut self.timing, } } + + pub fn get_content(&self) -> &Content { + &self.ct + } } diff --git a/crates/galactica/src/main.rs b/crates/galactica/src/main.rs index 22ba264..5010e2f 100644 --- a/crates/galactica/src/main.rs +++ b/crates/galactica/src/main.rs @@ -4,7 +4,7 @@ mod inputstatus; use anyhow::{bail, Result}; use galactica_content::Content; -use galactica_util::constants::{ASSET_CACHE, CONTENT_ROOT, IMAGE_ROOT, STARFIELD_SPRITE_NAME}; +use galactica_util::constants::ASSET_CACHE; use std::{ fs, path::{Path, PathBuf}, @@ -27,19 +27,17 @@ fn main() -> Result<()> { // TODO: pretty error if missing let content = Content::load_dir( - PathBuf::from(CONTENT_ROOT), - PathBuf::from(IMAGE_ROOT), + PathBuf::from("./content"), + PathBuf::from("./assets"), atlas_index, - STARFIELD_SPRITE_NAME.to_owned(), )?; 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))?; - gpu.init(); + gpu.init(&content); let mut game = game::Game::new(content); - gpu.update_starfield_buffer(); game.set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, ); @@ -49,7 +47,7 @@ fn main() -> Result<()> { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { match gpu.render(game.get_frame_state()) { Ok(_) => {} - Err(wgpu::SurfaceError::Lost) => gpu.resize(), + Err(wgpu::SurfaceError::Lost) => gpu.resize(game.get_content()), Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, // All other errors (Outdated, Timeout) should be resolved by the next frame Err(e) => eprintln!("{:?}", e), @@ -85,14 +83,14 @@ fn main() -> Result<()> { game.process_scroll(delta, phase); } WindowEvent::Resized(_) => { - gpu.resize(); + gpu.resize(game.get_content()); game.set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, ); } WindowEvent::ScaleFactorChanged { .. } => { - gpu.resize(); + gpu.resize(game.get_content()); game.set_camera_aspect( gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, diff --git a/crates/render/src/datastructs.rs b/crates/render/src/datastructs.rs index ceab9b2..7fd4cae 100644 --- a/crates/render/src/datastructs.rs +++ b/crates/render/src/datastructs.rs @@ -32,10 +32,10 @@ pub struct RenderInput<'a> { pub current_time: f32, /// Game content - pub content: &'a Content, + pub ct: &'a Content, /// Game data - pub data: &'a Galaxy, + pub gx: &'a Galaxy, /// Particles to spawn during this frame pub particles: &'a mut Vec, diff --git a/crates/render/src/gpustate/mod.rs b/crates/render/src/gpustate/mod.rs index c77846e..0d15940 100644 --- a/crates/render/src/gpustate/mod.rs +++ b/crates/render/src/gpustate/mod.rs @@ -1,4 +1,5 @@ use bytemuck; +use galactica_content::Content; use wgpu; use winit; @@ -40,7 +41,7 @@ impl GPUState { /// Update window size. /// This should be called whenever our window is resized. - pub fn resize(&mut self) { + pub fn resize(&mut self, ct: &Content) { let new_size = self.state.window.inner_size(); if new_size.width > 0 && new_size.height > 0 { self.state.window_size = new_size; @@ -49,23 +50,11 @@ impl GPUState { self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); } - self.update_starfield_buffer() - } - - /// Make a StarfieldInstance for each star that needs to be drawn. - /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded. - /// - /// Starfield data rarely changes, so this is called only when it's needed. - pub fn update_starfield_buffer(&mut self) { - self.state.queue.write_buffer( - &self.state.vertex_buffers.starfield.instances, - 0, - bytemuck::cast_slice(&self.starfield.make_instances(self.state.window_aspect)), - ); + self.starfield.update_buffer(ct, &mut self.state); } /// Initialize the rendering engine - pub fn init(&mut self) { + pub fn init(&mut self, ct: &Content) { // Update global values self.state.queue.write_buffer( &self.state.global_uniform.atlas_buffer, @@ -78,6 +67,6 @@ impl GPUState { bytemuck::cast_slice(&[self.texture_array.sprite_data]), ); - self.update_starfield_buffer(); + self.starfield.update_buffer(ct, &mut self.state); } } diff --git a/crates/render/src/gpustate/new.rs b/crates/render/src/gpustate/new.rs index 64adb17..ee20deb 100644 --- a/crates/render/src/gpustate/new.rs +++ b/crates/render/src/gpustate/new.rs @@ -1,8 +1,7 @@ use anyhow::Result; use galactica_content::Content; use galactica_util::constants::{ - OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, STARFIELD_SPRITE_INSTANCE_LIMIT, - UI_SPRITE_INSTANCE_LIMIT, + OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT, }; use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer}; use std::rc::Rc; @@ -110,7 +109,7 @@ impl GPUState { &device, Some(SPRITE_VERTICES), Some(SPRITE_INDICES), - STARFIELD_SPRITE_INSTANCE_LIMIT, + ct.get_config().starfield_instance_limit, )), ui: Rc::new(VertexBuffer::new::( @@ -150,7 +149,32 @@ impl GPUState { // Text renderer let mut text_atlas = TextAtlas::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb); - let text_font_system = FontSystem::new(); + let mut text_font_system = FontSystem::new_with_locale_and_db( + "en-US".to_string(), + glyphon::fontdb::Database::new(), + ); + + let conf = ct.get_config(); + for font in &conf.font_files { + text_font_system.db_mut().load_font_file(font)?; + } + + // TODO: nice error if no family with this name is found + text_font_system + .db_mut() + .set_sans_serif_family(conf.font_sans.clone()); + text_font_system + .db_mut() + .set_serif_family(conf.font_serif.clone()); + text_font_system + .db_mut() + .set_monospace_family(conf.font_mono.clone()); + //text_font_system + // .db_mut() + // .set_cursive_family(conf.font_cursive.clone()); + //text_font_system + // .db_mut() + // .set_fantasy_family(conf.font_fantasy.clone()); let text_cache = SwashCache::new(); let text_renderer = TextRenderer::new( @@ -238,7 +262,7 @@ impl GPUState { .build(); let mut starfield = Starfield::new(); - starfield.regenerate(); + starfield.regenerate(ct); let mut state = RenderState { queue, diff --git a/crates/render/src/gpustate/render.rs b/crates/render/src/gpustate/render.rs index 4fbcfbc..256a239 100644 --- a/crates/render/src/gpustate/render.rs +++ b/crates/render/src/gpustate/render.rs @@ -1,10 +1,7 @@ use anyhow::Result; use bytemuck; use cgmath::Point2; -use galactica_util::constants::{ - PARTICLE_SPRITE_INSTANCE_LIMIT, STARFIELD_SIZE, STARFIELD_SIZE_MAX, STARFIELD_SIZE_MIN, - ZOOM_MAX, ZOOM_MIN, -}; +use galactica_util::constants::PARTICLE_SPRITE_INSTANCE_LIMIT; use glyphon::Resolution; use rand::seq::SliceRandom; use std::iter; @@ -56,7 +53,7 @@ impl super::GPUState { self.state.vertex_buffers.radialbar_counter = 0; // Don't reset particle counter, it's special - let s = input.content.get_starfield_handle(); + let s = input.ct.get_starfield_handle(); // Update global values self.state.queue.write_buffer( @@ -65,7 +62,10 @@ impl super::GPUState { bytemuck::cast_slice(&[GlobalDataContent { camera_position: input.camera_pos.into(), camera_zoom: [input.camera_zoom, 0.0], - camera_zoom_limits: [ZOOM_MIN, ZOOM_MAX], + camera_zoom_limits: [ + input.ct.get_config().zoom_min, + input.ct.get_config().zoom_max, + ], window_size: [ self.state.window_size.width as f32, self.state.window_size.height as f32, @@ -73,8 +73,11 @@ impl super::GPUState { window_scale: [self.state.window.scale_factor() as f32, 0.0], window_aspect: [self.state.window_aspect, 0.0], starfield_sprite: [s.get_index(), 0], - starfield_tile_size: [STARFIELD_SIZE as f32, 0.0], - starfield_size_limits: [STARFIELD_SIZE_MIN, STARFIELD_SIZE_MAX], + starfield_tile_size: [input.ct.get_config().starfield_size, 0.0], + starfield_size_limits: [ + input.ct.get_config().starfield_min_size, + input.ct.get_config().starfield_max_size, + ], current_time: [input.current_time, 0.0], }]), ); diff --git a/crates/render/src/gpustate/systemsim.rs b/crates/render/src/gpustate/systemsim.rs index 97e94d0..a9b9cfa 100644 --- a/crates/render/src/gpustate/systemsim.rs +++ b/crates/render/src/gpustate/systemsim.rs @@ -23,7 +23,7 @@ impl GPUState { let ship_pos = util::rigidbody_position(&r); let ship_rot = util::rigidbody_rotation(r); let ship_ang = -ship_rot.angle(Vector2 { x: 0.0, y: 1.0 }); // TODO: inconsistent angles. Fix! - let ship_cnt = state.content.get_ship(s.data_handle.content_handle()); + let ship_cnt = state.ct.get_ship(s.data_handle.content_handle()); // Position adjusted for parallax // TODO: adjust parallax for zoom? @@ -83,12 +83,12 @@ impl GPUState { // This will be None if this ship is dead. // (physics object stays around to complete the death animation) // If that is the case, we're done, no flares to draw anyway! - let ship = match state.data.get_ship(s.data_handle) { + let ship = match state.gx.get_ship(s.data_handle) { None => continue, Some(s) => s, }; - let flare = ship.get_outfits().get_flare_sprite(state.content); + let flare = ship.get_outfits().get_flare_sprite(state.ct); if s.get_controls().thrust && flare.is_some() { for engine_point in &ship_cnt.engines { self.state.queue.write_buffer( @@ -204,7 +204,7 @@ impl GPUState { // NE and SW corners of screen screen_clip: (Point2, Point2), ) { - let system = state.content.get_system(state.current_system); + let system = state.ct.get_system(state.current_system); for o in &system.objects { // Position adjusted for parallax diff --git a/crates/render/src/starfield.rs b/crates/render/src/starfield.rs index 6ae7c7d..a5178e9 100644 --- a/crates/render/src/starfield.rs +++ b/crates/render/src/starfield.rs @@ -1,11 +1,11 @@ use cgmath::{Point2, Point3, Vector2, Vector3}; -use galactica_util::constants::{ - STARFIELD_COUNT, STARFIELD_SIZE, STARFIELD_SIZE_MAX, STARFIELD_SIZE_MIN, - STARFIELD_SPRITE_INSTANCE_LIMIT, STARFIELD_Z_MAX, STARFIELD_Z_MIN, ZOOM_MAX, -}; +use galactica_content::Content; use rand::{self, Rng}; -use crate::vertexbuffer::types::StarfieldInstance; +use crate::{ + datastructs::RenderState, + vertexbuffer::{types::StarfieldInstance, BufferObject}, +}; pub(crate) struct StarfieldStar { /// Star coordinates, in world space. @@ -30,22 +30,26 @@ impl Starfield { pub fn new() -> Self { Self { stars: Vec::new(), - instance_count: 0u32, + instance_count: 0, } } - pub fn regenerate(&mut self) { + pub fn regenerate(&mut self, ct: &Content) { // TODO: save seed in system, regenerate on jump let mut rng = rand::thread_rng(); - let sz = STARFIELD_SIZE as f32 / 2.0; - self.stars = (0..STARFIELD_COUNT) + let sz = ct.get_config().starfield_size as f32 / 2.0; + self.stars = (0..ct.get_config().starfield_count) .map(|_| StarfieldStar { pos: Point3 { x: rng.gen_range(-sz..=sz), y: rng.gen_range(-sz..=sz), - z: rng.gen_range(STARFIELD_Z_MIN..STARFIELD_Z_MAX), + z: rng.gen_range( + ct.get_config().starfield_min_dist..=ct.get_config().starfield_max_dist, + ), }, - size: rng.gen_range(STARFIELD_SIZE_MIN..STARFIELD_SIZE_MAX), + size: rng.gen_range( + ct.get_config().starfield_min_size..ct.get_config().starfield_max_size, + ), tint: Vector2 { x: rng.gen_range(0.0..=1.0), y: rng.gen_range(0.0..=1.0), @@ -54,18 +58,18 @@ impl Starfield { .collect(); } - pub fn make_instances(&mut self, aspect: f32) -> Vec { - let sz = STARFIELD_SIZE as f32; + pub fn update_buffer(&mut self, ct: &Content, state: &mut RenderState) { + let sz = ct.get_config().starfield_size as f32; // Compute window size in starfield tiles let mut nw_tile: Point2 = { // Game coordinates (relative to camera) of nw corner of screen. - let clip_nw = Point2::from((aspect, 1.0)) * ZOOM_MAX; + let clip_nw = Point2::from((state.window_aspect, 1.0)) * ct.get_config().zoom_max; // Parallax correction. // Also, adjust v for mod to work properly // (v is centered at 0) - let v: Point2 = clip_nw * STARFIELD_Z_MIN; + let v: Point2 = clip_nw * ct.get_config().starfield_min_dist; let v_adj: Point2 = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); #[rustfmt::skip] @@ -89,14 +93,14 @@ impl Starfield { // Truncate tile grid to buffer size // (The window won't be full of stars if our instance limit is too small) - while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * STARFIELD_COUNT as i32) - > STARFIELD_SPRITE_INSTANCE_LIMIT as i32 + while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * ct.get_config().starfield_count as i32) + > ct.get_config().starfield_instance_limit as i32 { nw_tile -= Vector2::from((1, 1)); } // Add all tiles to buffer - let mut instances = Vec::new(); + self.instance_count = 0; // Keep track of buffer index for x in (-nw_tile.x)..=nw_tile.x { for y in (-nw_tile.y)..=nw_tile.y { let offset = Vector3 { @@ -105,21 +109,21 @@ impl Starfield { z: 0.0, }; for s in &self.stars { - instances.push(StarfieldInstance { - position: (s.pos + offset).into(), - size: s.size, - tint: s.tint.into(), - }) + state.queue.write_buffer( + &state.vertex_buffers.starfield.instances, + StarfieldInstance::SIZE * self.instance_count as u64, + bytemuck::cast_slice(&[StarfieldInstance { + position: (s.pos + offset).into(), + size: s.size, + tint: s.tint.into(), + }]), + ); + self.instance_count += 1; + + // instance_count is guaranteed to stay within buffer limits, + // this is guaranteed by previous checks. } } } - - // Enforce starfield limit - if instances.len() as u64 > STARFIELD_SPRITE_INSTANCE_LIMIT { - unreachable!("Starfield limit exceeded!") - } - self.instance_count = instances.len() as u32; - - return instances; } } diff --git a/crates/render/src/ui/fpsindicator.rs b/crates/render/src/ui/fpsindicator.rs index 5b7aeb3..69f9f8a 100644 --- a/crates/render/src/ui/fpsindicator.rs +++ b/crates/render/src/ui/fpsindicator.rs @@ -9,7 +9,7 @@ pub(super) struct FpsIndicator { impl FpsIndicator { pub fn new(state: &mut RenderState) -> Self { - let mut buffer = Buffer::new(&mut state.text_font_system, Metrics::new(12.0, 20.0)); + let mut buffer = Buffer::new(&mut state.text_font_system, Metrics::new(15.0, 20.0)); buffer.set_size( &mut state.text_font_system, state.window_size.width as f32, @@ -36,7 +36,7 @@ impl FpsIndicator { self.buffer.set_text( &mut state.text_font_system, &format!( - "Frame: {:04.02?}%\nGame: {:04.02?}%\nShips: {:04.02?}%\nPhys: {:04.02?}%\nRender: {:.02?}", + "Frame: {:05.02?}%\nGame: {:05.02?}%\nShips: {:05.02?}%\nPhys: {:05.02?}%\nRender: {:.02?}", 100.0 * (input.timing.frame / input.timing.render), 100.0 * (input.timing.galaxy / input.timing.frame), 100.0 * (input.timing.physics_sim / input.timing.frame), diff --git a/crates/render/src/ui/radar.rs b/crates/render/src/ui/radar.rs index 69bcfdd..85e4a69 100644 --- a/crates/render/src/ui/radar.rs +++ b/crates/render/src/ui/radar.rs @@ -32,9 +32,9 @@ impl Radar { .unwrap(); let player_position = util::rigidbody_position(player_body); - let planet_sprite = input.content.get_sprite_handle("ui::planetblip"); - let ship_sprite = input.content.get_sprite_handle("ui::shipblip"); - let arrow_sprite = input.content.get_sprite_handle("ui::centerarrow"); + let planet_sprite = input.ct.get_sprite_handle("ui::planetblip"); + let ship_sprite = input.ct.get_sprite_handle("ui::shipblip"); + let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow"); // Enforce buffer limit if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT { @@ -52,13 +52,13 @@ impl Radar { angle: 0.0, size: radar_size, color: [1.0, 1.0, 1.0, 1.0], - sprite_index: input.content.get_sprite_handle("ui::radar").get_index(), + sprite_index: input.ct.get_sprite_handle("ui::radar").get_index(), }]), ); state.vertex_buffers.ui_counter += 1; // Draw system objects - let system = input.content.get_system(input.current_system); + let system = input.ct.get_system(input.current_system); for o in &system.objects { let size = (o.size / o.pos.z) / (radar_range * system_object_scale); let p = Point2 { @@ -107,17 +107,17 @@ impl Radar { for (s, r) in input.systemsim.iter_ship_body() { // This will be None if this ship is dead. // Stays around while the physics system runs a collapse sequence - let color = match input.data.get_ship(s.data_handle) { + let color = match input.gx.get_ship(s.data_handle) { None => { // TODO: configurable [0.2, 0.2, 0.2, 1.0] } Some(data) => { - let c = input.content.get_faction(data.get_faction()).color; + let c = input.ct.get_faction(data.get_faction()).color; [c[0], c[1], c[2], 1.0] } }; - let ship = input.content.get_ship(s.data_handle.content_handle()); + let ship = input.ct.get_ship(s.data_handle.content_handle()); let size = (ship.size * ship.sprite.aspect) * ship_scale; let p = util::rigidbody_position(r); let d = (p - player_position) / radar_range; @@ -167,7 +167,7 @@ impl Radar { let d = d * (radar_size / 2.0); let color = [0.3, 0.3, 0.3, 1.0]; if m < 0.8 { - let sprite = input.content.get_sprite_handle("ui::radarframe"); + let sprite = input.ct.get_sprite_handle("ui::radarframe"); let size = 7.0f32.min((0.8 - m) * 70.0); // Enforce buffer limit (this section adds four items) diff --git a/crates/render/src/ui/status.rs b/crates/render/src/ui/status.rs index fb41794..694404b 100644 --- a/crates/render/src/ui/status.rs +++ b/crates/render/src/ui/status.rs @@ -27,14 +27,11 @@ impl Status { let player_world_object = input.systemsim.get_ship(input.player_data).unwrap(); - let data = input - .data - .get_ship(player_world_object.data_handle) - .unwrap(); + let data = input.gx.get_ship(player_world_object.data_handle).unwrap(); let max_shields = data.get_outfits().get_shield_strength(); let current_shields = data.get_shields(); let current_hull = data.get_hull(); - let max_hull = input.content.get_ship(data.get_content()).hull; + let max_hull = input.ct.get_ship(data.get_content()).hull; state.queue.write_buffer( &state.vertex_buffers.ui.instances, @@ -45,7 +42,7 @@ impl Status { angle: 0.0, size: 200.0, color: [1.0, 1.0, 1.0, 1.0], - sprite_index: input.content.get_sprite_handle("ui::status").get_index(), + sprite_index: input.ct.get_sprite_handle("ui::status").get_index(), }]), ); state.vertex_buffers.ui_counter += 1; diff --git a/crates/util/src/constants.rs b/crates/util/src/constants.rs index 73fc453..ad07e6c 100644 --- a/crates/util/src/constants.rs +++ b/crates/util/src/constants.rs @@ -2,48 +2,6 @@ // TODO: many of these should be moved to a config file or cli option -/// Minimum zoom level -pub const ZOOM_MIN: f32 = 200.0; -/// Maximum zoom level -pub const ZOOM_MAX: f32 = 2000.0; - -/// Z-axis range for starfield stars -/// This does not affect scale. -pub const STARFIELD_Z_MIN: f32 = 75.0; -/// Z-axis range for starfield stars -/// This does not affect scale. -pub const STARFIELD_Z_MAX: f32 = 200.0; - -/// Size range for starfield stars, in game units. -/// This is scaled for zoom, but NOT for distance. -pub const STARFIELD_SIZE_MIN: f32 = 0.2; -/// Size range for starfield stars, in game units. -/// This is scaled for zoom, but NOT for distance. -pub const STARFIELD_SIZE_MAX: f32 = 1.8; - -/// Size of a square starfield tile, in game units. -/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units -/// will completely cover a (square) screen. This depends on zoom! -/// -/// Use a value smaller than zoom_max for debug. -pub const STARFIELD_SIZE: u64 = STARFIELD_Z_MAX as u64 * ZOOM_MAX as u64; - -/// Average number of stars per game unit -pub const STARFIELD_DENSITY: f64 = 0.01; - -/// Number of stars in one starfield tile -/// Must fit inside an i32 -pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64; - -/// Name of starfield sprite -pub const STARFIELD_SPRITE_NAME: &'static str = "starfield"; - -/// Root directory of game content -pub const CONTENT_ROOT: &'static str = "./content"; - -/// Root directory of game images -pub const IMAGE_ROOT: &'static str = "./assets/render"; - /// We can draw at most this many object sprites on the screen. pub const OBJECT_SPRITE_INSTANCE_LIMIT: u64 = 500; @@ -57,9 +15,6 @@ pub const RADIALBAR_SPRITE_INSTANCE_LIMIT: u64 = 10; /// The size of our circular particle buffer. When we create particles, the oldest ones are replaced. pub const PARTICLE_SPRITE_INSTANCE_LIMIT: u64 = 1000; -/// Must be small enough to fit in an i32 -pub const STARFIELD_SPRITE_INSTANCE_LIMIT: u64 = STARFIELD_COUNT * 24; - /// The maximum number of sprites we can define pub const SPRITE_LIMIT: u32 = 1024;