Invader AI (#6)

* WIP: Invader AI

- Adds debug mode for visualizing bounding boxes
- Adds rectangle and line drawing (for debug mode)
- Invaders move as a close approximation to the original game
- TODO: Demonstrates that the blit function needs to ignore black pixels (or "transparency")
- TODO: The invader movement code is really bad

* clippy and fmt

* Refactor Invader movement

* Support "transparency" in blit function

* Scale player movement to 60 pixels per second, regardless of frame rate.

* Add assertions in blit to prevent drawing out of bounds

* Add bullets, shoot with space

* Add lasers, and improve the bullet animation a little bit

* fmt
This commit is contained in:
Jay Oster 2019-10-12 14:26:47 -07:00 committed by GitHub
parent 31bc8c7614
commit b6fcf803e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 372 additions and 74 deletions

46
Cargo.lock generated
View file

@ -329,6 +329,16 @@ name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "getrandom"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gfx-backend-dx11"
version = "0.3.0"
@ -492,6 +502,14 @@ dependencies = [
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "line_drawing"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lock_api"
version = "0.3.1"
@ -615,6 +633,14 @@ name = "nodrop"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num-traits"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.8"
@ -757,6 +783,14 @@ dependencies = [
"proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "range-alloc"
version = "0.1.0"
@ -919,7 +953,9 @@ dependencies = [
name = "simple-invaders"
version = "0.1.0"
dependencies = [
"line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pcx 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1071,6 +1107,11 @@ dependencies = [
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "wasi"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wasm-bindgen"
version = "0.2.50"
@ -1391,6 +1432,7 @@ dependencies = [
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571"
"checksum gfx-backend-dx11 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "<none>"
"checksum gfx-backend-dx12 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "<none>"
"checksum gfx-backend-empty 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "<none>"
@ -1407,6 +1449,7 @@ dependencies = [
"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba"
"checksum libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753"
"checksum line_drawing 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5cc7ad3d82c845bdb5dde34ffdcc7a5fb4d2996e1e1ee0f19c33bc80e15196b9"
"checksum line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f81902e542483002b103c6424d23e765c2e5a65f732923299053a601bce50ab2"
"checksum lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8912e782533a93a167888781b836336a6ca5da6175c05944c86cf28c31104dc"
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
"checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
@ -1420,6 +1463,7 @@ dependencies = [
"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
"checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "31d20fd2b37e07cf5125be68357b588672e8cefe9a96f8c17a9d46053b3e590d"
"checksum objc_exception 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "098cd29a2fa3c230d3463ae069cecccc3fdfd64c0d2496ab5b96f82dab6a00dc"
@ -1435,6 +1479,7 @@ dependencies = [
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
"checksum range-alloc 0.1.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "<none>"
"checksum raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "af3d3b2e1053b3ff2171efc29a8bff3439ce6b2ce6a0432695134bc1c7ff8e87"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
@ -1472,6 +1517,7 @@ dependencies = [
"checksum vk-shader-macros-impl 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4941ca9c0867ee70ebd9680bdcd659ff53d2d789c65586500da2ff0aa813f7b7"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e"
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
"checksum wasm-bindgen 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "dcddca308b16cd93c2b67b126c688e5467e4ef2e28200dc7dfe4ae284f2faefc"
"checksum wasm-bindgen-backend 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "f805d9328b5fc7e5c6399960fd1889271b9b58ae17bdb2417472156cc9fafdd0"
"checksum wasm-bindgen-macro 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "3ff88201a482abfc63921621f6cb18eb1efd74f136b05e5841e7f8ca434539e9"

View file

@ -88,7 +88,7 @@ fn main() -> Result<(), Error> {
last = now;
// Update the game logic and request redraw
invaders.update(dt, &controls);
invaders.update(&dt, &controls);
window.request_redraw();
}
_ => (),

View file

@ -5,4 +5,6 @@ authors = ["Jay Oster <jay@kodewerx.org>"]
edition = "2018"
[dependencies]
line_drawing = "0.8"
pcx = "0.2"
rand_core = { version = "0.5", features = ["std"] }

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -4,11 +4,13 @@
//! this in practice. That said, the game is fully functional, and it should not be too difficult
//! to understand the code.
use rand_core::{OsRng, RngCore};
use std::env;
use std::time::Duration;
pub use controls::{Controls, Direction};
use loader::{load_assets, Assets};
use sprites::{blit, Animation, Frame, Sprite, SpriteRef};
use sprites::{blit, line, Animation, Frame, Sprite, SpriteRef};
mod controls;
mod loader;
@ -20,7 +22,7 @@ pub const SCREEN_WIDTH: usize = 224;
pub const SCREEN_HEIGHT: usize = 256;
// Invader positioning
const START: Point = Point::new(24, 60);
const START: Point = Point::new(24, 64);
const GRID: Point = Point::new(16, 16);
const ROWS: usize = 5;
const COLS: usize = 11;
@ -31,29 +33,34 @@ pub struct World {
lasers: Vec<Laser>,
shields: Vec<Shield>,
player: Player,
bullets: Vec<Bullet>,
bullet: Option<Bullet>,
score: u32,
assets: Assets,
screen: Vec<u8>,
dt: Duration,
gameover: bool,
random: OsRng,
debug: bool,
}
/// A tiny position vector
/// A tiny position vector.
#[derive(Debug, Default, Eq, PartialEq)]
struct Point {
x: usize,
y: usize,
}
/// A formation of invaders.
/// A fleet of invaders.
#[derive(Debug)]
struct Invaders {
grid: Vec<Vec<Option<Invader>>>,
stepper: Stepper,
stepper: Point,
direction: Direction,
descend: bool,
bounds: Bounds,
}
/// Everything you ever wanted to know about Invaders
/// Everything you ever wanted to know about Invaders.
#[derive(Debug)]
struct Invader {
sprite: SpriteRef,
@ -61,22 +68,14 @@ struct Invader {
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,
left_col: usize,
right_col: usize,
px: usize,
}
/// The player entity.
@ -84,7 +83,7 @@ struct Bounds {
struct Player {
sprite: SpriteRef,
pos: Point,
last_update: usize,
dt: usize,
}
/// The shield entity.
@ -100,6 +99,7 @@ struct Shield {
struct Laser {
sprite: SpriteRef,
pos: Point,
dt: usize,
}
/// The cannon entity.
@ -107,6 +107,7 @@ struct Laser {
struct Bullet {
sprite: SpriteRef,
pos: Point,
dt: usize,
}
impl World {
@ -117,15 +118,18 @@ impl World {
// Load assets first
let assets = load_assets();
// TODO: Create invaders one-at-a-time
let invaders = Invaders {
grid: make_invader_grid(&assets),
stepper: Stepper::default(),
stepper: Point::new(COLS - 1, 0),
direction: Direction::Right,
descend: false,
bounds: Bounds::default(),
};
let player = Player {
sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)),
pos: Point::new(80, 216),
last_update: 0,
dt: 0,
};
let shields = (0..4)
.map(|i| Shield {
@ -138,16 +142,25 @@ impl World {
let mut screen = Vec::new();
screen.resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default);
// Enable debug mode with `DEBUG=true` environment variable
let debug = env::var("DEBUG")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false);
World {
invaders,
lasers: Vec::new(),
shields,
player,
bullets: Vec::new(),
bullet: None,
score: 0,
assets,
screen,
dt: Duration::default(),
gameover: false,
random: OsRng,
debug,
}
}
@ -157,11 +170,16 @@ 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) {
if self.gameover {
// TODO: Add a game over screen
return;
}
let one_frame = Duration::new(0, 16_666_667);
// Advance the timer by the delta time
self.dt += dt;
self.dt += *dt;
// Step the invaders one by one
while self.dt >= one_frame {
@ -172,8 +190,35 @@ impl World {
// Handle player movement and animation
self.step_player(controls, dt);
// TODO: Handle lasers and bullets
// Movements can be multiplied by the delta-time frame count, instead of looping
// Handle bullet movement
if let Some(bullet) = &mut self.bullet {
let velocity = update_dt(&mut bullet.dt, dt) * 4;
if bullet.pos.y > velocity {
bullet.pos.y -= velocity;
bullet.sprite.animate(&self.assets, dt);
} else {
self.bullet = None;
}
}
// Handle laser movement
let mut destroy = Vec::new();
for (i, laser) in self.lasers.iter_mut().enumerate() {
let velocity = update_dt(&mut laser.dt, dt) * 2;
if laser.pos.y < self.player.pos.y {
laser.pos.y += velocity;
laser.sprite.animate(&self.assets, dt);
} else {
destroy.push(i);
}
}
// Destroy dead lasers
for i in destroy.iter().rev() {
self.lasers.remove(*i);
}
}
/// Draw the internal state to the screen.
@ -198,41 +243,131 @@ impl World {
// Draw the player
blit(&mut self.screen, &self.player.pos, &self.player.sprite);
// Draw the bullet
if let Some(bullet) = &self.bullet {
blit(&mut self.screen, &bullet.pos, &bullet.sprite);
}
// Draw lasers
for laser in self.lasers.iter() {
blit(&mut self.screen, &laser.pos, &laser.sprite);
}
if self.debug {
// Draw invaders bounding box
let (left, right) = self.invaders.get_bounds();
let red = [255, 0, 0, 255];
let p1 = Point::new(left, START.y);
let p2 = Point::new(left, self.player.pos.y);
line(&mut self.screen, &p1, &p2, red);
let p1 = Point::new(right, START.y);
let p2 = Point::new(right, self.player.pos.y);
line(&mut self.screen, &p1, &p2, red);
}
&self.screen
}
fn step_invaders(&mut self) {
// Find the next invader
let mut invader = None;
while let None = invader {
let (col, row) = self.invaders.stepper.incr();
invader = self.invaders.grid[row][col].as_mut();
}
let invader = invader.unwrap();
let (left, right) = self.invaders.get_bounds();
let (invader, is_leader) =
next_invader(&mut self.invaders.grid, &mut self.invaders.stepper);
// TODO: Move the invader
// The leader controls the fleet
if is_leader {
// The leader first commands the fleet to stop descending
self.invaders.descend = false;
// Then the leader redirects the fleet when they reach the boundaries
match self.invaders.direction {
Direction::Left => {
if left < 2 {
self.invaders.bounds.px += 2;
self.invaders.descend = true;
self.invaders.direction = Direction::Right;
} else {
self.invaders.bounds.px -= 2;
}
}
Direction::Right => {
if right > SCREEN_WIDTH - 2 {
self.invaders.bounds.px -= 2;
self.invaders.descend = true;
self.invaders.direction = Direction::Left;
} else {
self.invaders.bounds.px += 2;
}
}
_ => unreachable!(),
}
}
// Every invader in the fleet moves 2px per frame
match self.invaders.direction {
Direction::Left => invader.pos.x -= 2,
Direction::Right => invader.pos.x += 2,
_ => unreachable!(),
}
// And they descend 8px on command
if self.invaders.descend {
invader.pos.y += 8;
// One of the end scenarios
if invader.pos.y + 8 >= self.player.pos.y {
self.gameover = true;
}
}
// Animate the invader
invader.sprite.step_frame(&self.assets);
// They also shoot lasers at random with a 1:50 chance
let r = self.random.next_u32() as usize;
let chance = r % 50;
if self.lasers.len() < 3 && chance == 0 {
// Pick a random column to begin searching for an invader that can fire a laser
let col = r / 50 % COLS;
let invader = self.invaders.get_closest_invader(col);
let laser = Laser {
sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)),
pos: Point::new(invader.pos.x + 4, invader.pos.y + 10),
dt: 0,
};
self.lasers.push(laser);
}
}
fn step_player(&mut self, controls: &Controls, dt: Duration) {
fn step_player(&mut self, controls: &Controls, dt: &Duration) {
let frames = update_dt(&mut self.player.dt, dt);
match controls.direction {
Direction::Left => {
if self.player.pos.x > 0 {
self.player.pos.x -= 1;
if self.player.pos.x >= frames {
self.player.pos.x -= frames;
self.player.sprite.animate(&self.assets, dt);
}
}
Direction::Right => {
if self.player.pos.x < 224 - 16 {
self.player.pos.x += 1;
if self.player.pos.x < SCREEN_WIDTH - 15 - frames {
self.player.pos.x += frames;
self.player.sprite.animate(&self.assets, dt);
}
}
_ => (),
}
if controls.fire && self.bullet.is_none() {
self.bullet = Some(Bullet {
sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)),
pos: Point::new(self.player.pos.x + 7, self.player.pos.y),
dt: 0,
});
}
}
/// Clear the screen
@ -271,27 +406,32 @@ impl std::ops::Mul for Point {
}
}
impl Stepper {
fn incr(&mut self) -> (usize, usize) {
self.col += 1;
if self.col >= COLS {
self.col = 0;
if self.row == 0 {
self.row = ROWS - 1;
} else {
self.row -= 1;
}
}
impl Invaders {
fn get_bounds(&self) -> (usize, usize) {
let width = (self.bounds.right_col - self.bounds.left_col + 1) * GRID.x;
(self.col, self.row)
let left = self.bounds.px;
let right = left + width;
(left, right)
}
}
impl Default for Stepper {
fn default() -> Self {
Self {
row: 0,
col: COLS - 1,
fn get_closest_invader(&self, mut col: usize) -> &Invader {
let mut row = ROWS - 1;
loop {
if self.grid[row][col].is_some() {
return self.grid[row][col].as_ref().unwrap();
}
if row == 0 {
row = ROWS - 1;
col += 1;
if col == COLS {
col = 0;
}
} else {
row -= 1;
}
}
}
}
@ -299,9 +439,9 @@ impl Default for Stepper {
impl Default for Bounds {
fn default() -> Self {
Self {
left: START.x,
right: START.x + COLS * GRID.x,
bottom: START.y + ROWS * GRID.y,
left_col: 0,
right_col: COLS - 1,
px: START.x,
}
}
}
@ -350,3 +490,38 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
}))
.collect()
}
fn next_invader<'a>(
invaders: &'a mut Vec<Vec<Option<Invader>>>,
stepper: &mut Point,
) -> (&'a mut Invader, bool) {
let mut is_leader = false;
loop {
// Iterate through the entire grid
stepper.x += 1;
if stepper.x >= COLS {
stepper.x = 0;
if stepper.y == 0 {
stepper.y = ROWS - 1;
// After a full cycle, the next invader will be the leader
is_leader = true;
} else {
stepper.y -= 1;
}
}
if invaders[stepper.y][stepper.x].is_some() {
return (invaders[stepper.y][stepper.x].as_mut().unwrap(), is_leader);
}
}
}
fn update_dt(dest_dt: &mut usize, dt: &Duration) -> usize {
*dest_dt += dt.subsec_nanos() as usize;
let frames = *dest_dt / 16_666_667;
*dest_dt -= frames * 16_666_667;
frames
}

View file

@ -37,8 +37,20 @@ pub(crate) fn load_assets() -> Assets {
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")));
sprites.insert(Bullet1, load_pcx(include_bytes!("assets/bullet1.pcx")));
sprites.insert(Bullet2, load_pcx(include_bytes!("assets/bullet2.pcx")));
sprites.insert(Bullet3, load_pcx(include_bytes!("assets/bullet3.pcx")));
sprites.insert(Bullet4, load_pcx(include_bytes!("assets/bullet4.pcx")));
sprites.insert(Bullet5, load_pcx(include_bytes!("assets/bullet5.pcx")));
sprites.insert(Laser1, load_pcx(include_bytes!("assets/laser1.pcx")));
sprites.insert(Laser2, load_pcx(include_bytes!("assets/laser2.pcx")));
sprites.insert(Laser3, load_pcx(include_bytes!("assets/laser3.pcx")));
sprites.insert(Laser4, load_pcx(include_bytes!("assets/laser4.pcx")));
sprites.insert(Laser5, load_pcx(include_bytes!("assets/laser5.pcx")));
sprites.insert(Laser6, load_pcx(include_bytes!("assets/laser6.pcx")));
sprites.insert(Laser7, load_pcx(include_bytes!("assets/laser7.pcx")));
sprites.insert(Laser8, load_pcx(include_bytes!("assets/laser8.pcx")));
Assets { sprites }
}

View file

@ -1,8 +1,10 @@
use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use crate::loader::Assets;
use crate::{Point, SCREEN_WIDTH};
use crate::{Point, SCREEN_HEIGHT, SCREEN_WIDTH};
use line_drawing::Bresenham;
// This is the type stored in the `Assets` hash map
pub(crate) type CachedSprite = (usize, usize, Rc<Vec<u8>>);
@ -23,8 +25,21 @@ pub(crate) enum Frame {
Player2,
Shield1,
// Laser1,
// Laser2,
Bullet1,
Bullet2,
Bullet3,
Bullet4,
Bullet5,
Laser1,
Laser2,
Laser3,
Laser4,
Laser5,
Laser6,
Laser7,
Laser8,
}
/// Sprites can be drawn and procedurally generated.
@ -59,7 +74,7 @@ pub(crate) trait Drawable {
}
pub(crate) trait Animation {
fn animate(&mut self, assets: &Assets, dt: Duration);
fn animate(&mut self, assets: &Assets, dt: &Duration);
}
impl Sprite {
@ -105,10 +120,22 @@ impl SpriteRef {
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),
Bullet1 => (assets.get(&Bullet2).unwrap().2.clone(), Bullet2),
Bullet2 => (assets.get(&Bullet3).unwrap().2.clone(), Bullet3),
Bullet3 => (assets.get(&Bullet4).unwrap().2.clone(), Bullet4),
Bullet4 => (assets.get(&Bullet5).unwrap().2.clone(), Bullet5),
Bullet5 => (assets.get(&Bullet1).unwrap().2.clone(), Bullet1),
Laser1 => (assets.get(&Laser2).unwrap().2.clone(), Laser2),
Laser2 => (assets.get(&Laser3).unwrap().2.clone(), Laser3),
Laser3 => (assets.get(&Laser4).unwrap().2.clone(), Laser4),
Laser4 => (assets.get(&Laser5).unwrap().2.clone(), Laser5),
Laser5 => (assets.get(&Laser6).unwrap().2.clone(), Laser6),
Laser6 => (assets.get(&Laser7).unwrap().2.clone(), Laser7),
Laser7 => (assets.get(&Laser8).unwrap().2.clone(), Laser8),
Laser8 => (assets.get(&Laser1).unwrap().2.clone(), Laser1),
_ => unreachable!(),
};
self.pixels = pixels;
@ -145,11 +172,11 @@ impl Drawable for SpriteRef {
}
impl Animation for SpriteRef {
fn animate(&mut self, assets: &Assets, dt: Duration) {
fn animate(&mut self, assets: &Assets, dt: &Duration) {
if self.duration.subsec_nanos() == 0 {
self.step_frame(assets);
} else {
self.dt += dt;
self.dt += *dt;
while self.dt >= self.duration {
self.dt -= self.duration;
@ -164,13 +191,49 @@ pub(crate) fn blit<S>(screen: &mut [u8], dest: &Point, sprite: &S)
where
S: Drawable,
{
assert!(dest.x + sprite.width() <= SCREEN_WIDTH);
assert!(dest.y + sprite.height() <= SCREEN_HEIGHT);
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]);
// Merge pixels from sprite into screen
let zipped = screen[i..i + width].iter_mut().zip(&pixels[s..s + width]);
for (left, right) in zipped {
if *right > 0 {
*left = *right;
}
}
s += width;
}
}
/// Draw a line to the pixel buffer using Bresenham's algorithm.
pub(crate) fn line(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) {
let p1 = (p1.x as i64, p1.y as i64);
let p2 = (p2.x as i64, p2.y as i64);
for (x, y) in Bresenham::new(p1, p2) {
let x = min(x as usize, SCREEN_WIDTH - 1);
let y = min(y as usize, SCREEN_HEIGHT - 1);
let i = x * 4 + y * SCREEN_WIDTH * 4;
screen[i..i + 4].copy_from_slice(&color);
}
}
/// Draw a rectangle to the pixel buffer using two points in opposite corners.
pub(crate) fn _rect(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) {
let p3 = Point::new(p1.x, p2.y);
let p4 = Point::new(p2.x, p1.y);
line(screen, p1, &p3, color);
line(screen, &p3, p2, color);
line(screen, p2, &p4, color);
line(screen, &p4, p1, color);
}