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:
parent
31bc8c7614
commit
b6fcf803e7
46
Cargo.lock
generated
46
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -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"] }
|
||||
|
|
BIN
simple-invaders/src/assets/bullet1.pcx
Normal file
BIN
simple-invaders/src/assets/bullet1.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/bullet2.pcx
Normal file
BIN
simple-invaders/src/assets/bullet2.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/bullet3.pcx
Normal file
BIN
simple-invaders/src/assets/bullet3.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/bullet4.pcx
Normal file
BIN
simple-invaders/src/assets/bullet4.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/bullet5.pcx
Normal file
BIN
simple-invaders/src/assets/bullet5.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser1.pcx
Normal file
BIN
simple-invaders/src/assets/laser1.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser2.pcx
Normal file
BIN
simple-invaders/src/assets/laser2.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser3.pcx
Normal file
BIN
simple-invaders/src/assets/laser3.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser4.pcx
Normal file
BIN
simple-invaders/src/assets/laser4.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser5.pcx
Normal file
BIN
simple-invaders/src/assets/laser5.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser6.pcx
Normal file
BIN
simple-invaders/src/assets/laser6.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser7.pcx
Normal file
BIN
simple-invaders/src/assets/laser7.pcx
Normal file
Binary file not shown.
BIN
simple-invaders/src/assets/laser8.pcx
Normal file
BIN
simple-invaders/src/assets/laser8.pcx
Normal file
Binary file not shown.
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue