Implement collision detection (#8)

* Implement collision detection

* Minor cleanups.

* Add laser/player collisions

* Add laser collision with bullets and fix fire button repeating

* Add basic shield collisions

* Refactor collision and debug

* Simplify collision and debug by not tracking laser indices

- We don't care about which laser collided, because they get destroyed immediately.

* Don't track laser indicies against bullets either

* DRY and docs

* Adjust the fleet bounding box as invaders are shot
This commit is contained in:
Jay Oster 2019-10-13 19:48:20 -07:00 committed by GitHub
parent a793787292
commit 076e4e519e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 607 additions and 89 deletions

View file

@ -1,3 +1,4 @@
use std::env;
use std::time::Instant; use std::time::Instant;
use pixels::{Error, Pixels, SurfaceTexture}; use pixels::{Error, Pixels, SurfaceTexture};
@ -9,6 +10,12 @@ fn main() -> Result<(), Error> {
env_logger::init(); env_logger::init();
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
// Enable debug mode with `DEBUG=true` environment variable
let debug = env::var("DEBUG")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false);
let (window, surface, width, height) = { let (window, surface, width, height) = {
let scale = 3.0; let scale = 3.0;
let width = SCREEN_WIDTH as f64 * scale; let width = SCREEN_WIDTH as f64 * scale;
@ -27,9 +34,13 @@ fn main() -> Result<(), Error> {
let surface_texture = SurfaceTexture::new(width, height, &surface); let surface_texture = SurfaceTexture::new(width, height, &surface);
let mut fb = Pixels::new(224, 256, surface_texture)?; let mut fb = Pixels::new(224, 256, surface_texture)?;
let mut invaders = World::new(); let mut invaders = World::new(debug);
let mut last = Instant::now(); let mut last = Instant::now();
let mut controls = Controls::default(); let mut controls = Controls::default();
let mut last_state = false;
let mut button_state = false;
let mut rising_edge = false;
event_loop.run(move |event, _, control_flow| match event { event_loop.run(move |event, _, control_flow| match event {
event::Event::WindowEvent { event, .. } => match event { event::Event::WindowEvent { event, .. } => match event {
@ -57,7 +68,7 @@ fn main() -> Result<(), Error> {
} => match virtual_code { } => match virtual_code {
event::VirtualKeyCode::Left => controls.direction = Direction::Left, event::VirtualKeyCode::Left => controls.direction = Direction::Left,
event::VirtualKeyCode::Right => controls.direction = Direction::Right, event::VirtualKeyCode::Right => controls.direction = Direction::Right,
event::VirtualKeyCode::Space => controls.fire = true, event::VirtualKeyCode::Space => button_state = true,
_ => (), _ => (),
}, },
@ -72,7 +83,7 @@ fn main() -> Result<(), Error> {
} => match virtual_code { } => match virtual_code {
event::VirtualKeyCode::Left => controls.direction = Direction::Still, event::VirtualKeyCode::Left => controls.direction = Direction::Still,
event::VirtualKeyCode::Right => controls.direction = Direction::Still, event::VirtualKeyCode::Right => controls.direction = Direction::Still,
event::VirtualKeyCode::Space => controls.fire = false, event::VirtualKeyCode::Space => button_state = false,
_ => (), _ => (),
}, },
@ -87,6 +98,13 @@ fn main() -> Result<(), Error> {
let dt = now.duration_since(last); let dt = now.duration_since(last);
last = now; last = now;
// Compute rising edge based on current and last button states
rising_edge = button_state && !last_state;
last_state = button_state;
// Fire button only uses rising edge
controls.fire = rising_edge;
// 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

@ -0,0 +1,190 @@
//! Collision detection primitives.
use crate::geo::{Point, Rect};
use crate::{Bullet, Invaders, Laser, Player, Shield, COLS, GRID, ROWS};
use std::collections::HashSet;
/// Store information about collisions (for debug mode).
#[derive(Debug, Default)]
pub(crate) struct Collision {
pub(crate) bullet_details: HashSet<BulletDetail>,
pub(crate) laser_details: HashSet<LaserDetail>,
}
/// Information regarding collisions between bullets and invaders, lasers, or shields.
#[derive(Debug, Eq, Hash, PartialEq)]
pub(crate) enum BulletDetail {
/// A grid position (col, row) for an invader.
Invader(usize, usize),
/// A shield index.
Shield(usize),
/// Collided with a laser.
Laser,
}
/// Information regarding collisions between lasers and shields or the player.
#[derive(Debug, Eq, Hash, PartialEq)]
pub(crate) enum LaserDetail {
/// A shield index.
Shield(usize),
/// Collided with the player.
Player,
}
impl Collision {
/// Clear the collision details.
pub(crate) fn clear(&mut self) {
self.bullet_details.clear();
self.laser_details.clear();
}
/// Handle collisions between bullets and invaders.
pub(crate) fn bullet_to_invader(
&mut self,
bullet: &mut Option<Bullet>,
invaders: &mut Invaders,
) -> bool {
// Broad phase collision detection
let (top, right, bottom, left) = invaders.get_bounds();
let invaders_rect = Rect::new(&Point::new(left, top), &Point::new(right, bottom));
let bullet_rect = {
let bullet = bullet.as_ref().unwrap();
Rect::from_drawable(&bullet.pos, &bullet.sprite)
};
if bullet_rect.intersects(&invaders_rect) {
// Narrow phase collision detection
let corners = [
(bullet_rect.p1.x, bullet_rect.p1.y),
(bullet_rect.p1.x, bullet_rect.p2.y),
(bullet_rect.p2.x, bullet_rect.p1.y),
(bullet_rect.p2.x, bullet_rect.p2.y),
];
for (x, y) in corners.iter() {
let col = (x - left) / GRID.x + invaders.bounds.left_col;
let row = (y - top) / GRID.y + invaders.bounds.top_row;
if col < COLS && row < ROWS && invaders.grid[row][col].is_some() {
let detail = BulletDetail::Invader(col, row);
self.bullet_details.insert(detail);
}
}
// If any collision candidate is a hit, kill the bullet and invader
for detail in self.bullet_details.iter() {
if let BulletDetail::Invader(x, y) = *detail {
let invader = invaders.grid[y][x].as_ref().unwrap();
let invader_rect = Rect::from_drawable(&invader.pos, &invader.sprite);
if bullet_rect.intersects(&invader_rect) {
// TODO: Explosion! Score!
invaders.grid[y][x] = None;
// Destroy bullet
*bullet = None;
return true;
}
}
}
}
false
}
/// Handle collisions between bullets and shields.
pub(crate) fn bullet_to_shield(&mut self, bullet: &mut Option<Bullet>, shields: &mut [Shield]) {
if bullet.is_some() {
let shield_rects = create_shield_rects(shields);
let bullet_rect = {
let bullet = bullet.as_ref().unwrap();
Rect::from_drawable(&bullet.pos, &bullet.sprite)
};
for (i, shield_rect) in shield_rects.iter().enumerate() {
// broad phase collision detection
if bullet_rect.intersects(&shield_rect) {
// TODO: Narrow phase (per-pixel) collision detection
// TODO: Break shield
// TODO: Explosion!
let detail = BulletDetail::Shield(i);
self.bullet_details.insert(detail);
// Destroy bullet
*bullet = None;
}
}
}
}
/// Handle collisions between lasers and the player.
pub(crate) fn laser_to_player(&mut self, laser: &Laser, player: &Player) -> bool {
let laser_rect = Rect::from_drawable(&laser.pos, &laser.sprite);
let player_rect = Rect::from_drawable(&player.pos, &player.sprite);
if laser_rect.intersects(&player_rect) {
self.laser_details.insert(LaserDetail::Player);
true
} else {
false
}
}
/// Handle collisions between lasers and bullets.
pub(crate) fn laser_to_bullet(&mut self, laser: &Laser, bullet: &mut Option<Bullet>) -> bool {
let mut destroy = false;
if bullet.is_some() {
let laser_rect = Rect::from_drawable(&laser.pos, &laser.sprite);
if let Some(bullet) = &bullet {
let bullet_rect = Rect::from_drawable(&bullet.pos, &bullet.sprite);
if bullet_rect.intersects(&laser_rect) {
// TODO: Explosion!
let detail = BulletDetail::Laser;
self.bullet_details.insert(detail);
// Destroy laser and bullet
destroy = true;
}
}
if destroy {
*bullet = None;
}
}
destroy
}
/// Handle collisions between lasers and shields.
pub(crate) fn laser_to_shield(&mut self, laser: &Laser, shields: &mut [Shield]) -> bool {
let laser_rect = Rect::from_drawable(&laser.pos, &laser.sprite);
let shield_rects = create_shield_rects(shields);
let mut destroy = false;
for (i, shield_rect) in shield_rects.iter().enumerate() {
// broad phase collision detection
if laser_rect.intersects(&shield_rect) {
// TODO: Narrow phase (per-pixel) collision detection
// TODO: Break shield
// TODO: Explosion!
let detail = LaserDetail::Shield(i);
self.laser_details.insert(detail);
// Destroy laser
destroy = true;
}
}
destroy
}
}
fn create_shield_rects(shields: &[Shield]) -> [Rect; 4] {
[
Rect::from_drawable(&shields[0].pos, &shields[0].sprite),
Rect::from_drawable(&shields[1].pos, &shields[1].sprite),
Rect::from_drawable(&shields[2].pos, &shields[2].sprite),
Rect::from_drawable(&shields[3].pos, &shields[3].sprite),
]
}

View file

@ -0,0 +1,106 @@
use crate::collision::{BulletDetail, Collision, LaserDetail};
use crate::geo::Point;
use crate::sprites::{rect, Drawable};
use crate::{Bullet, Invaders, Laser, Player, Shield, GRID};
// Colors
const RED: [u8; 4] = [255, 0, 0, 255];
const GREEN: [u8; 4] = [0, 255, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
const YELLOW: [u8; 4] = [255, 255, 0, 255];
/// Draw bounding boxes for the invader fleet and each invader.
pub(crate) fn draw_invaders(screen: &mut [u8], invaders: &Invaders, collision: &Collision) {
// Draw invaders bounding box
{
let (top, right, bottom, left) = invaders.get_bounds();
let p1 = Point::new(left, top);
let p2 = Point::new(right, bottom);
rect(screen, &p1, &p2, BLUE);
}
// Draw bounding boxes for each invader
for (y, row) in invaders.grid.iter().enumerate() {
for (x, col) in row.iter().enumerate() {
let detail = BulletDetail::Invader(x, y);
if let Some(invader) = col {
let p1 = invader.pos;
let p2 = p1 + Point::new(invader.sprite.width(), invader.sprite.height());
// Select color based on proximity to bullet
let color = if collision.bullet_details.contains(&detail) {
YELLOW
} else {
GREEN
};
rect(screen, &p1, &p2, color);
} else if collision.bullet_details.contains(&detail) {
let x = x - invaders.bounds.left_col;
let y = y - invaders.bounds.top_row;
let p1 = invaders.bounds.pos + Point::new(x, y) * GRID;
let p2 = p1 + GRID;
rect(screen, &p1, &p2, RED);
}
}
}
}
/// Draw bounding box for bullet.
pub(crate) fn draw_bullet(screen: &mut [u8], bullet: Option<&Bullet>) {
if let Some(bullet) = bullet {
let p1 = bullet.pos;
let p2 = p1 + Point::new(bullet.sprite.width(), bullet.sprite.height());
rect(screen, &p1, &p2, GREEN);
}
}
/// Draw bounding box for lasers.
pub(crate) fn draw_lasers(screen: &mut [u8], lasers: &[Laser]) {
for laser in lasers {
let p1 = laser.pos;
let p2 = p1 + Point::new(laser.sprite.width(), laser.sprite.height());
rect(screen, &p1, &p2, GREEN);
}
}
/// Draw bounding box for player.
pub(crate) fn draw_player(screen: &mut [u8], player: &Player, collision: &Collision) {
let p1 = player.pos;
let p2 = p1 + Point::new(player.sprite.width(), player.sprite.height());
// Select color based on collisions
let detail = LaserDetail::Player;
let color = if collision.laser_details.contains(&detail) {
RED
} else {
GREEN
};
rect(screen, &p1, &p2, color);
}
/// Draw bounding boxes for shields.
pub(crate) fn draw_shields(screen: &mut [u8], shields: &[Shield], collision: &Collision) {
for (i, shield) in shields.iter().enumerate() {
let p1 = shield.pos;
let p2 = p1 + Point::new(shield.sprite.width(), shield.sprite.height());
// Select color based on collisions
let laser_detail = LaserDetail::Shield(i);
let bullet_detail = BulletDetail::Shield(i);
let color = if collision.laser_details.contains(&laser_detail)
|| collision.bullet_details.contains(&bullet_detail)
{
RED
} else {
GREEN
};
rect(screen, &p1, &p2, color);
}
}

127
simple-invaders/src/geo.rs Normal file
View file

@ -0,0 +1,127 @@
//! Simple geometry primitives.
use crate::sprites::Drawable;
/// A tiny position vector.
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct Point {
pub(crate) x: usize,
pub(crate) y: usize,
}
/// A tiny rectangle based on two absolute `Point`s.
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct Rect {
pub(crate) p1: Point,
pub(crate) p2: Point,
}
impl Point {
/// Create a new point.
pub(crate) 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 Rect {
/// Create a rectangle from two `Point`s.
pub(crate) fn new(p1: &Point, p2: &Point) -> Rect {
let p1 = *p1;
let p2 = *p2;
Rect { p1, p2 }
}
/// Create a rectangle from a `Point` and a `Drawable`.
pub(crate) fn from_drawable<D>(pos: &Point, drawable: &D) -> Rect
where
D: Drawable,
{
let p1 = *pos;
let p2 = p1 + Point::new(drawable.width(), drawable.height());
Rect { p1, p2 }
}
/// Test for intersections between two rectangles.
///
/// Rectangles intersect when the geometry of either overlaps.
pub(crate) fn intersects(&self, other: &Rect) -> bool {
let (top1, right1, bottom1, left1) = self.get_bounds();
let (top2, right2, bottom2, left2) = other.get_bounds();
bottom1 > top2 && bottom2 > top1 && right1 > left2 && right2 > left1
}
/// Compute the bounding box for this rectangle.
///
/// # Returns
///
/// Tuple of `(top, right, bottom, left)`, e.g. in CSS clockwise order.
fn get_bounds(&self) -> (usize, usize, usize, usize) {
(self.p1.y, self.p2.x, self.p2.y, self.p1.x)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_intersect() {
let rect_size = Point::new(10, 10);
let r1 = Rect::new(&rect_size, &(rect_size + rect_size));
// Test intersection between equal-sized rectangles
for y in 0..3 {
for x in 0..3 {
let x = x * 5 + 5;
let y = y * 5 + 5;
let r2 = Rect::new(&Point::new(x, y), &(Point::new(x, y) + rect_size));
assert!(r1.intersects(&r2), "Should intersect");
assert!(r2.intersects(&r1), "Should intersect");
}
}
// Test non-intersections
for y in 0..3 {
for x in 0..3 {
if x == 1 && y == 1 {
continue;
}
let x = x * 10;
let y = y * 10;
let r2 = Rect::new(&Point::new(x, y), &(Point::new(x, y) + rect_size));
assert!(!r1.intersects(&r2), "Should not intersect");
assert!(!r2.intersects(&r1), "Should not intersect");
}
}
// Test intersection between different-sized rectangles
let r2 = Rect::new(&Point::new(0, 0), &Point::new(30, 30));
assert!(r1.intersects(&r2), "Should intersect");
assert!(r2.intersects(&r1), "Should intersect");
}
}

View file

@ -5,14 +5,18 @@
//! to understand the code. //! to understand the code.
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use std::env;
use std::time::Duration; use std::time::Duration;
pub use controls::{Controls, Direction}; pub use crate::controls::{Controls, Direction};
use loader::{load_assets, Assets}; use crate::geo::Point;
use sprites::{blit, line, Animation, Frame, Sprite, SpriteRef}; use crate::loader::{load_assets, Assets};
use crate::sprites::{blit, Animation, Drawable, Frame, Sprite, SpriteRef};
use collision::Collision;
mod collision;
mod controls; mod controls;
mod debug;
mod geo;
mod loader; mod loader;
mod sprites; mod sprites;
@ -27,6 +31,13 @@ const GRID: Point = Point::new(16, 16);
const ROWS: usize = 5; const ROWS: usize = 5;
const COLS: usize = 11; const COLS: usize = 11;
// Player positioning
const PLAYER_START: Point = Point::new(80, 216);
// Projectile positioning
const LASER_OFFSET: Point = Point::new(4, 10);
const BULLET_OFFSET: Point = Point::new(7, 0);
#[derive(Debug)] #[derive(Debug)]
pub struct World { pub struct World {
invaders: Invaders, invaders: Invaders,
@ -34,6 +45,7 @@ pub struct World {
shields: Vec<Shield>, shields: Vec<Shield>,
player: Player, player: Player,
bullet: Option<Bullet>, bullet: Option<Bullet>,
collision: Collision,
score: u32, score: u32,
assets: Assets, assets: Assets,
screen: Vec<u8>, screen: Vec<u8>,
@ -43,13 +55,6 @@ pub struct World {
debug: bool, debug: bool,
} }
/// A tiny position vector.
#[derive(Debug, Default, Eq, PartialEq)]
struct Point {
x: usize,
y: usize,
}
/// A fleet of invaders. /// A fleet of invaders.
#[derive(Debug)] #[derive(Debug)]
struct Invaders { struct Invaders {
@ -73,9 +78,11 @@ struct Invader {
/// Used for collision detection and minor optimizations. /// Used for collision detection and minor optimizations.
#[derive(Debug)] #[derive(Debug)]
struct Bounds { struct Bounds {
pos: Point,
left_col: usize, left_col: usize,
right_col: usize, right_col: usize,
px: usize, top_row: usize,
bottom_row: usize,
} }
/// The player entity. /// The player entity.
@ -112,7 +119,7 @@ struct Bullet {
impl World { impl World {
/// Create a new simple-invaders `World`. /// Create a new simple-invaders `World`.
pub fn new() -> World { pub fn new(debug: bool) -> World {
use Frame::*; use Frame::*;
// Load assets first // Load assets first
@ -126,40 +133,43 @@ impl World {
descend: false, descend: false,
bounds: Bounds::default(), bounds: Bounds::default(),
}; };
let player = Player { let lasers = Vec::new();
sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)),
pos: Point::new(80, 216),
dt: 0,
};
let shields = (0..4) let shields = (0..4)
.map(|i| Shield { .map(|i| Shield {
sprite: Sprite::new(&assets, Shield1), sprite: Sprite::new(&assets, Shield1),
pos: Point::new(i * 45 + 32, 192), pos: Point::new(i * 45 + 32, 192),
}) })
.collect(); .collect();
let player = Player {
sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)),
pos: PLAYER_START,
dt: 0,
};
let bullet = None;
let collision = Collision::default();
let score = 0;
// Create a screen with the correct size // Create a screen with the correct size
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 dt = Duration::default();
let debug = env::var("DEBUG") let gameover = false;
.unwrap_or_else(|_| "false".to_string()) let random = OsRng;
.parse()
.unwrap_or(false);
World { World {
invaders, invaders,
lasers: Vec::new(), lasers,
shields, shields,
player, player,
bullet: None, bullet,
score: 0, collision,
score,
assets, assets,
screen, screen,
dt: Duration::default(), dt,
gameover: false, gameover,
random: OsRng, random,
debug, debug,
} }
} }
@ -181,6 +191,9 @@ impl World {
// Advance the timer by the delta time // Advance the timer by the delta time
self.dt += *dt; self.dt += *dt;
// Clear the collision details
self.collision.clear();
// Step the invaders one by one // Step the invaders one by one
while self.dt >= one_frame { while self.dt >= one_frame {
self.dt -= one_frame; self.dt -= one_frame;
@ -190,13 +203,25 @@ impl World {
// Handle player movement and animation // Handle player movement and animation
self.step_player(controls, dt); self.step_player(controls, dt);
// Handle bullet movement
if let Some(bullet) = &mut self.bullet { if let Some(bullet) = &mut self.bullet {
// Handle bullet movement
let velocity = update_dt(&mut bullet.dt, dt) * 4; let velocity = update_dt(&mut bullet.dt, dt) * 4;
if bullet.pos.y > velocity { if bullet.pos.y > velocity {
bullet.pos.y -= velocity; bullet.pos.y -= velocity;
bullet.sprite.animate(&self.assets, dt); bullet.sprite.animate(&self.assets, dt);
// Handle collisions
if self
.collision
.bullet_to_invader(&mut self.bullet, &mut self.invaders)
{
// One of the end scenarios
self.gameover = self.invaders.shrink_bounds();
} else {
self.collision
.bullet_to_shield(&mut self.bullet, &mut self.shields);
}
} else { } else {
self.bullet = None; self.bullet = None;
} }
@ -210,18 +235,32 @@ impl World {
if laser.pos.y < self.player.pos.y { if laser.pos.y < self.player.pos.y {
laser.pos.y += velocity; laser.pos.y += velocity;
laser.sprite.animate(&self.assets, dt); laser.sprite.animate(&self.assets, dt);
// Handle collisions
if self.collision.laser_to_player(laser, &self.player) {
// One of the end scenarios
self.gameover = true;
destroy.push(i);
} else if self.collision.laser_to_bullet(laser, &mut self.bullet)
|| self.collision.laser_to_shield(laser, &mut self.shields)
{
destroy.push(i);
}
} else { } else {
destroy.push(i); destroy.push(i);
} }
} }
// Destroy dead lasers // Destroy dead lasers
for i in destroy.iter().rev() { for &i in destroy.iter().rev() {
self.lasers.remove(*i); self.lasers.remove(i);
} }
} }
/// Draw the internal state to the screen. /// Draw the internal state to the screen.
///
/// Calling this method more than once without an `update` call between is a no-op.
pub fn draw(&mut self) -> &[u8] { pub fn draw(&mut self) -> &[u8] {
// Clear the screen // Clear the screen
self.clear(); self.clear();
@ -253,25 +292,20 @@ impl World {
blit(&mut self.screen, &laser.pos, &laser.sprite); blit(&mut self.screen, &laser.pos, &laser.sprite);
} }
// Draw debug information
if self.debug { if self.debug {
// Draw invaders bounding box debug::draw_invaders(&mut self.screen, &self.invaders, &self.collision);
let (left, right) = self.invaders.get_bounds(); debug::draw_bullet(&mut self.screen, self.bullet.as_ref());
let red = [255, 0, 0, 255]; debug::draw_lasers(&mut self.screen, &self.lasers);
debug::draw_player(&mut self.screen, &self.player, &self.collision);
let p1 = Point::new(left, START.y); debug::draw_shields(&mut self.screen, &self.shields, &self.collision);
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) {
let (left, right) = self.invaders.get_bounds(); let (_, right, _, left) = self.invaders.get_bounds();
let (invader, is_leader) = let (invader, is_leader) =
next_invader(&mut self.invaders.grid, &mut self.invaders.stepper); next_invader(&mut self.invaders.grid, &mut self.invaders.stepper);
@ -284,20 +318,22 @@ impl World {
match self.invaders.direction { match self.invaders.direction {
Direction::Left => { Direction::Left => {
if left < 2 { if left < 2 {
self.invaders.bounds.px += 2; self.invaders.bounds.pos.x += 2;
self.invaders.bounds.pos.y += 8;
self.invaders.descend = true; self.invaders.descend = true;
self.invaders.direction = Direction::Right; self.invaders.direction = Direction::Right;
} else { } else {
self.invaders.bounds.px -= 2; self.invaders.bounds.pos.x -= 2;
} }
} }
Direction::Right => { Direction::Right => {
if right > SCREEN_WIDTH - 2 { if right > SCREEN_WIDTH - 2 {
self.invaders.bounds.px -= 2; self.invaders.bounds.pos.x -= 2;
self.invaders.bounds.pos.y += 8;
self.invaders.descend = true; self.invaders.descend = true;
self.invaders.direction = Direction::Left; self.invaders.direction = Direction::Left;
} else { } else {
self.invaders.bounds.px += 2; self.invaders.bounds.pos.x += 2;
} }
} }
_ => unreachable!(), _ => unreachable!(),
@ -334,7 +370,7 @@ impl World {
let laser = Laser { let laser = Laser {
sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)), sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)),
pos: Point::new(invader.pos.x + 4, invader.pos.y + 10), pos: invader.pos + LASER_OFFSET,
dt: 0, dt: 0,
}; };
self.lasers.push(laser); self.lasers.push(laser);
@ -343,17 +379,18 @@ impl World {
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); let frames = update_dt(&mut self.player.dt, dt);
let width = self.player.sprite.width();
match controls.direction { match controls.direction {
Direction::Left => { Direction::Left => {
if self.player.pos.x >= frames { if self.player.pos.x > width {
self.player.pos.x -= frames; 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 < SCREEN_WIDTH - 15 - frames { if self.player.pos.x < SCREEN_WIDTH - width * 2 {
self.player.pos.x += frames; self.player.pos.x += frames;
self.player.sprite.animate(&self.assets, dt); self.player.sprite.animate(&self.assets, dt);
} }
@ -364,7 +401,7 @@ impl World {
if controls.fire && self.bullet.is_none() { if controls.fire && self.bullet.is_none() {
self.bullet = Some(Bullet { self.bullet = Some(Bullet {
sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)), sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)),
pos: Point::new(self.player.pos.x + 7, self.player.pos.y), pos: self.player.pos + BULLET_OFFSET,
dt: 0, dt: 0,
}); });
} }
@ -380,40 +417,77 @@ impl World {
impl Default for World { impl Default for World {
fn default() -> Self { fn default() -> Self {
World::new() World::new(false)
}
}
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 Invaders { impl Invaders {
fn get_bounds(&self) -> (usize, usize) { /// Compute the bounding box for the Invader fleet.
///
/// # Returns
///
/// Tuple of `(top, right, bottom, left)`, e.g. in CSS clockwise order.
fn get_bounds(&self) -> (usize, usize, usize, usize) {
let width = (self.bounds.right_col - self.bounds.left_col + 1) * GRID.x; let width = (self.bounds.right_col - self.bounds.left_col + 1) * GRID.x;
let height = (self.bounds.bottom_row - self.bounds.top_row + 1) * GRID.y;
let left = self.bounds.px; let top = self.bounds.pos.y;
let bottom = top + height;
let left = self.bounds.pos.x;
let right = left + width; let right = left + width;
(left, right) (top, right, bottom, left)
}
/// Resize the bounds to fit the live invaders.
///
/// # Returns
///
/// `true` when all invaders have been destroyed.
fn shrink_bounds(&mut self) -> bool {
let mut top = ROWS;
let mut right = 0;
let mut bottom = 0;
let mut left = COLS;
// Scan through the entire grid
for (y, row) in self.grid.iter().enumerate() {
for (x, col) in row.iter().enumerate() {
if col.is_some() {
// Build a boundary box of invaders in the grid
if top > y {
top = y;
}
if bottom < y {
bottom = y;
}
if left > x {
left = x;
}
if right < x {
right = x;
}
}
}
}
if top > bottom || left > right {
// No more invaders left alive
return true;
}
// Adjust the bounding box position
self.bounds.pos.x += (left - self.bounds.left_col) * GRID.x;
self.bounds.pos.y += (top - self.bounds.top_row) * GRID.y;
// Adjust the bounding box columns and rows
self.bounds.left_col = left;
self.bounds.right_col = right;
self.bounds.top_row = top;
self.bounds.bottom_row = bottom;
// No more changes
false
} }
fn get_closest_invader(&self, mut col: usize) -> &Invader { fn get_closest_invader(&self, mut col: usize) -> &Invader {
@ -439,9 +513,11 @@ impl Invaders {
impl Default for Bounds { impl Default for Bounds {
fn default() -> Self { fn default() -> Self {
Self { Self {
pos: START,
left_col: 0, left_col: 0,
right_col: COLS - 1, right_col: COLS - 1,
px: START.x, top_row: 0,
bottom_row: ROWS - 1,
} }
} }
} }
@ -451,7 +527,7 @@ fn make_invader_grid(assets: &Assets) -> Vec<Vec<Option<Invader>>> {
use Frame::*; use Frame::*;
const BLIPJOY_OFFSET: Point = Point::new(3, 4); const BLIPJOY_OFFSET: Point = Point::new(3, 4);
const FERRIS_OFFSET: Point = Point::new(3, 5); const FERRIS_OFFSET: Point = Point::new(2, 5);
const CTHULHU_OFFSET: Point = Point::new(1, 3); const CTHULHU_OFFSET: Point = Point::new(1, 3);
(0..1) (0..1)

View file

@ -203,9 +203,9 @@ where
// Merge pixels from sprite into screen // Merge pixels from sprite into screen
let zipped = screen[i..i + width].iter_mut().zip(&pixels[s..s + width]); let zipped = screen[i..i + width].iter_mut().zip(&pixels[s..s + width]);
for (left, right) in zipped { for (left, &right) in zipped {
if *right > 0 { if right > 0 {
*left = *right; *left = right;
} }
} }
@ -228,12 +228,13 @@ pub(crate) fn line(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) {
} }
/// Draw a rectangle to the pixel buffer using two points in opposite corners. /// 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]) { pub(crate) fn rect(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) {
let p2 = Point::new(p2.x - 1, p2.y - 1);
let p3 = Point::new(p1.x, p2.y); let p3 = Point::new(p1.x, p2.y);
let p4 = Point::new(p2.x, p1.y); let p4 = Point::new(p2.x, p1.y);
line(screen, p1, &p3, color); line(screen, p1, &p3, color);
line(screen, &p3, p2, color); line(screen, &p3, &p2, color);
line(screen, p2, &p4, color); line(screen, &p2, &p4, color);
line(screen, &p4, p1, color); line(screen, &p4, p1, color);
} }