Reworked sprite content

master
Mark 2024-01-20 09:36:12 -08:00
parent 7334ebd00e
commit ad34dc4f70
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
7 changed files with 561 additions and 150 deletions

View File

@ -27,8 +27,8 @@ file = "ship/gypsum.png"
[sprite."ship::peregrine"]
timing.duration = 2
repeat = "reverse"
random_start_frame = true
top = "reverse"
bot = "reverse"
frames = [
"ship/peregrine/01.png",
"ship/peregrine/02.png",
@ -73,7 +73,6 @@ file = "ui/landscape-mask.png"
[sprite."particle::blaster"]
timing.duration = 0.15
repeat = "once"
frames = [
"particle/blaster/01.png",
"particle/blaster/02.png",
@ -84,7 +83,6 @@ frames = [
[sprite."particle::explosion::tiny"]
timing.fps = 15
repeat = "once"
frames = [
"particle/explosion-tiny/01.png",
"particle/explosion-tiny/02.png",
@ -96,7 +94,6 @@ frames = [
[sprite."particle::explosion::small"]
timing.fps = 15
repeat = "once"
frames = [
"particle/explosion-small/01.png",
"particle/explosion-small/02.png",
@ -109,7 +106,6 @@ frames = [
[sprite."particle::explosion::medium"]
timing.fps = 15
repeat = "once"
frames = [
"particle/explosion-medium/01.png",
"particle/explosion-medium/02.png",
@ -124,7 +120,6 @@ frames = [
[sprite."particle::explosion::large"]
timing.fps = 15
repeat = "once"
frames = [
"particle/explosion-large/01.png",
"particle/explosion-large/02.png",
@ -139,7 +134,6 @@ frames = [
[sprite."particle::explosion::huge"]
timing.fps = 15
repeat = "once"
frames = [
"particle/explosion-huge/01.png",
"particle/explosion-huge/02.png",
@ -156,9 +150,8 @@ frames = [
[sprite."particle::spark::blue"]
timing.duration = 0.3
#timing.rng = 0.2 # each frame will be independently sped up/slowed by this factor
#timing.uniform_rng = 0.2 # one factor for all frames
repeat = "reverse"
top = "reverse"
bot = "reverse"
frames = [
"particle/spark-blue/01.png",
"particle/spark-blue/02.png",
@ -170,7 +163,6 @@ frames = [
[sprite."particle::spark::yellow"]
timing.duration = 0.3
timing.rng = 0.2
repeat = "once"
frames = [
"particle/spark-yellow/01.png",
"particle/spark-yellow/02.png",
@ -182,7 +174,6 @@ frames = [
[sprite."particle::spark::red"]
timing.duration = 0.3
timing.rng = 0.2
repeat = "once"
frames = [
"particle/spark-red/01.png",
"particle/spark-red/02.png",

View File

@ -0,0 +1,234 @@
use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle};
/// A single frame's state
#[derive(Debug, Clone)]
pub struct SpriteAnimationFrame {
/// The index of the texture we're fading from
pub texture_a: u32,
/// The index of the texture we're fading to
pub texture_b: u32,
/// Between 0.0 and 1.0, denoting how far we are between
/// texture_a and texture_b
/// 0.0 means fully show texture_a;
/// 1.0 means fully show texture_b.
pub fade: f32,
}
impl SpriteAnimationFrame {
/// Convenience method.
/// Get texture index as an array
pub fn texture_index(&self) -> [u32; 2] {
[self.texture_a, self.texture_b]
}
}
/// What direction are we playing our animation in?
#[derive(Debug, Clone)]
enum AnimDirection {
/// Top to bottom, with increasing frame indices
/// (normal)
Up,
/// Bottom to top, with decreasing frame indices
/// (reverse)
Down,
/// Stopped, no animation
Stop,
}
/// Manages a single sprite's animation state.
#[derive(Debug, Clone)]
pub struct AnimAutomaton {
/// The sprite we're animating
sprite: SpriteHandle,
/// Which animation section we're on
/// This MUST be a section from this Automaton's sprite
current_section: AnimSectionHandle,
/// Which frame we're on
current_frame: usize,
/// Where we are between frames.
/// Always between zero and one.
current_fade: f32,
/// In what direction are we playing the current section?
current_direction: AnimDirection,
/// The texture we're fading from
/// (if we're moving downwards)
last_texture: u32,
/// The texture we're fading to
/// (if we're moving downwards)
next_texture: u32,
}
impl AnimAutomaton {
/// Create a new AnimAutomaton
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self {
let sprite = ct.get_sprite(sprite_handle);
Self {
current_direction: AnimDirection::Down,
sprite: sprite.handle,
current_frame: 0,
current_fade: 0.0,
current_section: sprite.default_section,
last_texture: *sprite
.get_section(sprite.default_section)
.frames
.first()
.unwrap(),
next_texture: *sprite
.get_section(sprite.default_section)
.frames
.first()
.unwrap(),
}
}
/// Reset this animation
pub fn reset(&mut self, ct: &Content) {
let sprite = ct.get_sprite(self.sprite);
self.current_fade = 0.0;
self.current_frame = 0;
self.current_section = sprite.default_section
}
/// Reverse this animation's direction
pub fn reverse(&mut self) {
match self.current_direction {
AnimDirection::Stop => {}
AnimDirection::Up => {
self.current_direction = AnimDirection::Down;
}
AnimDirection::Down => {
self.current_direction = AnimDirection::Up;
}
}
}
/// Step this animation by `t` seconds
pub fn step(&mut self, ct: &Content, t: f32) {
let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section);
// Current_fade and current_frame keep track of where we are in the current section.
// current_frame indexes this section frames. When it exceeds the number of frames
// or falls below zero (when moving in reverse), we switch to the next section.
//
// current_fade keeps track of our state between frames. It is zero once a frame starts,
// and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases,
// and if we are stepping backwards, it decreases.
// If this is zero, this section isn't animated.
if current_section.frame_duration == 0.0 {
return;
}
match self.current_direction {
AnimDirection::Down => self.current_fade += t / current_section.frame_duration,
AnimDirection::Up => self.current_fade -= t / current_section.frame_duration,
AnimDirection::Stop => {}
}
// We're stepping foward and finished this frame
// (implies we're travelling downwards)
if self.current_fade > 1.0 {
while self.current_fade > 1.0 {
self.current_fade -= 1.0;
}
if self.current_frame < current_section.frames.len() - 1 {
self.current_frame += 1;
} else {
match current_section.edge_bot {
SectionEdge::Stop => {
self.current_fade = 0.0;
self.current_frame = current_section.frames.len() - 1;
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section } => {
self.current_section = section;
self.current_frame = 0;
}
SectionEdge::Bot { section } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
self.reverse();
}
SectionEdge::Restart => {
self.current_frame = 0;
}
SectionEdge::Reverse => {
// Jump to SECOND frame, since we've already shown the
// first during the fade transition
self.current_frame = current_section.frames.len() - 1;
self.reverse()
}
}
}
let current_section = sprite.get_section(self.current_section);
self.last_texture = self.next_texture;
self.next_texture = current_section.frames[self.current_frame];
}
// We're stepping backward and finished this frame
// (implies we're travelling upwards)
if self.current_fade < 0.0 {
while self.current_fade < 0.0 {
self.current_fade += 1.0;
}
if self.current_frame > 0 {
self.current_frame -= 1;
} else {
match current_section.edge_top {
SectionEdge::Stop => {
self.current_fade = 0.0;
self.current_frame = 0;
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section } => {
self.current_section = section;
self.current_frame = 0;
self.reverse();
}
SectionEdge::Bot { section } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
}
SectionEdge::Reverse => {
self.current_frame = 0;
self.reverse();
}
SectionEdge::Restart => {
self.current_frame = current_section.frames.len() - 1;
}
}
}
let current_section = sprite.get_section(self.current_section);
self.next_texture = self.last_texture;
self.last_texture = current_section.frames[self.current_frame];
}
}
/// Get the current frame of this animation
pub fn get_texture_idx(&self) -> SpriteAnimationFrame {
return SpriteAnimationFrame {
texture_a: self.last_texture,
texture_b: self.next_texture,
fade: self.current_fade,
};
}
}

View File

@ -16,14 +16,6 @@ pub struct SpriteHandle {
pub aspect: f32,
}
impl SpriteHandle {
/// The index of this sprite in content's sprite array.
/// Render uses this to build its buffers.
pub fn get_index(&self) -> u32 {
self.index as u32
}
}
impl Hash for SpriteHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state)

View File

@ -3,6 +3,7 @@
//! This subcrate is responsible for loading, parsing, validating game content,
//! which is usually stored in `./content`.
mod animautomaton;
mod handle;
mod part;
mod util;
@ -18,6 +19,7 @@ use std::{
use toml;
use walkdir::WalkDir;
pub use animautomaton::*;
pub use handle::*;
pub use part::*;
@ -279,11 +281,13 @@ impl Content {
}
/// Get the handle for the starfield sprite
pub fn get_starfield_handle(&self) -> SpriteHandle {
match self.starfield_handle {
pub fn get_starfield_texture(&self) -> u32 {
let h = match self.starfield_handle {
Some(h) => h,
None => unreachable!("Starfield sprite hasn't been loaded yet!"),
}
};
let sprite = self.get_sprite(h);
sprite.get_section(sprite.default_section).frames[0]
}
/// Get a handle from a sprite name
@ -306,9 +310,9 @@ impl Content {
return &self.sprite_atlas.atlas_list;
}
/// Get a sprite from a path
pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage {
self.sprite_atlas.index.get(p).unwrap()
/// Get a texture by its index
pub fn get_image(&self, idx: u32) -> &SpriteAtlasImage {
&self.sprite_atlas.index[idx as usize]
}
/// Get an outfit from a handle

View File

@ -57,7 +57,8 @@ pub(crate) mod syntax {
TextOrFloat::Text(s) => {
if s == "inherit" {
let sprite = content.get_sprite(sprite);
sprite.frame_duration * sprite.frames.len() as f32
let sec = sprite.get_section(sprite.default_section);
sec.frame_duration * sec.frames.len() as f32
} else {
bail!("bad effect lifetime, must be float or \"inherit\"",)
}

View File

@ -18,5 +18,5 @@ pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
ShipCollapse,
};
pub use sprite::{RepeatMode, Sprite};
pub use sprite::*;
pub use system::{System, SystemObject};

View File

@ -1,44 +1,36 @@
use anyhow::{bail, Context, Result};
use image::io::Reader;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use std::collections::HashMap;
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
pub(crate) mod syntax {
use crate::{Content, ContentBuildContext};
use anyhow::{anyhow, bail, Context, Ok, Result};
use serde::Deserialize;
use std::path::PathBuf;
use std::{collections::HashMap, path::PathBuf};
use super::RepeatMode;
use super::AnimSectionHandle;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
/// Convenience variants of sprite definitions
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Sprite {
Static(StaticSprite),
Frames(FrameSprite),
}
#[derive(Debug, Deserialize)]
pub struct StaticSprite {
pub file: PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct FrameSprite {
pub frames: Vec<PathBuf>,
pub timing: Timing,
pub repeat: RepeatMode,
pub random_start_frame: Option<bool>,
OneSection(SpriteSection),
Complete(CompleteSprite),
}
/// Two ways to specify animation length
#[derive(Debug, Deserialize)]
pub enum TimingVariant {
/// The duration of this whole section
#[serde(rename = "duration")]
Duration(f32),
/// The fps of this section
#[serde(rename = "fps")]
Fps(f32),
}
@ -47,36 +39,176 @@ pub(crate) mod syntax {
pub struct Timing {
#[serde(flatten)]
pub variant: TimingVariant,
//pub uniform_rng: Option<f32>,
}
/// An unanimated sprite
#[derive(Debug, Deserialize)]
pub struct StaticSprite {
pub file: PathBuf,
}
/// The proper, full sprite definition
#[derive(Debug, Deserialize)]
pub struct CompleteSprite {
pub section: HashMap<String, SpriteSection>,
pub default_section: String,
}
/// A single animation section
#[derive(Debug, Deserialize)]
pub struct SpriteSection {
pub frames: Vec<PathBuf>,
pub timing: Timing,
pub top: Option<SectionEdge>,
pub bot: Option<SectionEdge>,
}
impl SpriteSection {
pub fn add_to(
&self,
_build_context: &mut ContentBuildContext,
content: &mut Content,
// An index of all sections in this sprite, used to resolve
// top and bot edges.
all_sections: &HashMap<String, AnimSectionHandle>,
) -> Result<((u32, u32), super::SpriteSection)> {
// Make sure all frames have the same size and add them
// to the frame vector
let mut dim = None;
let mut frames = Vec::new();
for f in &self.frames {
let idx = match content.sprite_atlas.path_map.get(f) {
Some(s) => *s,
None => {
bail!("error: file `{}` isn't in the sprite atlas", f.display());
}
};
let img = &content.sprite_atlas.index[idx as usize];
match dim {
None => dim = Some(img.true_size),
Some(e) => {
if img.true_size != e {
bail!("failed to load section frames because frames have different sizes.",)
}
}
}
/// How to replay a texture's animation
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum RepeatMode {
/// Play this animation once, and stop at the last frame
#[serde(rename = "once")]
Once,
frames.push(img.idx);
}
let dim = dim.unwrap();
/// After the first frame, jump to the last frame
#[serde(rename = "repeat")]
Repeat,
let frame_duration = match self.timing.variant {
TimingVariant::Duration(d) => d / self.frames.len() as f32,
TimingVariant::Fps(f) => 1.0 / f,
};
/// Play this animation in reverse after the last frame
#[serde(rename = "reverse")]
if frame_duration <= 0.0 {
bail!("frame duration must be positive (and therefore nonzero).")
}
let edge_top = match &self.top {
Some(x) => x.resolve(all_sections)?,
None => super::SectionEdge::Stop,
};
let edge_bot = match &self.bot {
Some(x) => x.resolve(all_sections)?,
None => super::SectionEdge::Stop,
};
return Ok((
dim,
super::SpriteSection {
frames,
frame_duration,
edge_top,
edge_bot,
},
));
}
}
/// A link between two animation sections
#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct SectionEdge {
pub val: String,
}
impl SectionEdge {
pub fn resolve(
&self,
all_sections: &HashMap<String, AnimSectionHandle>,
) -> Result<super::SectionEdge> {
if self.val == "stop" {
return Ok(super::SectionEdge::Stop);
}
if self.val == "reverse" {
return Ok(super::SectionEdge::Reverse);
}
if self.val == "restart" {
return Ok(super::SectionEdge::Restart);
}
let (s, p) = match self.val.split_once(":") {
Some(x) => x,
None => {
bail!("bad section edge specification `{}`", self.val);
}
};
let section = match all_sections.get(s) {
Some(s) => *s,
None => {
return Err(anyhow!("bad section edge specification `{}`", self.val))
.with_context(|| format!("section `{}` doesn't exist", s));
}
};
match p {
"top" => Ok(super::SectionEdge::Top { section }),
"bot" => Ok(super::SectionEdge::Bot { section }),
_ => {
return Err(anyhow!("bad section edge specification `{}`", self.val))
.with_context(|| format!("invalid target `{}`", p));
}
}
}
}
}
// TODO: should be pub crate
/// A handle for an animation section inside a sprite
#[derive(Debug, Copy, Clone)]
pub struct AnimSectionHandle(usize);
/// An edge between two animation sections
#[derive(Debug, Clone)]
pub enum SectionEdge {
/// Stop at the last frame of this section
Stop,
/// Play the given section from the bottm
Bot {
/// The section to play
section: AnimSectionHandle,
},
/// Play the given section from the top
Top {
/// The section to play
section: AnimSectionHandle,
},
/// Replay this section in the opposite direction
Reverse,
}
impl RepeatMode {
/// Represent this repeatmode as an integer
/// Used to pass this enum into shaders
pub fn as_int(&self) -> u32 {
match self {
Self::Repeat => 0,
Self::Once => 1,
Self::Reverse => 2,
}
}
/// Restart this section from the opposite end
Restart,
}
/// Represents a sprite that may be used in the game.
@ -88,25 +220,44 @@ pub struct Sprite {
/// This sprite's handle
pub handle: SpriteHandle,
/// The file names of frames of this sprite.
/// unanimated sprites have one frame.
pub frames: Vec<PathBuf>,
/// This sprite's default section
pub default_section: AnimSectionHandle,
/// The speed of this sprite's animation.
/// This is zero for unanimate sprites.
pub frame_duration: f32,
/// All frames will be sped up/slowed by this factor.
//pub frame_uniform_rng: f32,
/// How to replay this sprite's animation
pub repeat: RepeatMode,
/// This sprite's animation sections
sections: Vec<SpriteSection>,
/// Aspect ratio of this sprite (width / height)
pub aspect: f32,
}
/// If true, start on a random frame of this sprite.
pub random_start_frame: bool,
impl Sprite {
/// Get an animation section from a handle
pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection {
&self.sections[section.0]
}
/// Iterate this sprite's sections
pub fn iter_sections(&self) -> impl Iterator<Item = &SpriteSection> {
self.sections.iter()
}
}
/// A part of a sprite's animation
#[derive(Debug, Clone)]
pub struct SpriteSection {
/// The texture index of each frame in this animation section.
/// unanimated sections have one frame.
pub frames: Vec<u32>,
/// The speed of this sprite's animation.
/// This must always be positive (and therefore, nonzero)
pub frame_duration: f32,
/// What to do when we reach the top of this section
pub edge_top: SectionEdge,
/// What to do when we reach the bottom of this section
pub edge_bot: SectionEdge,
}
impl crate::Build for Sprite {
@ -114,31 +265,32 @@ impl crate::Build for Sprite {
fn build(
sprites: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> Result<()> {
for (sprite_name, t) in sprites {
match t {
syntax::Sprite::Static(t) => {
let file = content.config.sprite_root.join(&t.file);
let reader = Reader::open(&file).with_context(|| {
format!(
"Failed to read file `{}` in sprite `{}`",
file.display(),
sprite_name,
let idx = match content.sprite_atlas.path_map.get(&t.file) {
Some(s) => *s,
None => {
return Err(
anyhow!("error while processing sprite `{}`", sprite_name,),
)
})?;
let dim = reader.into_dimensions().with_context(|| {
.with_context(|| {
format!(
"Failed to get dimensions of file `{}` in sprite `{}`",
file.display(),
sprite_name,
"file `{}` isn't in the sprite atlas, cannot proceed",
t.file.display()
)
})?;
});
}
};
let img = &content.sprite_atlas.index[idx as usize];
let aspect = img.w / img.h;
let h = SpriteHandle {
index: content.sprites.len(),
aspect: dim.0 as f32 / dim.1 as f32,
aspect,
};
if sprite_name == content.config.starfield_sprite {
@ -154,71 +306,108 @@ impl crate::Build for Sprite {
content.sprites.push(Self {
name: sprite_name,
frames: vec![t.file],
frame_duration: 0.0,
//frame_uniform_rng: 0.0,
default_section: AnimSectionHandle(0),
sections: vec![SpriteSection {
frames: vec![img.idx],
// We implement unanimated sprites with a very fast framerate
// and STOP endpoints.
frame_duration: 0.01,
edge_top: SectionEdge::Stop,
edge_bot: SectionEdge::Stop,
}],
handle: h,
repeat: RepeatMode::Once,
aspect: dim.0 as f32 / dim.1 as f32,
random_start_frame: false,
aspect,
});
}
syntax::Sprite::Frames(t) => {
let mut dim = None;
for f in &t.frames {
let file = content.config.sprite_root.join(f);
let reader = Reader::open(&file).with_context(|| {
format!(
"Failed to read file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
let d = reader.into_dimensions().with_context(|| {
format!(
"Failed to get dimensions of file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
match dim {
None => dim = Some(d),
Some(e) => {
if d != e {
bail!(
"Failed to load frames of sprite `{}` because frames have different sizes.",
sprite_name,
)
}
}
}
}
let dim = dim.unwrap();
syntax::Sprite::OneSection(s) => {
let mut section_names: HashMap<String, _> = HashMap::new();
// Name the one section in this sprite "anim"
section_names.insert("anim".to_owned(), AnimSectionHandle(0));
let (dim, section) = s
.add_to(build_context, content, &section_names)
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
let aspect = dim.0 as f32 / dim.1 as f32;
let h = SpriteHandle {
index: content.sprites.len(),
aspect: dim.0 as f32 / dim.1 as f32,
aspect,
};
// TODO: remove?
if sprite_name == content.config.starfield_sprite {
unreachable!("Starfield texture may not be animated")
}
let frame_duration = match t.timing.variant {
syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32,
syntax::TimingVariant::Fps(f) => 1.0 / f,
let mut sections = Vec::new();
sections.push(section);
content.sprite_index.insert(sprite_name.clone(), h);
content.sprites.push(Self {
name: sprite_name,
sections,
default_section: AnimSectionHandle(0),
handle: h,
aspect,
});
}
syntax::Sprite::Complete(s) => {
let mut idx = 0;
let mut section_names = HashMap::new();
for (name, _) in &s.section {
section_names.insert(name.to_owned(), AnimSectionHandle(idx));
idx += 1;
}
if !section_names.contains_key(&s.default_section) {
bail!(
"could not load sprite `{}`, default section `{}` doesn't exist",
sprite_name,
s.default_section
);
}
let mut sections = Vec::with_capacity(idx);
let mut dim = None;
// Make sure we add sections in order
let mut names = section_names.iter().collect::<Vec<_>>();
names.sort_by(|a, b| (a.1).0.cmp(&(b.1).0));
for (k, _) in names {
let v = s.section.get(k).unwrap();
let (d, s) = v
.add_to(build_context, content, &section_names)
.with_context(|| format!("while parsing sprite `{}`", sprite_name))
.with_context(|| format!("while parsing section `{}`", k))?;
// Make sure all dimensions are the same
if dim.is_none() {
dim = Some(d);
} else if dim.unwrap() != d {
bail!(
"could not load sprite `{}`, image sizes in section `{}` are different",
sprite_name,
k
);
}
sections.push(s);
}
let dim = dim.unwrap();
let aspect = dim.0 as f32 / dim.1 as f32;
let h = SpriteHandle {
index: content.sprites.len(),
aspect,
};
content.sprite_index.insert(sprite_name.clone(), h);
content.sprites.push(Self {
name: sprite_name,
frames: t.frames,
frame_duration,
//frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0),
sections,
default_section: *section_names.get(&s.default_section).unwrap(),
handle: h,
repeat: t.repeat,
aspect: dim.0 as f32 / dim.1 as f32,
random_start_frame: t.random_start_frame.unwrap_or(false),
aspect,
});
}
}