Refactor SpriteCache and animation (#3)

* Refactor SpriteCache and animations

- Animation frames are now enumerated
- Renamed the `Sprites` trait into the more apt `Drawable`
- Moved animation to a default impl on `Drawable`
- Added the Cthulhu invader
- Clarify that `World::step_invaders()` is only needed for handling the invader grid
- Renamed `Cannon` to `Bullet`

* Improve readability of some `SpriteCache` uses.
This commit is contained in:
Jay Oster 2019-10-07 20:22:34 -07:00 committed by GitHub
parent e4249ed766
commit d7fc1eca3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 90 deletions

Binary file not shown.

Binary file not shown.

View file

@ -8,7 +8,7 @@ use std::time::Duration;
pub use controls::{Controls, Direction}; pub use controls::{Controls, Direction};
use loader::{load_assets, Assets}; use loader::{load_assets, Assets};
use sprites::{blit, Sprite, SpriteRef, Sprites}; use sprites::{blit, Drawable, Frame, Sprite, SpriteRef};
mod controls; mod controls;
mod loader; mod loader;
@ -31,7 +31,7 @@ pub struct World {
lasers: Vec<Laser>, lasers: Vec<Laser>,
shields: Vec<Shield>, shields: Vec<Shield>,
player: Player, player: Player,
cannons: Vec<Cannon>, bullets: Vec<Bullet>,
score: u32, score: u32,
assets: Assets, assets: Assets,
screen: Vec<u8>, screen: Vec<u8>,
@ -103,7 +103,7 @@ struct Laser {
/// The cannon entity. /// The cannon entity.
#[derive(Debug)] #[derive(Debug)]
struct Cannon { struct Bullet {
sprite: SpriteRef, sprite: SpriteRef,
pos: Point, pos: Point,
} }
@ -111,6 +111,8 @@ struct Cannon {
impl World { impl World {
/// Create a new simple-invaders `World`. /// Create a new simple-invaders `World`.
pub fn new() -> World { pub fn new() -> World {
use Frame::*;
// Load assets first // Load assets first
let assets = load_assets(); let assets = load_assets();
@ -120,12 +122,12 @@ impl World {
bounds: Bounds::default(), bounds: Bounds::default(),
}; };
let player = Player { let player = Player {
sprite: SpriteRef::new(&assets, "player1"), sprite: SpriteRef::new(&assets, Player1),
pos: Point::new(80, 216), pos: Point::new(80, 216),
}; };
let shields = (0..4) let shields = (0..4)
.map(|i| Shield { .map(|i| Shield {
sprite: Sprite::new(&assets, "shield"), sprite: Sprite::new(&assets, Shield1),
pos: Point::new(i * 45 + 32, 192), pos: Point::new(i * 45 + 32, 192),
}) })
.collect(); .collect();
@ -139,7 +141,7 @@ impl World {
lasers: Vec::new(), lasers: Vec::new(),
shields, shields,
player, player,
cannons: Vec::new(), bullets: Vec::new(),
score: 0, score: 0,
assets, assets,
screen, screen,
@ -153,17 +155,21 @@ impl World {
/// ///
/// * `dt`: The time delta since last update. /// * `dt`: The time delta since last update.
/// * `controls`: The player inputs. /// * `controls`: The player inputs.
pub fn update(&mut self, dt: Duration, controls: Controls) { pub fn update(&mut self, dt: Duration, _controls: Controls) {
let one_frame = Duration::new(0, 16_666_667); let one_frame = Duration::new(0, 16_666_667);
// Advance the timer by the delta time // Advance the timer by the delta time
self.timing += dt; self.timing += dt;
// Step the game logic by one frame at a time // Step the invaders one by one
while self.timing >= one_frame { while self.timing >= one_frame {
self.timing -= one_frame; self.timing -= one_frame;
self.step(&controls); self.step_invaders();
} }
// TODO: Handle controls to move the player
// TODO: Handle lasers and bullets
// Movements can be multiplied by the delta-time frame count, instead of looping
} }
/// Draw the internal state to the screen. /// Draw the internal state to the screen.
@ -183,7 +189,7 @@ impl World {
&self.screen &self.screen
} }
fn step(&mut self, _controls: &Controls) { fn step_invaders(&mut self) {
// Find the next invader // Find the next invader
let mut invader = None; let mut invader = None;
while let None = invader { while let None = invader {
@ -192,18 +198,10 @@ impl World {
} }
let invader = invader.unwrap(); let invader = invader.unwrap();
// Animate the invader // TODO: Move the invader
let assets = self.assets.sprites();
let (pixels, frame) = match invader.sprite.frame().as_ref() {
"blipjoy1" => (assets.get("blipjoy2").unwrap().2.clone(), "blipjoy2"),
"blipjoy2" => (assets.get("blipjoy1").unwrap().2.clone(), "blipjoy1"),
"ferris1" => (assets.get("ferris2").unwrap().2.clone(), "ferris2"),
"ferris2" => (assets.get("ferris1").unwrap().2.clone(), "ferris1"),
_ => unreachable!(),
};
invader.sprite.update_pixels(pixels); // Animate the invader
invader.sprite.update_frame(frame); invader.sprite.animate(&self.assets);
} }
/// Clear the screen /// Clear the screen
@ -279,6 +277,8 @@ impl Default for Bounds {
/// Create a grid of invaders. /// Create a grid of invaders.
fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> { fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
use Frame::*;
const BLIPJOY_OFFSET: Point = Point::new(3, 4); const BLIPJOY_OFFSET: Point = Point::new(3, 4);
const FERRIS_OFFSET: Point = Point::new(3, 5); const FERRIS_OFFSET: Point = Point::new(3, 5);
@ -287,7 +287,7 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
(0..COLS) (0..COLS)
.map(|x| { .map(|x| {
Some(Invader { Some(Invader {
sprite: SpriteRef::new(assets, "blipjoy1"), sprite: SpriteRef::new(assets, Blipjoy1),
pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID, pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID,
score: 10, score: 10,
}) })
@ -298,7 +298,7 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
(0..COLS) (0..COLS)
.map(|x| { .map(|x| {
Some(Invader { Some(Invader {
sprite: SpriteRef::new(assets, "ferris1"), sprite: SpriteRef::new(assets, Ferris1),
pos: START + FERRIS_OFFSET + Point::new(x, y) * GRID, pos: START + FERRIS_OFFSET + Point::new(x, y) * GRID,
score: 10, score: 10,
}) })
@ -309,8 +309,7 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
(0..COLS) (0..COLS)
.map(|x| { .map(|x| {
Some(Invader { Some(Invader {
// TODO: Need a third invader sprite: SpriteRef::new(assets, Cthulhu1),
sprite: SpriteRef::new(assets, "blipjoy1"),
pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID, pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID,
score: 10, score: 10,
}) })

View file

@ -2,55 +2,43 @@ use std::collections::HashMap;
use std::io::Cursor; use std::io::Cursor;
use std::rc::Rc; use std::rc::Rc;
use crate::sprites::CachedSprite; use crate::sprites::{CachedSprite, Frame};
/// A list of assets loaded into memory. /// A list of assets loaded into memory.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Assets { pub(crate) struct Assets {
// sounds: TODO // sounds: TODO
sprites: HashMap<String, CachedSprite>, sprites: HashMap<Frame, CachedSprite>,
} }
impl Assets { impl Assets {
pub(crate) fn sprites(&self) -> &HashMap<String, CachedSprite> { pub(crate) fn sprites(&self) -> &HashMap<Frame, CachedSprite> {
&self.sprites &self.sprites
} }
} }
/// Load all static assets into an `Assets` structure /// Load all static assets into an `Assets` structure
pub(crate) fn load_assets() -> Assets { pub(crate) fn load_assets() -> Assets {
use Frame::*;
let mut sprites = HashMap::new(); let mut sprites = HashMap::new();
sprites.insert( sprites.insert(Blipjoy1, load_pcx(include_bytes!("assets/blipjoy1.pcx")));
"blipjoy1".into(), sprites.insert(Blipjoy2, load_pcx(include_bytes!("assets/blipjoy2.pcx")));
load_pcx(include_bytes!("assets/blipjoy1.pcx")),
); sprites.insert(Ferris1, load_pcx(include_bytes!("assets/ferris1.pcx")));
sprites.insert( sprites.insert(Ferris2, load_pcx(include_bytes!("assets/ferris2.pcx")));
"blipjoy2".into(),
load_pcx(include_bytes!("assets/blipjoy2.pcx")), sprites.insert(Cthulhu1, load_pcx(include_bytes!("assets/cthulhu1.pcx")));
); sprites.insert(Cthulhu2, load_pcx(include_bytes!("assets/cthulhu2.pcx")));
sprites.insert(
"ferris1".into(), sprites.insert(Player1, load_pcx(include_bytes!("assets/player1.pcx")));
load_pcx(include_bytes!("assets/ferris1.pcx")), sprites.insert(Player2, load_pcx(include_bytes!("assets/player2.pcx")));
);
sprites.insert( sprites.insert(Shield1, load_pcx(include_bytes!("assets/shield.pcx")));
"ferris2".into(),
load_pcx(include_bytes!("assets/ferris2.pcx")), // sprites.insert(Laser1, load_pcx(include_bytes!("assets/laser1.pcx")));
); // sprites.insert(Laser2, load_pcx(include_bytes!("assets/laser2.pcx")));
sprites.insert(
"player1".into(),
load_pcx(include_bytes!("assets/player1.pcx")),
);
sprites.insert(
"player2".into(),
load_pcx(include_bytes!("assets/player2.pcx")),
);
sprites.insert(
"shield".into(),
load_pcx(include_bytes!("assets/shield.pcx")),
);
// sprites.insert("laser1".into(), load_pcx(include_bytes!("assets/laser1.pcx")));
// sprites.insert("laser2".into(), load_pcx(include_bytes!("assets/laser2.pcx")));
Assets { sprites } Assets { sprites }
} }

View file

@ -6,13 +6,33 @@ use crate::{Point, SCREEN_WIDTH};
// This is the type stored in the `Assets` hash map // This is the type stored in the `Assets` hash map
pub(crate) type CachedSprite = (usize, usize, Rc<Vec<u8>>); pub(crate) type CachedSprite = (usize, usize, Rc<Vec<u8>>);
/// Frame identifier for managing animations.
#[derive(Debug, Eq, Hash, PartialEq)]
pub(crate) enum Frame {
Blipjoy1,
Blipjoy2,
Ferris1,
Ferris2,
Cthulhu1,
Cthulhu2,
Player1,
Player2,
Shield1,
// Laser1,
// Laser2,
}
/// Sprites can be drawn and animated. /// Sprites can be drawn and animated.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Sprite { pub(crate) struct Sprite {
width: usize, width: usize,
height: usize, height: usize,
pixels: Vec<u8>, pixels: Vec<u8>,
frame: String, frame: Frame,
} }
/// SpriteRefs can be drawn and animated. /// SpriteRefs can be drawn and animated.
@ -23,30 +43,73 @@ pub(crate) struct SpriteRef {
width: usize, width: usize,
height: usize, height: usize,
pixels: Rc<Vec<u8>>, pixels: Rc<Vec<u8>>,
frame: String, frame: Frame,
} }
pub(crate) trait Sprites { /// Drawables can be blitted to the pixel buffer and animated.
fn new(assets: &Assets, name: &str) -> Self; pub(crate) trait Drawable {
fn width(&self) -> usize; fn width(&self) -> usize;
fn height(&self) -> usize; fn height(&self) -> usize;
fn pixels(&self) -> &[u8]; fn pixels(&self) -> &[u8];
fn update_pixels(&mut self, pixels: Rc<Vec<u8>>); fn update_pixels(&mut self, pixels: Rc<Vec<u8>>);
fn frame(&self) -> &str; fn frame(&self) -> &Frame;
fn update_frame(&mut self, frame: &str); fn update_frame(&mut self, frame: Frame);
fn animate(&mut self, assets: &Assets) {
use Frame::*;
let assets = assets.sprites();
let (pixels, frame) = match self.frame() {
Blipjoy1 => (assets.get(&Blipjoy2).unwrap().2.clone(), Blipjoy2),
Blipjoy2 => (assets.get(&Blipjoy1).unwrap().2.clone(), Blipjoy1),
Ferris1 => (assets.get(&Ferris2).unwrap().2.clone(), Ferris2),
Ferris2 => (assets.get(&Ferris1).unwrap().2.clone(), Ferris1),
Cthulhu1 => (assets.get(&Cthulhu2).unwrap().2.clone(), Cthulhu2),
Cthulhu2 => (assets.get(&Cthulhu1).unwrap().2.clone(), Cthulhu1),
Player1 => (assets.get(&Player2).unwrap().2.clone(), Player2),
Player2 => (assets.get(&Player1).unwrap().2.clone(), Player1),
// This should not happen, but here we are!
Shield1 => (assets.get(&Shield1).unwrap().2.clone(), Shield1),
// Laser1 => (assets.get(&Laser2).unwrap().2.clone(), Laser2),
// Laser2 => (assets.get(&Laser1).unwrap().2.clone(), Laser1),
};
self.update_pixels(pixels);
self.update_frame(frame);
}
} }
impl Sprites for Sprite { impl Sprite {
fn new(assets: &Assets, name: &str) -> Sprite { pub(crate) fn new(assets: &Assets, frame: Frame) -> Sprite {
let cached_sprite = assets.sprites().get(name).unwrap(); let (width, height, pixels) = assets.sprites().get(&frame).unwrap();
Sprite { Sprite {
width: cached_sprite.0, width: *width,
height: cached_sprite.1, height: *height,
pixels: cached_sprite.2.to_vec(), pixels: pixels.to_vec(),
frame: name.into(), frame,
} }
} }
}
impl SpriteRef {
pub(crate) fn new(assets: &Assets, frame: Frame) -> SpriteRef {
let (width, height, pixels) = assets.sprites().get(&frame).unwrap();
SpriteRef {
width: *width,
height: *height,
pixels: pixels.clone(),
frame,
}
}
}
impl Drawable for Sprite {
fn width(&self) -> usize { fn width(&self) -> usize {
self.width self.width
} }
@ -60,29 +123,19 @@ impl Sprites for Sprite {
} }
fn update_pixels(&mut self, pixels: Rc<Vec<u8>>) { fn update_pixels(&mut self, pixels: Rc<Vec<u8>>) {
self.pixels.copy_from_slice(&pixels); self.pixels = pixels.to_vec();
} }
fn frame(&self) -> &str { fn frame(&self) -> &Frame {
&self.frame &self.frame
} }
fn update_frame(&mut self, frame: &str) { fn update_frame(&mut self, frame: Frame) {
self.frame = frame.into(); self.frame = frame;
} }
} }
impl Sprites for SpriteRef { impl Drawable for SpriteRef {
fn new(assets: &Assets, name: &str) -> SpriteRef {
let cached_sprite = assets.sprites().get(name).unwrap();
SpriteRef {
width: cached_sprite.0,
height: cached_sprite.1,
pixels: cached_sprite.2.clone(),
frame: name.into(),
}
}
fn width(&self) -> usize { fn width(&self) -> usize {
self.width self.width
} }
@ -99,18 +152,19 @@ impl Sprites for SpriteRef {
self.pixels = pixels; self.pixels = pixels;
} }
fn frame(&self) -> &str { fn frame(&self) -> &Frame {
&self.frame &self.frame
} }
fn update_frame(&mut self, frame: &str) { fn update_frame(&mut self, frame: Frame) {
self.frame = frame.into(); self.frame = frame;
} }
} }
/// Blit a drawable to the pixel buffer.
pub(crate) fn blit<S>(screen: &mut [u8], dest: &Point, sprite: &S) pub(crate) fn blit<S>(screen: &mut [u8], dest: &Point, sprite: &S)
where where
S: Sprites, S: Drawable,
{ {
let pixels = sprite.pixels(); let pixels = sprite.pixels();
let width = sprite.width() * 4; let width = sprite.width() * 4;