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

View file

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

View file

@ -6,13 +6,33 @@ use crate::{Point, SCREEN_WIDTH};
// This is the type stored in the `Assets` hash map
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.
#[derive(Debug)]
pub(crate) struct Sprite {
width: usize,
height: usize,
pixels: Vec<u8>,
frame: String,
frame: Frame,
}
/// SpriteRefs can be drawn and animated.
@ -23,30 +43,73 @@ pub(crate) struct SpriteRef {
width: usize,
height: usize,
pixels: Rc<Vec<u8>>,
frame: String,
frame: Frame,
}
pub(crate) trait Sprites {
fn new(assets: &Assets, name: &str) -> Self;
/// Drawables can be blitted to the pixel buffer and animated.
pub(crate) trait Drawable {
fn width(&self) -> usize;
fn height(&self) -> usize;
fn pixels(&self) -> &[u8];
fn update_pixels(&mut self, pixels: Rc<Vec<u8>>);
fn frame(&self) -> &str;
fn update_frame(&mut self, frame: &str);
fn frame(&self) -> &Frame;
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 {
fn new(assets: &Assets, name: &str) -> Sprite {
let cached_sprite = assets.sprites().get(name).unwrap();
Sprite {
width: cached_sprite.0,
height: cached_sprite.1,
pixels: cached_sprite.2.to_vec(),
frame: name.into(),
}
}
impl Sprite {
pub(crate) fn new(assets: &Assets, frame: Frame) -> Sprite {
let (width, height, pixels) = assets.sprites().get(&frame).unwrap();
Sprite {
width: *width,
height: *height,
pixels: pixels.to_vec(),
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 {
self.width
}
@ -60,29 +123,19 @@ impl Sprites for Sprite {
}
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
}
fn update_frame(&mut self, frame: &str) {
self.frame = frame.into();
fn update_frame(&mut self, frame: Frame) {
self.frame = frame;
}
}
impl Sprites 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(),
}
}
impl Drawable for SpriteRef {
fn width(&self) -> usize {
self.width
}
@ -99,18 +152,19 @@ impl Sprites for SpriteRef {
self.pixels = pixels;
}
fn frame(&self) -> &str {
fn frame(&self) -> &Frame {
&self.frame
}
fn update_frame(&mut self, frame: &str) {
self.frame = frame.into();
fn update_frame(&mut self, frame: Frame) {
self.frame = frame;
}
}
/// Blit a drawable to the pixel buffer.
pub(crate) fn blit<S>(screen: &mut [u8], dest: &Point, sprite: &S)
where
S: Sprites,
S: Drawable,
{
let pixels = sprite.pixels();
let width = sprite.width() * 4;