Initial simple-invaders WIP

- Currently animates two flavors of invader in their usual formation
- Lots left to do, but this is a good start
This commit is contained in:
Jay Oster 2019-10-04 22:48:29 -07:00
parent 2847a8bd39
commit 3b7638a012
12 changed files with 555 additions and 47 deletions

17
Cargo.lock generated
View file

@ -672,6 +672,14 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pcx"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -684,6 +692,7 @@ dependencies = [
"env_logger 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pixels-mocks 0.1.0",
"simple-invaders 0.1.0",
"vk-shader-macros 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"wgpu 0.3.0 (git+https://github.com/gfx-rs/wgpu-rs?rev=697393df4793e1a58578209885036114adfb9213)",
"winit 0.20.0-alpha3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -906,6 +915,13 @@ dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "simple-invaders"
version = "0.1.0"
dependencies = [
"pcx 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "slab"
version = "0.4.2"
@ -1410,6 +1426,7 @@ dependencies = [
"checksum ordered-float 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "18869315e81473c951eb56ad5558bbc56978562d3ecfb87abb7a1e944cea4518"
"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
"checksum pcx 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1b00b062973776578e7863f8395f86e821760d827384f1ad9371d0893c87481a"
"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
"checksum pkg-config 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea"
"checksum proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e688f31d92ffd7c1ddc57a1b4e6d773c0f2a14ee437a4b0a4f5a69c80eb221c8"

View file

@ -18,9 +18,11 @@ wgpu = { git = "https://github.com/gfx-rs/wgpu-rs", rev = "697393df4793e1a585782
env_logger = "0.7"
log = { version = "0.4", features = ["release_max_level_warn"] }
pixels-mocks = { path = "pixels-mocks" }
simple-invaders = { path = "simple-invaders" }
winit = "0.20.0-alpha3"
[workspace]
members = [
"pixels-mocks",
"simple-invaders",
]

View file

@ -1,54 +1,21 @@
use pixels::{Error, Pixels, SurfaceTexture};
use simple_invaders::{World, SCREEN_HEIGHT, SCREEN_WIDTH};
use winit::event;
use winit::event_loop::{ControlFlow, EventLoop};
fn scale_pixel_ferris(width: u32, height: u32) -> Vec<u8> {
let mut px = Vec::new();
const FERRIS_WIDTH: u32 = 11;
const FERRIS_HEIGHT: u32 = 5;
#[rustfmt::skip]
const FERRIS: [u8; (FERRIS_WIDTH * FERRIS_HEIGHT) as usize] = [
0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0,
1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1,
0, 0, 1, 1, 2, 1, 2, 1, 1, 0, 0,
0, 1, 3, 1, 1, 2, 1, 1, 3, 1, 0,
0, 0, 1, 3, 0, 0, 0, 3, 1, 0, 0,
];
let scale = width / FERRIS_WIDTH;
let top = (height - scale * FERRIS_HEIGHT) / 2;
let bottom = height - top - 1;
for y in 0..height {
for x in 0..width {
let rgba = if y < top || y >= bottom || x / scale >= FERRIS_WIDTH {
[0xdd, 0xba, 0xdc, 0xff]
} else {
let i = x / scale + (y - top) / scale * FERRIS_WIDTH;
match FERRIS[i as usize] {
0 => [0xdd, 0xba, 0xdc, 0xff],
1 => [0xf7, 0x4c, 0x00, 0xff],
2 => [0x00, 0x00, 0x00, 0xff],
3 => [0xa5, 0x2b, 0x00, 0xff],
_ => unreachable!(),
}
};
px.extend_from_slice(&rgba);
}
}
px
}
fn main() -> Result<(), Error> {
env_logger::init();
let event_loop = EventLoop::new();
let (window, surface, width, height) = {
let window = winit::window::Window::new(&event_loop).unwrap();
let scale = 3.0;
let width = SCREEN_WIDTH as f64 * scale;
let height = SCREEN_HEIGHT as f64 * scale;
let window = winit::window::WindowBuilder::new()
.with_inner_size(winit::dpi::LogicalSize::new(width, height))
.build(&event_loop)
.unwrap();
let surface = wgpu::Surface::create(&window);
let size = window.inner_size().to_physical(window.hidpi_factor());
@ -56,9 +23,8 @@ fn main() -> Result<(), Error> {
};
let surface_texture = SurfaceTexture::new(width, height, &surface);
let mut fb = Pixels::new(320, 240, surface_texture)?;
let ferris = scale_pixel_ferris(320, 240);
let mut fb = Pixels::new(224, 256, surface_texture)?;
let mut invaders = World::new();
event_loop.run(move |event, _, control_flow| match event {
event::Event::WindowEvent { event, .. } => match event {
@ -72,10 +38,13 @@ fn main() -> Result<(), Error> {
..
}
| event::WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
event::WindowEvent::RedrawRequested => fb.render(&ferris),
event::WindowEvent::RedrawRequested => fb.render(invaders.draw()),
_ => (),
},
event::Event::EventsCleared => window.request_redraw(),
event::Event::EventsCleared => {
invaders.update();
window.request_redraw();
}
_ => (),
});
}

View file

@ -0,0 +1,8 @@
[package]
name = "simple-invaders"
version = "0.1.0"
authors = ["Jay Oster <jay@kodewerx.org>"]
edition = "2018"
[dependencies]
pcx = "0.2"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

512
simple-invaders/src/lib.rs Normal file
View file

@ -0,0 +1,512 @@
use std::collections::HashMap;
use std::io::Cursor;
use std::rc::Rc;
type CachedSprite = (usize, usize, Rc<Vec<u8>>);
// Invader positioning
const START: Point = Point::new(24, 60);
const GRID: Point = Point::new(16, 16);
// Screen handling
pub const SCREEN_WIDTH: usize = 224;
pub const SCREEN_HEIGHT: usize = 256;
#[derive(Debug)]
pub struct World {
invaders: Invaders,
lasers: Vec<Laser>,
shields: Vec<Shield>,
player: Player,
cannons: Vec<Cannon>,
score: u32,
assets: Assets,
screen: Vec<u8>,
}
/// A list of assets loaded into memory.
#[derive(Debug)]
struct Assets {
// sounds: TODO
sprites: HashMap<String, CachedSprite>,
}
/// A tiny position vector
#[derive(Debug, Default, Eq, PartialEq)]
struct Point {
x: usize,
y: usize,
}
/// A collection of invaders.
#[derive(Debug)]
struct Invaders {
grid: Vec<Vec<Option<Invader>>>,
stepper: Stepper,
bounds: Bounds,
}
/// Everything you ever wanted to know about Invaders
#[derive(Debug)]
struct Invader {
sprite: SpriteRef,
pos: Point,
score: u32,
}
/// The stepper will linerly walk through the 2D vector of invaders, updating their state along the
/// way.
#[derive(Debug)]
struct Stepper {
row: usize,
col: usize,
}
/// Creates a boundary around the live invaders.
///
/// Used for collision detection and minor optimizations.
#[derive(Debug)]
struct Bounds {
left: usize,
right: usize,
bottom: usize,
}
/// The player entity.
#[derive(Debug)]
struct Player {
sprite: SpriteRef,
pos: Point,
}
/// The shield entity.
#[derive(Debug)]
struct Shield {
// Shield sprite is not referenced because we want to deform it when it gets shot
sprite: Sprite,
pos: Point,
}
/// The laser entity.
#[derive(Debug)]
struct Laser {
sprite: SpriteRef,
pos: Point,
}
/// The cannon entity.
#[derive(Debug)]
struct Cannon {
sprite: SpriteRef,
pos: Point,
}
/// Sprites can be drawn and animated.
#[derive(Debug)]
struct Sprite {
width: usize,
height: usize,
pixels: Vec<u8>,
frame: String,
}
/// SpriteRefs can be drawn and animated.
///
/// They reference their pixel data (instead of owning it).
#[derive(Debug)]
struct SpriteRef {
width: usize,
height: usize,
pixels: Rc<Vec<u8>>,
frame: String,
}
trait Sprites {
fn width(&self) -> usize;
fn height(&self) -> usize;
fn pixels(&self) -> &[u8];
fn frame(&self) -> &str;
}
impl World {
pub fn new() -> World {
// Load assets first
let assets = load_assets();
let invaders = Invaders {
grid: make_invader_grid(&assets),
stepper: Stepper::default(),
bounds: Bounds::default(),
};
let player = Player {
sprite: SpriteRef::new(&assets, "player1"),
pos: Point::new(80, 216),
};
let shields = (0..5)
.map(|i| Shield {
sprite: Sprite::new(&assets, "shield"),
pos: Point::new(i * 45 + 32, 192),
})
.collect();
// Create a screen with the correct size
let mut screen = Vec::new();
screen.resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default);
World {
invaders,
lasers: Vec::new(),
shields,
player,
cannons: Vec::new(),
score: 0,
assets,
screen,
}
}
pub fn update(&mut self) {
// Update the next invader
let row = self.invaders.stepper.row;
let col = self.invaders.stepper.col;
// Animate the invader
if let Some(invader) = &mut self.invaders.grid[row][col] {
invader.sprite.frame = match invader.sprite.frame.as_ref() {
"blipjoy1" => {
invader.sprite.pixels = self.assets.sprites.get("blipjoy2").unwrap().2.clone();
"blipjoy2".into()
}
"blipjoy2" => {
invader.sprite.pixels = self.assets.sprites.get("blipjoy1").unwrap().2.clone();
"blipjoy1".into()
}
"ferris1" => {
invader.sprite.pixels = self.assets.sprites.get("ferris2").unwrap().2.clone();
"ferris2".into()
}
"ferris2" => {
invader.sprite.pixels = self.assets.sprites.get("ferris1").unwrap().2.clone();
"ferris1".into()
}
_ => unreachable!(),
};
}
// Find the next invader
self.invaders.stepper.col += 1;
if self.invaders.stepper.col >= 11 {
self.invaders.stepper.col = 0;
if self.invaders.stepper.row == 0 {
self.invaders.stepper.row = 4;
} else {
self.invaders.stepper.row -= 1;
}
}
}
pub fn draw(&mut self) -> &[u8] {
// Clear the screen
self.clear();
// Draw the invaders
for row in &self.invaders.grid {
for col in row {
if let Some(invader) = col {
blit(&mut self.screen, &invader.pos, &invader.sprite);
}
}
}
&self.screen
}
fn clear(&mut self) {
for (i, byte) in self.screen.iter_mut().enumerate() {
*byte = if i % 4 == 3 { 255 } else { 0 };
}
}
}
impl Point {
const fn new(x: usize, y: usize) -> Point {
Point { x, y }
}
}
impl std::ops::Add for Point {
type Output = Self;
fn add(self, other: Self) -> Self {
Self::new(self.x + other.x, self.y + other.y)
}
}
impl std::ops::Mul for Point {
type Output = Self;
fn mul(self, other: Self) -> Self {
Self::new(self.x * other.x, self.y * other.y)
}
}
impl Default for Stepper {
fn default() -> Self {
Self { row: 4, col: 0 }
}
}
impl Default for Bounds {
fn default() -> Self {
Self {
left: START.x,
right: START.x + 11 * GRID.x,
bottom: START.y + 5 * GRID.y,
}
}
}
impl 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 Sprites for Sprite {
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn pixels(&self) -> &[u8] {
&self.pixels
}
fn frame(&self) -> &str {
&self.frame
}
}
impl 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 Sprites for SpriteRef {
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn pixels(&self) -> &[u8] {
&self.pixels
}
fn frame(&self) -> &str {
&self.frame
}
}
/// Load all static assets into an `Assets` structure
fn load_assets() -> Assets {
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")));
Assets { sprites }
}
/// Convert PCX data to raw pixels
fn load_pcx(pcx: &[u8]) -> CachedSprite {
let mut reader = pcx::Reader::new(Cursor::new(pcx)).unwrap();
let width = reader.width() as usize;
let height = reader.height() as usize;
let mut result = Vec::new();
if reader.is_paletted() {
// Read the raw pixel data
let mut buffer = Vec::new();
buffer.resize_with(width * height, Default::default);
for y in 0..height {
let a = y as usize * width;
let b = a + width;
reader.next_row_paletted(&mut buffer[a..b]).unwrap();
}
// Read the pallete
let mut palette = Vec::new();
let palette_length = reader.palette_length().unwrap() as usize;
palette.resize_with(palette_length * 3, Default::default);
reader.read_palette(&mut palette).unwrap();
// Copy to result with an alpha component
let pixels = buffer
.into_iter()
.map(|pal| {
let i = pal as usize * 3;
&palette[i..i + 3]
})
.flatten()
.cloned()
.collect::<Vec<u8>>();
result.extend_from_slice(&pixels);
} else {
for _ in 0..height {
// Read the raw pixel data
let mut buffer = Vec::new();
buffer.resize_with(width * 3, Default::default);
reader.next_row_rgb(&mut buffer[..]).unwrap();
// Copy to result with an alpha component
let pixels = buffer
.chunks(3)
.map(|rgb| {
let mut rgb = rgb.to_vec();
rgb.push(255);
rgb
})
.flatten()
.collect::<Vec<u8>>();
result.extend_from_slice(&pixels);
}
}
(width, height, Rc::new(result))
}
/// Create a grid of invaders.
fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
const BLIPJOY_OFFSET: Point = Point::new(3, 4);
const FERRIS_OFFSET: Point = Point::new(3, 5);
(0..1)
.map(|y| {
(0..11)
.map(|x| {
Some(Invader {
sprite: SpriteRef::new(assets, "blipjoy1"),
pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID,
score: 10,
})
})
.collect()
})
.chain((1..3).map(|y| {
(0..11)
.map(|x| {
Some(Invader {
sprite: SpriteRef::new(assets, "ferris1"),
pos: START + FERRIS_OFFSET + Point::new(x, y) * GRID,
score: 10,
})
})
.collect()
}))
.chain((3..5).map(|y| {
(0..11)
.map(|x| {
Some(Invader {
// TODO: Need a third invader
sprite: SpriteRef::new(assets, "blipjoy1"),
pos: START + BLIPJOY_OFFSET + Point::new(x, y) * GRID,
score: 10,
})
})
.collect()
}))
.collect()
}
fn blit<S>(screen: &mut [u8], dest: &Point, sprite: &S)
where
S: Sprites,
{
let pixels = sprite.pixels();
let width = sprite.width() * 4;
let mut s = 0;
for y in 0..sprite.height() {
let i = dest.x * 4 + dest.y * SCREEN_WIDTH * 4 + y * SCREEN_WIDTH * 4;
screen[i..i + width].copy_from_slice(&pixels[s..s + width]);
s += width;
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn test_pcx() {
let pixels = load_pcx(include_bytes!("assets/blipjoy1.pcx"));
let expected = vec![
0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0,
0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255,
255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0,
0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255,
255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255,
255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255,
255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255,
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255,
];
assert_eq!(pixels.0, 10, "Width differs");
assert_eq!(pixels.1, 8, "Height differs");
assert_eq!(Rc::try_unwrap(pixels.2).unwrap(), expected, "Pixels differ");
}
}