Compare commits

...

2 Commits

Author SHA1 Message Date
Mark 876a95e546
Updated TODO 2024-01-10 22:44:30 -08:00
Mark 7e12a0e26d
Added fonts & config 2024-01-10 22:44:22 -08:00
20 changed files with 347 additions and 178 deletions

View File

@ -66,6 +66,5 @@ cgmath = "0.18.0"
rand = "0.8.5" rand = "0.8.5"
walkdir = "2.4.0" walkdir = "2.4.0"
toml = "0.8.8" toml = "0.8.8"
# Glyphon's crates.io release doesn't support wgpu 0.18 yet # Glyphon's crates.io release doesn't support wgpu 0.18 yet
glyphon = { git = "https://github.com/grovesNL/glyphon.git", branch = "main" } glyphon = { git = "https://github.com/grovesNL/glyphon.git", branch = "main" }

View File

@ -1,8 +1,8 @@
## Specific Jobs ## Specific Jobs
- UI: text arranger
- Start documenting - Start documenting
- Check for handle leaks - Check for handle leaks
- Don't allocate each frame - Don't allocate each frame
- UI: text arranger
- Sound system - Sound system
- Ship death debris - Ship death debris
- Sprite reels - Sprite reels
@ -19,7 +19,6 @@
- GPU limits? (texture size, texture number, particle/sprite limits) - GPU limits? (texture size, texture number, particle/sprite limits)
- Particles when a ship is damaged (events) - Particles when a ship is damaged (events)
- Sticky radar - Sticky radar
- Fix gun points
- Arbitrary size resource names - Arbitrary size resource names
@ -87,7 +86,7 @@
## Internal ## Internal
- Logging/warning system - Logging/warning system
- Frame timings (compute/render/physics/etc) - Only compute timings when necessary
- Elegantly handle lost focus - Elegantly handle lost focus
- Pause game - Pause game
- Clear all `// TODO:` comments littered in the source - Clear all `// TODO:` comments littered in the source
@ -117,7 +116,6 @@
- Non-removable outfits - Non-removable outfits
- Space-converting outfits - Space-converting outfits
- Damage struct and debuffs - Damage struct and debuffs
-
## Camera ## Camera
- Shake/wobble on heavy hits? - Shake/wobble on heavy hits?

2
assets

@ -1 +1 @@
Subproject commit 1674e86c1edcbd119d94516950d2d274b46a19d4 Subproject commit 9746acb16c6e2c5f6f232e8d1b53e27fb9ef486e

38
content/config.toml Normal file
View File

@ -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

View File

