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" version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "gfx-backend-dx11" name = "gfx-backend-dx11"
version = "0.3.0" version = "0.3.0"
@ -492,6 +502,14 @@ dependencies = [
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.3.1" version = "0.3.1"
@ -615,6 +633,14 @@ name = "nodrop"
version = "0.1.13" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.8" version = "0.2.8"
@ -757,6 +783,14 @@ dependencies = [
"proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "range-alloc" name = "range-alloc"
version = "0.1.0" version = "0.1.0"
@ -919,7 +953,9 @@ dependencies = [
name = "simple-invaders" name = "simple-invaders"
version = "0.1.0" version = "0.1.0"
dependencies = [ 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)", "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]] [[package]]
@ -1071,6 +1107,11 @@ dependencies = [
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.50" 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 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 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 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-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-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>" "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 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 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.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 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 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" "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 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 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 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 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 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" "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 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 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 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 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 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" "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 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 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 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 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-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" "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; last = now;
// Update the game logic and request redraw // Update the game logic and request redraw
invaders.update(dt, &controls); invaders.update(&dt, &controls);
window.request_redraw(); window.request_redraw();
} }
_ => (), _ => (),

View file

@ -5,4 +5,6 @@ authors = ["Jay Oster <jay@kodewerx.org>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
line_drawing = "0.8"
pcx = "0.2" 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 //! this in practice. That said, the game is fully functional, and it should not be too difficult
//! to understand the code. //! to understand the code.
use rand_core::{OsRng, RngCore};
use std::env;
use std::time::Duration; 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, Animation, Frame, Sprite, SpriteRef}; use sprites::{blit, line, Animation, Frame, Sprite, SpriteRef};
mod controls; mod controls;
mod loader; mod loader;
@ -20,7 +22,7 @@ pub const SCREEN_WIDTH: usize = 224;
pub const SCREEN_HEIGHT: usize = 256; pub const SCREEN_HEIGHT: usize = 256;
// Invader positioning // Invader positioning
const START: Point = Point::new(24, 60); const START: Point = Point::new(24, 64);
const GRID: Point = Point::new(16, 16); const GRID: Point = Point::new(16, 16);
const ROWS: usize = 5; const ROWS: usize = 5;
const COLS: usize = 11; const COLS: usize = 11;
@ -31,29 +33,34 @@ pub struct World {
lasers: Vec<Laser>, lasers: Vec<Laser>,
shields: Vec<Shield>, shields: Vec<Shield>,
player: Player, player: Player,
bullets: Vec<Bullet>, bullet: Option<Bullet>,
score: u32, score: u32,
assets: Assets, assets: Assets,
screen: Vec<u8>, screen: Vec<u8>,
dt: Duration, dt: Duration,
gameover: bool,
random: OsRng,
debug: bool,
} }
/// A tiny position vector /// A tiny position vector.
#[derive(Debug, Default, Eq, PartialEq)] #[derive(Debug, Default, Eq, PartialEq)]
struct Point { struct Point {
x: usize, x: usize,
y: usize, y: usize,
} }
/// A formation of invaders. /// A fleet of invaders.
#[derive(Debug)] #[derive(Debug)]
struct Invaders { struct Invaders {
grid: Vec<Vec<Option<Invader>>>, grid: Vec<Vec<Option<Invader>>>,
stepper: Stepper, stepper: Point,
direction: Direction,
descend: bool,
bounds: Bounds, bounds: Bounds,
} }
/// Everything you ever wanted to know about Invaders /// Everything you ever wanted to know about Invaders.
#[derive(Debug)] #[derive(Debug)]
struct Invader { struct Invader {
sprite: SpriteRef, sprite: SpriteRef,
@ -61,22 +68,14 @@ struct Invader {
score: u32, 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. /// Creates a boundary around the live invaders.
/// ///
/// Used for collision detection and minor optimizations. /// Used for collision detection and minor optimizations.
#[derive(Debug)] #[derive(Debug)]
struct Bounds { struct Bounds {
left: usize, left_col: usize,
right: usize, right_col: usize,
bottom: usize, px: usize,
} }
/// The player entity. /// The player entity.
@ -84,7 +83,7 @@ struct Bounds {
struct Player { struct Player {
sprite: SpriteRef, sprite: SpriteRef,
pos: Point, pos: Point,
last_update: usize, dt: usize,
} }
/// The shield entity. /// The shield entity.
@ -100,6 +99,7 @@ struct Shield {
struct Laser { struct Laser {
sprite: SpriteRef, sprite: SpriteRef,
pos: Point, pos: Point,
dt: usize,
} }
/// The cannon entity. /// The cannon entity.
@ -107,6 +107,7 @@ struct Laser {
struct Bullet { struct Bullet {
sprite: SpriteRef, sprite: SpriteRef,
pos: Point, pos: Point,
dt: usize,
} }
impl World { impl World {
@ -117,15 +118,18 @@ impl World {
// Load assets first // Load assets first
let assets = load_assets(); let assets = load_assets();
// TODO: Create invaders one-at-a-time
let invaders = Invaders { let invaders = Invaders {
grid: make_invader_grid(&assets), grid: make_invader_grid(&assets),
stepper: Stepper::default(), stepper: Point::new(COLS - 1, 0),
direction: Direction::Right,
descend: false,
bounds: Bounds::default(), bounds: Bounds::default(),
}; };
let player = Player { let player = Player {
sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)),
pos: Point::new(80, 216), pos: Point::new(80, 216),
last_update: 0, dt: 0,
}; };
let shields = (0..4) let shields = (0..4)
.map(|i| Shield { .map(|i| Shield {
@ -138,16 +142,25 @@ impl World {
let mut screen = Vec::new(); let mut screen = Vec::new();
screen.resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default); 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 { World {
invaders, invaders,
lasers: Vec::new(), lasers: Vec::new(),
shields, shields,
player, player,
bullets: Vec::new(), bullet: None,
score: 0, score: 0,
assets, assets,
screen, screen,
dt: Duration::default(), dt: Duration::default(),
gameover: false,
random: OsRng,
debug,
} }
} }
@ -157,11 +170,16 @@ 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) {
if self.gameover {
// TODO: Add a game over screen
return;
}
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.dt += dt; self.dt += *dt;
// Step the invaders one by one // Step the invaders one by one
while self.dt >= one_frame { while self.dt >= one_frame {
@ -172,8 +190,35 @@ impl World {
// Handle player movement and animation // Handle player movement and animation
self.step_player(controls, dt); self.step_player(controls, dt);
// TODO: Handle lasers and bullets // Handle bullet movement
// Movements can be multiplied by the delta-time frame count, instead of looping 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. /// Draw the internal state to the screen.
@ -198,41 +243,131 @@ impl World {
// Draw the player // Draw the player
blit(&mut self.screen, &self.player.pos, &self.player.sprite); 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 &self.screen
} }
fn step_invaders(&mut self) { fn step_invaders(&mut self) {
// Find the next invader let (left, right) = self.invaders.get_bounds();
let mut invader = None; let (invader, is_leader) =
while let None = invader { next_invader(&mut self.invaders.grid, &mut self.invaders.stepper);
let (col, row) = self.invaders.stepper.incr();
invader = self.invaders.grid[row][col].as_mut();
}
let invader = invader.unwrap();
// 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 // Animate the invader
invader.sprite.step_frame(&self.assets); 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 { match controls.direction {
Direction::Left => { Direction::Left => {
if self.player.pos.x > 0 { if self.player.pos.x >= frames {
self.player.pos.x -= 1; self.player.pos.x -= frames;
self.player.sprite.animate(&self.assets, dt); self.player.sprite.animate(&self.assets, dt);
} }
} }
Direction::Right => { Direction::Right => {
if self.player.pos.x < 224 - 16 { if self.player.pos.x < SCREEN_WIDTH - 15 - frames {
self.player.pos.x += 1; self.player.pos.x += frames;
self.player.sprite.animate(&self.assets, dt); 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 /// Clear the screen
@ -271,27 +406,32 @@ impl std::ops::Mul for Point {
} }
} }
impl Stepper { impl Invaders {
fn incr(&mut self) -> (usize, usize) { fn get_bounds(&self) -> (usize, usize) {
self.col += 1; let width = (self.bounds.right_col - self.bounds.left_col + 1) * GRID.x;
if self.col >= COLS {
self.col = 0;
if self.row == 0 {
self.row = ROWS - 1;
} else {
self.row -= 1;
}
}
(self.col, self.row) let left = self.bounds.px;
let right = left + width;
(left, right)
} }
}
impl Default for Stepper { fn get_closest_invader(&self, mut col: usize) -> &Invader {
fn default() -> Self { let mut row = ROWS - 1;
Self { loop {
row: 0, if self.grid[row][col].is_some() {
col: COLS - 1, 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 { impl Default for Bounds {
fn default() -> Self { fn default() -> Self {
Self { Self {
left: START.x, left_col: 0,
right: START.x + COLS * GRID.x, right_col: COLS - 1,
bottom: START.y + ROWS * GRID.y, px: START.x,
} }
} }
} }
@ -350,3 +490,38 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
})) }))
.collect() .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(Shield1, load_pcx(include_bytes!("assets/shield.pcx")));
// sprites.insert(Laser1, load_pcx(include_bytes!("assets/laser1.pcx"))); sprites.insert(Bullet1, load_pcx(include_bytes!("assets/bullet1.pcx")));
// sprites.insert(Laser2, load_pcx(include_bytes!("assets/laser2.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 } Assets { sprites }
} }

View file

@ -1,8 +1,10 @@
use std::cmp::min;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use crate::loader::Assets; 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 // 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>>);
@ -23,8 +25,21 @@ pub(crate) enum Frame {
Player2, Player2,
Shield1, Shield1,
// Laser1,
// Laser2, Bullet1,
Bullet2,
Bullet3,
Bullet4,
Bullet5,
Laser1,
Laser2,
Laser3,
Laser4,
Laser5,
Laser6,
Laser7,
Laser8,
} }
/// Sprites can be drawn and procedurally generated. /// Sprites can be drawn and procedurally generated.
@ -59,7 +74,7 @@ pub(crate) trait Drawable {
} }
pub(crate) trait Animation { pub(crate) trait Animation {
fn animate(&mut self, assets: &Assets, dt: Duration); fn animate(&mut self, assets: &Assets, dt: &Duration);
} }
impl Sprite { impl Sprite {
@ -105,10 +120,22 @@ impl SpriteRef {
Player1 => (assets.get(&Player2).unwrap().2.clone(), Player2), Player1 => (assets.get(&Player2).unwrap().2.clone(), Player2),
Player2 => (assets.get(&Player1).unwrap().2.clone(), Player1), Player2 => (assets.get(&Player1).unwrap().2.clone(), Player1),
// This should not happen, but here we are! Bullet1 => (assets.get(&Bullet2).unwrap().2.clone(), Bullet2),
Shield1 => (assets.get(&Shield1).unwrap().2.clone(), Shield1), Bullet2 => (assets.get(&Bullet3).unwrap().2.clone(), Bullet3),
// Laser1 => (assets.get(&Laser2).unwrap().2.clone(), Laser2), Bullet3 => (assets.get(&Bullet4).unwrap().2.clone(), Bullet4),
// Laser2 => (assets.get(&Laser1).unwrap().2.clone(), Laser1), 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; self.pixels = pixels;
@ -145,11 +172,11 @@ impl Drawable for SpriteRef {
} }
impl Animation 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 { if self.duration.subsec_nanos() == 0 {
self.step_frame(assets); self.step_frame(assets);
} else { } else {
self.dt += dt; self.dt += *dt;
while self.dt >= self.duration { while self.dt >= self.duration {
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 where
S: Drawable, S: Drawable,
{ {
assert!(dest.x + sprite.width() <= SCREEN_WIDTH);
assert!(dest.y + sprite.height() <= SCREEN_HEIGHT);
let pixels = sprite.pixels(); let pixels = sprite.pixels();
let width = sprite.width() * 4; let width = sprite.width() * 4;
let mut s = 0; let mut s = 0;
for y in 0..sprite.height() { for y in 0..sprite.height() {
let i = dest.x * 4 + dest.y * SCREEN_WIDTH * 4 + y * SCREEN_WIDTH * 4; 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; 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);
}