@ -7,7 +7,7 @@ mod handle;
mod part; mod part;
mod util; mod util;
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -28,7 +28,10 @@ mod syntax {
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, fmt::Display, hash::Hash}; 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)] #[derive(Debug, Deserialize)]
pub struct Root { pub struct Root {
@ -38,6 +41,7 @@ mod syntax {
pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>, pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>,
pub faction: Option<HashMap<String, faction::syntax::Faction>>, pub faction: Option<HashMap<String, faction::syntax::Faction>>,
pub effect: Option<HashMap<String, effect::syntax::Effect>>, pub effect: Option<HashMap<String, effect::syntax::Effect>>,
pub config: Option<config::syntax::Config>,
} }
fn merge_hashmap<K, V>( fn merge_hashmap<K, V>(
@ -74,6 +78,7 @@ mod syntax {
sprite: None, sprite: None,
faction: None, faction: None,
effect: None, effect: None,
config: None,
} }
} }
@ -89,6 +94,13 @@ mod syntax {
.with_context(|| "while merging factions")?; .with_context(|| "while merging factions")?;
merge_hashmap(&mut self.effect, other.effect) merge_hashmap(&mut self.effect, other.effect)
.with_context(|| "while merging effects")?; .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(()); return Ok(());
} }
} }
@ -124,12 +136,6 @@ impl ContentBuildContext {
/// Represents static game content /// Represents static game content
#[derive(Debug)] #[derive(Debug)]
pub struct Content { pub struct Content {
/* Configuration values */
/// Root directory for image
image_root: PathBuf,
/// Name of starfield sprite
starfield_sprite_name: String,
/// Sprites /// Sprites
pub sprites: Vec<Sprite>, pub sprites: Vec<Sprite>,
/// Map strings to texture names. /// Map strings to texture names.
@ -147,6 +153,7 @@ pub struct Content {
systems: Vec<System>, systems: Vec<System>,
factions: Vec<Faction>, factions: Vec<Faction>,
effects: Vec<Effect>, effects: Vec<Effect>,
config: Config,
} }
// Loading methods // Loading methods
@ -159,12 +166,7 @@ impl Content {
} }
/// Load content from a directory. /// Load content from a directory.
pub fn load_dir( pub fn load_dir(path: PathBuf, asset_root: PathBuf, atlas_index: PathBuf) -> Result<Self> {
path: PathBuf,
texture_root: PathBuf,
atlas_index: PathBuf,
starfield_texture_name: String,
) -> Result<Self> {
let mut root = syntax::Root::new(); let mut root = syntax::Root::new();
for e in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { 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 build_context = ContentBuildContext::new();
let mut content = Self { 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, sprite_atlas: atlas,
systems: Vec::new(), systems: Vec::new(),
ships: Vec::new(), ships: Vec::new(),
@ -212,8 +223,6 @@ impl Content {
effects: Vec::new(), effects: Vec::new(),
sprite_index: HashMap::new(), sprite_index: HashMap::new(),
starfield_handle: None, starfield_handle: None,
image_root: texture_root,
starfield_sprite_name: starfield_texture_name,
}; };
// TODO: enforce sprite and image limits // TODO: enforce sprite and image limits
@ -333,4 +342,9 @@ impl Content {
pub fn get_effect(&self, h: EffectHandle) -> &Effect { pub fn get_effect(&self, h: EffectHandle) -> &Effect {
return &self.effects[h.index]; return &self.effects[h.index];
} }
/// Get content configuration
pub fn get_config(&self) -> &Config {
return &self.config;
}
} }

View File

@ -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<super::Config> {
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<PathBuf>,
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<PathBuf>,
/// 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,
}

View File

@ -1,5 +1,6 @@
//! Content parts //! Content parts
pub(crate) mod config;
pub(crate) mod effect; pub(crate) mod effect;
pub(crate) mod faction; pub(crate) mod faction;
pub(crate) mod outfit; pub(crate) mod outfit;
@ -8,6 +9,7 @@ pub(crate) mod ship;
pub(crate) mod sprite; pub(crate) mod sprite;
pub(crate) mod system; pub(crate) mod system;
pub use config::Config;
pub use effect::Effect; pub use effect::Effect;
pub use faction::{Faction, Relationship}; pub use faction::{Faction, Relationship};
pub use outfit::{Gun, Outfit, Projectile, ProjectileCollider}; pub use outfit::{Gun, Outfit, Projectile, ProjectileCollider};

View File

@ -120,7 +120,7 @@ impl crate::Build for Sprite {
for (sprite_name, t) in sprites { for (sprite_name, t) in sprites {
match t { match t {
syntax::Sprite::Static(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(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read file `{}` in sprite `{}`", "Failed to read file `{}` in sprite `{}`",
@ -141,7 +141,7 @@ impl crate::Build for Sprite {
aspect: dim.0 as f32 / dim.1 as f32, 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() { if content.starfield_handle.is_none() {
content.starfield_handle = Some(h) content.starfield_handle = Some(h)
} else { } else {
@ -166,7 +166,7 @@ impl crate::Build for Sprite {
syntax::Sprite::Frames(t) => { syntax::Sprite::Frames(t) => {
let mut dim = None; let mut dim = None;
for f in &t.frames { 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(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read file `{}` in sprite `{}`", "Failed to read file `{}` in sprite `{}`",
@ -200,7 +200,7 @@ impl crate::Build for Sprite {
aspect: dim.0 as f32 / dim.1 as f32, 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") unreachable!("Starfield texture may not be animated")
} }
@ -227,7 +227,7 @@ impl crate::Build for Sprite {
if content.starfield_handle.is_none() { if content.starfield_handle.is_none() {
bail!( bail!(
"Could not find a starfield texture (name: `{}`)", "Could not find a starfield texture (name: `{}`)",
content.starfield_sprite_name content.config.starfield_sprite
) )
} }

View File

@ -1,9 +1,6 @@
use galactica_content::{Content, FactionHandle, OutfitHandle, ShipHandle, SystemHandle}; use galactica_content::{Content, FactionHandle, OutfitHandle, ShipHandle, SystemHandle};
use galactica_galaxy::{ship::ShipPersonality, Galaxy, GxShipHandle}; use galactica_galaxy::{ship::ShipPersonality, Galaxy, GxShipHandle};
use galactica_util::{ use galactica_util::timing::Timing;
constants::{ZOOM_MAX, ZOOM_MIN},
timing::Timing,
};
use std::time::Instant; use std::time::Instant;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
@ -21,8 +18,9 @@ pub struct Game {
start_instant: Instant, start_instant: Instant,
camera: Camera, camera: Camera,
galaxy: Galaxy, ct: Content,
content: Content, gx: Galaxy,
systemsim: SystemSim, systemsim: SystemSim,
new_particles: Vec<ParticleBuilder>, new_particles: Vec<ParticleBuilder>,
@ -86,8 +84,8 @@ impl Game {
paused: false, paused: false,
time_scale: 1.0, time_scale: 1.0,
systemsim: physics, systemsim: physics,
galaxy, gx: galaxy,
content: ct, ct,
new_particles: Vec::new(), new_particles: Vec::new(),
timing: Timing::new(), timing: Timing::new(),
} }
@ -123,7 +121,7 @@ impl Game {
self.timing.start_frame(); self.timing.start_frame();
self.timing.start_galaxy(); self.timing.start_galaxy();
self.galaxy.step(t); self.gx.step(t);
self.timing.mark_galaxy(); self.timing.mark_galaxy();
self.systemsim.step(StepResources { self.systemsim.step(StepResources {
@ -134,15 +132,16 @@ impl Game {
thrust: self.input.key_thrust, thrust: self.input.key_thrust,
guns: self.input.key_guns, guns: self.input.key_guns,
}, },
ct: &self.content, ct: &self.ct,
gx: &mut self.galaxy, gx: &mut self.gx,
particles: &mut self.new_particles, particles: &mut self.new_particles,
timing: &mut self.timing, timing: &mut self.timing,
t, t,
}); });
if self.input.v_scroll != 0.0 { 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; self.input.v_scroll = 0.0;
} }
@ -161,13 +160,17 @@ impl Game {
camera_pos: self.camera.pos, camera_pos: self.camera.pos,
camera_zoom: self.camera.zoom, camera_zoom: self.camera.zoom,
current_time: self.start_instant.elapsed().as_secs_f32(), 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? systemsim: &self.systemsim, // TODO: maybe system should be stored here?
particles: &mut self.new_particles, particles: &mut self.new_particles,
player_data: self.player, player_data: self.player,
data: &self.galaxy, gx: &self.gx,
current_system: SystemHandle { index: 0 }, current_system: SystemHandle { index: 0 },
timing: &mut self.timing, timing: &mut self.timing,
} }
} }
pub fn get_content(&self) -> &Content {
&self.ct
}
} }

View File

@ -4,7 +4,7 @@ mod inputstatus;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use galactica_content::Content; 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::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -27,19 +27,17 @@ fn main() -> Result<()> {
// TODO: pretty error if missing // TODO: pretty error if missing
let content = Content::load_dir( let content = Content::load_dir(
PathBuf::from(CONTENT_ROOT), PathBuf::from("./content"),
PathBuf::from(IMAGE_ROOT), PathBuf::from("./assets"),
atlas_index, atlas_index,
STARFIELD_SPRITE_NAME.to_owned(),
)?; )?;
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?; let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?;
gpu.init(); gpu.init(&content);
let mut game = game::Game::new(content); let mut game = game::Game::new(content);
gpu.update_starfield_buffer();
game.set_camera_aspect( game.set_camera_aspect(
gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, 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() => { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => {
match gpu.render(game.get_frame_state()) { match gpu.render(game.get_frame_state()) {
Ok(_) => {} Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(), Err(wgpu::SurfaceError::Lost) => gpu.resize(game.get_content()),
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame // All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e), Err(e) => eprintln!("{:?}", e),
@ -85,14 +83,14 @@ fn main() -> Result<()> {
game.process_scroll(delta, phase); game.process_scroll(delta, phase);
} }
WindowEvent::Resized(_) => { WindowEvent::Resized(_) => {
gpu.resize(); gpu.resize(game.get_content());
game.set_camera_aspect( game.set_camera_aspect(
gpu.window().inner_size().width as f32 gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32, / gpu.window().inner_size().height as f32,
); );
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
gpu.resize(); gpu.resize(game.get_content());
game.set_camera_aspect( game.set_camera_aspect(
gpu.window().inner_size().width as f32 gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32, / gpu.window().inner_size().height as f32,

View File

@ -32,10 +32,10 @@ pub struct RenderInput<'a> {
pub current_time: f32, pub current_time: f32,
/// Game content /// Game content
pub content: &'a Content, pub ct: &'a Content,
/// Game data /// Game data
pub data: &'a Galaxy, pub gx: &'a Galaxy,
/// Particles to spawn during this frame /// Particles to spawn during this frame
pub particles: &'a mut Vec<ParticleBuilder>, pub particles: &'a mut Vec<ParticleBuilder>,

View File

@ -1,4 +1,5 @@
use bytemuck; use bytemuck;
use galactica_content::Content;
use wgpu; use wgpu;
use winit; use winit;
@ -40,7 +41,7 @@ impl GPUState {
/// Update window size. /// Update window size.
/// This should be called whenever our window is resized. /// 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(); let new_size = self.state.window.inner_size();
if new_size.width > 0 && new_size.height > 0 { if new_size.width > 0 && new_size.height > 0 {
self.state.window_size = new_size; self.state.window_size = new_size;
@ -49,23 +50,11 @@ impl GPUState {
self.config.height = new_size.height; self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
} }
self.update_starfield_buffer() self.starfield.update_buffer(ct, &mut self.state);
}
/// 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)),
);
} }
/// Initialize the rendering engine /// Initialize the rendering engine
pub fn init(&mut self) { pub fn init(&mut self, ct: &Content) {
// Update global values // Update global values
self.state.queue.write_buffer( self.state.queue.write_buffer(
&self.state.global_uniform.atlas_buffer, &self.state.global_uniform.atlas_buffer,
@ -78,6 +67,6 @@ impl GPUState {
bytemuck::cast_slice(&[self.texture_array.sprite_data]), bytemuck::cast_slice(&[self.texture_array.sprite_data]),
); );
self.update_starfield_buffer(); self.starfield.update_buffer(ct, &mut self.state);
} }
} }

View File

@ -1,8 +1,7 @@
use anyhow::Result; use anyhow::Result;
use galactica_content::Content; use galactica_content::Content;
use galactica_util::constants::{ use galactica_util::constants::{
OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, STARFIELD_SPRITE_INSTANCE_LIMIT, OBJECT_SPRITE_INSTANCE_LIMIT, PARTICLE_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT,
UI_SPRITE_INSTANCE_LIMIT,
}; };
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer}; use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc; use std::rc::Rc;
@ -110,7 +109,7 @@ impl GPUState {
&device, &device,
Some(SPRITE_VERTICES), Some(SPRITE_VERTICES),
Some(SPRITE_INDICES), Some(SPRITE_INDICES),
STARFIELD_SPRITE_INSTANCE_LIMIT, ct.get_config().starfield_instance_limit,
)), )),
ui: Rc::new(VertexBuffer::new::<TexturedVertex, UiInstance>( ui: Rc::new(VertexBuffer::new::<TexturedVertex, UiInstance>(
@ -150,7 +149,32 @@ impl GPUState {
// Text renderer // Text renderer
let mut text_atlas = TextAtlas::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb); 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_cache = SwashCache::new();
let text_renderer = TextRenderer::new( let text_renderer = TextRenderer::new(
@ -238,7 +262,7 @@ impl GPUState {
.build(); .build();
let mut starfield = Starfield::new(); let mut starfield = Starfield::new();
starfield.regenerate(); starfield.regenerate(ct);
let mut state = RenderState { let mut state = RenderState {
queue, queue,

View File

@ -1,10 +1,7 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::Point2; use cgmath::Point2;
use galactica_util::constants::{ use galactica_util::constants::PARTICLE_SPRITE_INSTANCE_LIMIT;
PARTICLE_SPRITE_INSTANCE_LIMIT, STARFIELD_SIZE, STARFIELD_SIZE_MAX, STARFIELD_SIZE_MIN,
ZOOM_MAX, ZOOM_MIN,
};
use glyphon::Resolution; use glyphon::Resolution;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use std::iter; use std::iter;
@ -56,7 +53,7 @@ impl super::GPUState {
self.state.vertex_buffers.radialbar_counter = 0; self.state.vertex_buffers.radialbar_counter = 0;
// Don't reset particle counter, it's special // 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 // Update global values
self.state.queue.write_buffer( self.state.queue.write_buffer(
@ -65,7 +62,10 @@ impl super::GPUState {
bytemuck::cast_slice(&[GlobalDataContent { bytemuck::cast_slice(&[GlobalDataContent {
camera_position: input.camera_pos.into(), camera_position: input.camera_pos.into(),
camera_zoom: [input.camera_zoom, 0.0], 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: [ window_size: [
self.state.window_size.width as f32, self.state.window_size.width as f32,
self.state.window_size.height 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_scale: [self.state.window.scale_factor() as f32, 0.0],
window_aspect: [self.state.window_aspect, 0.0], window_aspect: [self.state.window_aspect, 0.0],
starfield_sprite: [s.get_index(), 0], starfield_sprite: [s.get_index(), 0],
starfield_tile_size: [STARFIELD_SIZE as f32, 0.0], starfield_tile_size: [input.ct.get_config().starfield_size, 0.0],
starfield_size_limits: [STARFIELD_SIZE_MIN, STARFIELD_SIZE_MAX], starfield_size_limits: [
input.ct.get_config().starfield_min_size,
input.ct.get_config().starfield_max_size,
],
current_time: [input.current_time, 0.0], current_time: [input.current_time, 0.0],
}]), }]),
); );

View File

@ -23,7 +23,7 @@ impl GPUState {
let ship_pos = util::rigidbody_position(&r); let ship_pos = util::rigidbody_position(&r);
let ship_rot = util::rigidbody_rotation(r); let ship_rot = util::rigidbody_rotation(r);
let ship_ang = -ship_rot.angle(Vector2 { x: 0.0, y: 1.0 }); // TODO: inconsistent angles. Fix! 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 // Position adjusted for parallax
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
@ -83,12 +83,12 @@ impl GPUState {
// This will be None if this ship is dead. // This will be None if this ship is dead.
// (physics object stays around to complete the death animation) // (physics object stays around to complete the death animation)
// If that is the case, we're done, no flares to draw anyway! // 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, None => continue,
Some(s) => s, 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() { if s.get_controls().thrust && flare.is_some() {
for engine_point in &ship_cnt.engines { for engine_point in &ship_cnt.engines {
self.state.queue.write_buffer( self.state.queue.write_buffer(
@ -204,7 +204,7 @@ impl GPUState {
// NE and SW corners of screen // NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>), screen_clip: (Point2<f32>, Point2<f32>),
) { ) {
let system = state.content.get_system(state.current_system); let system = state.ct.get_system(state.current_system);
for o in &system.objects { for o in &system.objects {
// Position adjusted for parallax // Position adjusted for parallax

View File

@ -1,11 +1,11 @@
use cgmath::{Point2, Point3, Vector2, Vector3}; use cgmath::{Point2, Point3, Vector2, Vector3};
use galactica_util::constants::{ use galactica_content::Content;
STARFIELD_COUNT, STARFIELD_SIZE, STARFIELD_SIZE_MAX, STARFIELD_SIZE_MIN,
STARFIELD_SPRITE_INSTANCE_LIMIT, STARFIELD_Z_MAX, STARFIELD_Z_MIN, ZOOM_MAX,
};
use rand::{self, Rng}; use rand::{self, Rng};
use crate::vertexbuffer::types::StarfieldInstance; use crate::{
datastructs::RenderState,
vertexbuffer::{types::StarfieldInstance, BufferObject},
};
pub(crate) struct StarfieldStar { pub(crate) struct StarfieldStar {
/// Star coordinates, in world space. /// Star coordinates, in world space.
@ -30,22 +30,26 @@ impl Starfield {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
stars: Vec::new(), 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 // TODO: save seed in system, regenerate on jump
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let sz = STARFIELD_SIZE as f32 / 2.0; let sz = ct.get_config().starfield_size as f32 / 2.0;
self.stars = (0..STARFIELD_COUNT) self.stars = (0..ct.get_config().starfield_count)
.map(|_| StarfieldStar { .map(|_| StarfieldStar {
pos: Point3 { pos: Point3 {
x: rng.gen_range(-sz..=sz), x: rng.gen_range(-sz..=sz),
y: 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 { tint: Vector2 {
x: rng.gen_range(0.0..=1.0), x: rng.gen_range(0.0..=1.0),
y: rng.gen_range(0.0..=1.0), y: rng.gen_range(0.0..=1.0),
@ -54,18 +58,18 @@ impl Starfield {
.collect(); .collect();
} }
pub fn make_instances(&mut self, aspect: f32) -> Vec<StarfieldInstance> { pub fn update_buffer(&mut self, ct: &Content, state: &mut RenderState) {
let sz = STARFIELD_SIZE as f32; let sz = ct.get_config().starfield_size as f32;
// Compute window size in starfield tiles // Compute window size in starfield tiles
let mut nw_tile: Point2<i32> = { let mut nw_tile: Point2<i32> = {
// Game coordinates (relative to camera) of nw corner of screen. // 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. // Parallax correction.
// Also, adjust v for mod to work properly // Also, adjust v for mod to work properly
// (v is centered at 0) // (v is centered at 0)
let v: Point2<f32> = clip_nw * STARFIELD_Z_MIN; let v: Point2<f32> = clip_nw * ct.get_config().starfield_min_dist;
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
#[rustfmt::skip] #[rustfmt::skip]
@ -89,14 +93,14 @@ impl Starfield {
// Truncate tile grid to buffer size // Truncate tile grid to buffer size
// (The window won't be full of stars if our instance limit is too small) // (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) while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * ct.get_config().starfield_count as i32)
> STARFIELD_SPRITE_INSTANCE_LIMIT as i32 > ct.get_config().starfield_instance_limit as i32
{ {
nw_tile -= Vector2::from((1, 1)); nw_tile -= Vector2::from((1, 1));
} }
// Add all tiles to buffer // 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 x in (-nw_tile.x)..=nw_tile.x {
for y in (-nw_tile.y)..=nw_tile.y { for y in (-nw_tile.y)..=nw_tile.y {
let offset = Vector3 { let offset = Vector3 {
@ -105,21 +109,21 @@ impl Starfield {
z: 0.0, z: 0.0,
}; };
for s in &self.stars { for s in &self.stars {
instances.push(StarfieldInstance { state.queue.write_buffer(
position: (s.pos + offset).into(), &state.vertex_buffers.starfield.instances,
size: s.size, StarfieldInstance::SIZE * self.instance_count as u64,
tint: s.tint.into(), 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;
} }
} }

View File

@ -9,7 +9,7 @@ pub(super) struct FpsIndicator {
impl FpsIndicator { impl FpsIndicator {
pub fn new(state: &mut RenderState) -> Self { 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( buffer.set_size(
&mut state.text_font_system, &mut state.text_font_system,
state.window_size.width as f32, state.window_size.width as f32,
@ -36,7 +36,7 @@ impl FpsIndicator {
self.buffer.set_text( self.buffer.set_text(
&mut state.text_font_system, &mut state.text_font_system,
&format!( &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.frame / input.timing.render),
100.0 * (input.timing.galaxy / input.timing.frame), 100.0 * (input.timing.galaxy / input.timing.frame),
100.0 * (input.timing.physics_sim / input.timing.frame), 100.0 * (input.timing.physics_sim / input.timing.frame),

View File

@ -32,9 +32,9 @@ impl Radar {
.unwrap(); .unwrap();
let player_position = util::rigidbody_position(player_body); let player_position = util::rigidbody_position(player_body);
let planet_sprite = input.content.get_sprite_handle("ui::planetblip"); let planet_sprite = input.ct.get_sprite_handle("ui::planetblip");
let ship_sprite = input.content.get_sprite_handle("ui::shipblip"); let ship_sprite = input.ct.get_sprite_handle("ui::shipblip");
let arrow_sprite = input.content.get_sprite_handle("ui::centerarrow"); let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow");
// Enforce buffer limit // Enforce buffer limit
if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT { if state.vertex_buffers.ui_counter as u64 > UI_SPRITE_INSTANCE_LIMIT {
@ -52,13 +52,13 @@ impl Radar {
angle: 0.0, angle: 0.0,
size: radar_size, size: radar_size,
color: [1.0, 1.0, 1.0, 1.0], 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; state.vertex_buffers.ui_counter += 1;
// Draw system objects // 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 { for o in &system.objects {
let size = (o.size / o.pos.z) / (radar_range * system_object_scale); let size = (o.size / o.pos.z) / (radar_range * system_object_scale);
let p = Point2 { let p = Point2 {
@ -107,17 +107,17 @@ impl Radar {
for (s, r) in input.systemsim.iter_ship_body() { for (s, r) in input.systemsim.iter_ship_body() {
// This will be None if this ship is dead. // This will be None if this ship is dead.
// Stays around while the physics system runs a collapse sequence // 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 => { None => {
// TODO: configurable // TODO: configurable
[0.2, 0.2, 0.2, 1.0] [0.2, 0.2, 0.2, 1.0]
} }
Some(data) => { 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] [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 size = (ship.size * ship.sprite.aspect) * ship_scale;
let p = util::rigidbody_position(r); let p = util::rigidbody_position(r);
let d = (p - player_position) / radar_range; let d = (p - player_position) / radar_range;
@ -167,7 +167,7 @@ impl Radar {
let d = d * (radar_size / 2.0); let d = d * (radar_size / 2.0);
let color = [0.3, 0.3, 0.3, 1.0]; let color = [0.3, 0.3, 0.3, 1.0];
if m < 0.8 { 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); let size = 7.0f32.min((0.8 - m) * 70.0);
// Enforce buffer limit (this section adds four items) // Enforce buffer limit (this section adds four items)

View File

@ -27,14 +27,11 @@ impl Status {
let player_world_object = input.systemsim.get_ship(input.player_data).unwrap(); let player_world_object = input.systemsim.get_ship(input.player_data).unwrap();
let data = input let data = input.gx.get_ship(player_world_object.data_handle).unwrap();
.data
.get_ship(player_world_object.data_handle)
.unwrap();
let max_shields = data.get_outfits().get_shield_strength(); let max_shields = data.get_outfits().get_shield_strength();
let current_shields = data.get_shields(); let current_shields = data.get_shields();
let current_hull = data.get_hull(); 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.queue.write_buffer(
&state.vertex_buffers.ui.instances, &state.vertex_buffers.ui.instances,
@ -45,7 +42,7 @@ impl Status {
angle: 0.0, angle: 0.0,
size: 200.0, size: 200.0,
color: [1.0, 1.0, 1.0, 1.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; state.vertex_buffers.ui_counter += 1;

View File

@ -2,48 +2,6 @@
// TODO: many of these should be moved to a config file or cli option // 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. /// We can draw at most this many object sprites on the screen.
pub const OBJECT_SPRITE_INSTANCE_LIMIT: u64 = 500; 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. /// The size of our circular particle buffer. When we create particles, the oldest ones are replaced.
pub const PARTICLE_SPRITE_INSTANCE_LIMIT: u64 = 1000; 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 /// The maximum number of sprites we can define
pub const SPRITE_LIMIT: u32 = 1024; pub const SPRITE_LIMIT: u32 = 1024;