From c2c6709879953da2a1e9a41bb697d58f0e083db6 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Sun, 20 Oct 2019 16:59:30 -0700 Subject: [PATCH 1/6] WIP: Particles! --- Cargo.lock | 7 + simple-invaders/Cargo.toml | 1 + simple-invaders/src/collision.rs | 131 +++++++++-- simple-invaders/src/debug.rs | 14 +- simple-invaders/src/geo.rs | 382 +++++++++++++++++++++++++++++-- simple-invaders/src/lib.rs | 231 +++++++++++++------ simple-invaders/src/particles.rs | 194 ++++++++++++++++ simple-invaders/src/sprites.rs | 11 +- 8 files changed, 850 insertions(+), 121 deletions(-) create mode 100644 simple-invaders/src/particles.rs diff --git a/Cargo.lock b/Cargo.lock index 16c55ec5..67b72dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,11 @@ dependencies = [ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ash" version = "0.29.0" @@ -917,6 +922,7 @@ dependencies = [ name = "simple-invaders" version = "0.1.0" dependencies = [ + "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", @@ -1338,6 +1344,7 @@ dependencies = [ "checksum android_glue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" "checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" "checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" +"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum ash 0.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "003d1fb2eb12eb06d4a03dbe02eea67a9fac910fa97932ab9e3a75b96a1ea5e5" "checksum atom 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3c86699c3f02778ec07158376991c8f783dd1f2f95c579ffaf0738dc984b2fe2" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" diff --git a/simple-invaders/Cargo.toml b/simple-invaders/Cargo.toml index 4413b624..a767ed0f 100644 --- a/simple-invaders/Cargo.toml +++ b/simple-invaders/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Jay Oster "] edition = "2018" [dependencies] +arrayvec = "0.5" line_drawing = "0.8" pcx = "0.2" rand_core = { version = "0.5", features = ["std"] } diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index e72afcde..a3eacf67 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -1,7 +1,10 @@ //! Collision detection primitives. -use crate::geo::{Point, Rect}; +use crate::geo::{convex_hull, Point, Rect, Vec2D}; use crate::{Bullet, Invaders, Laser, Player, Shield, COLS, GRID, ROWS}; +use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; +use arrayvec::ArrayVec; +use line_drawing::Bresenham; use std::collections::HashSet; /// Store information about collisions (for debug mode). @@ -9,6 +12,7 @@ use std::collections::HashSet; pub(crate) struct Collision { pub(crate) bullet_details: HashSet, pub(crate) laser_details: HashSet, + pub(crate) pixel_mask: Vec, } /// Information regarding collisions between bullets and invaders, lasers, or shields. @@ -46,12 +50,12 @@ impl Collision { ) -> 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 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) + Rect::from_drawable(bullet.pos, &bullet.sprite) }; - if bullet_rect.intersects(&invaders_rect) { + if bullet_rect.intersects(invaders_rect) { // Narrow phase collision detection let corners = [ (bullet_rect.p1.x, bullet_rect.p1.y), @@ -74,8 +78,8 @@ impl Collision { 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) { + let invader_rect = Rect::from_drawable(invader.pos, &invader.sprite); + if bullet_rect.intersects(invader_rect) { // TODO: Explosion! Score! invaders.grid[y][x] = None; @@ -97,12 +101,12 @@ impl Collision { let shield_rects = create_shield_rects(shields); let bullet_rect = { let bullet = bullet.as_ref().unwrap(); - Rect::from_drawable(&bullet.pos, &bullet.sprite) + Rect::from_drawable(bullet.pos, &bullet.sprite) }; - for (i, shield_rect) in shield_rects.iter().enumerate() { + for (i, &shield_rect) in shield_rects.iter().enumerate() { // broad phase collision detection - if bullet_rect.intersects(&shield_rect) { + if bullet_rect.intersects(shield_rect) { // TODO: Narrow phase (per-pixel) collision detection // TODO: Break shield @@ -119,9 +123,9 @@ impl Collision { /// 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) { + 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 { @@ -133,11 +137,11 @@ impl Collision { pub(crate) fn laser_to_bullet(&mut self, laser: &Laser, bullet: &mut Option) -> bool { let mut destroy = false; if bullet.is_some() { - let laser_rect = Rect::from_drawable(&laser.pos, &laser.sprite); + 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) { + 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); @@ -157,13 +161,13 @@ impl Collision { /// 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 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() { + for (i, &shield_rect) in shield_rects.iter().enumerate() { // broad phase collision detection - if laser_rect.intersects(&shield_rect) { + if laser_rect.intersects(shield_rect) { // TODO: Narrow phase (per-pixel) collision detection // TODO: Break shield @@ -178,13 +182,96 @@ impl Collision { destroy } + + /// Trace a ray between the line segment formed by `start, end`, looking for collisions with + /// the collision mask. When a hit is detected, return the position of the hit and a new + /// velocity vector representing how the ray will proceed after bounding. + /// + /// In the case of no hits, returns `None`. + pub(crate) fn trace(&self, start: Vec2D, end: Vec2D, velocity: Vec2D) -> Option<(Vec2D, Vec2D)> { + let p1 = (start.x.round() as i32, start.y.round() as i32); + let p2 = (end.x.round() as i32, end.y.round() as i32); + let stride = SCREEN_WIDTH * 4; + + let mut hit = start; + + // Trace the particle's trajectory, checking each pixel in the collision mask along the way. + for (x, y) in Bresenham::new(p1, p2) { + let x = x as usize; + let y = y as usize; + let i = x * 4 + y * stride; + + // Only checking the red channel, that's all we really need + if x > 0 + && y > 0 + && x < SCREEN_WIDTH - 1 + && y < SCREEN_HEIGHT - 1 + && self.pixel_mask[i] > 0 + { + // A 3x3 pixel grid with four points surrounding each pixel center needs 24 points max. + let mut points = ArrayVec::<[_; 24]>::new(); + + // Create a list of vertices representing neighboring pixels. + for v in y - 1..=y + 1 { + for u in x - 1..=x + 1 { + let i = u * 4 + v * stride; + + // Only checking the red channel, again + if self.pixel_mask[i] > 0 { + let s = u as f32; + let t = v as f32; + + // Top and left sides of the pixel + points.push(Vec2D::new(s, t - 0.5)); + points.push(Vec2D::new(s - 0.5, t)); + + // Inspect neighrboring pixels to determine whether we need to also add the + // bottom and right sides of the pixel. This de-dupes overlapping points. + if u == x + 1 || self.pixel_mask[i + 4] == 0 { + // Right side + points.push(Vec2D::new(s + 0.5, t)); + } + if v == y + 1 || self.pixel_mask[i + stride] == 0 { + // Bottom side + points.push(Vec2D::new(s, t + 0.5)); + } + } + } + } + + // Compute the convex hull of the set of points. + let hull = convex_hull(&points); + + // TODO: For each line segment in the convex hull, compute the intersection between the + // line segment and the particle trajectory, keeping only the line segment that + // intersects closest to the particle's current position. In other words, find which + // slope the particle collides with. + // dbg!(x, y, hull); + + // TODO: Return updated velocity + // HAXX: For now, just invert the vector and dampen it. + let velocity = Vec2D::new(-velocity.x, -velocity.y) * Vec2D::new(0.8, 0.8); + + return Some((hit, velocity)); + } + + // Defer the hit location by 1 pixel. A fudge factor to prevent particles from getting + // stuck inside solids. + hit.x = x as f32; + hit.y = y as f32; + } + + None + } + + // TODO: Detect collisions between a `Drawable` and the internal pixel mask. } 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), + 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), ] } diff --git a/simple-invaders/src/debug.rs b/simple-invaders/src/debug.rs index 220f8378..88bc18a6 100644 --- a/simple-invaders/src/debug.rs +++ b/simple-invaders/src/debug.rs @@ -10,7 +10,12 @@ 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) { +pub(crate) fn draw_invaders(screen: &mut [u8], invaders: &Option, collision: &Collision) { + if invaders.is_none() { + return; + } + let invaders = invaders.as_ref().unwrap(); + // Draw invaders bounding box { let (top, right, bottom, left) = invaders.get_bounds(); @@ -69,7 +74,12 @@ pub(crate) fn draw_lasers(screen: &mut [u8], lasers: &[Laser]) { } /// Draw bounding box for player. -pub(crate) fn draw_player(screen: &mut [u8], player: &Player, collision: &Collision) { +pub(crate) fn draw_player(screen: &mut [u8], player: &Option, collision: &Collision) { + if player.is_none() { + return; + } + let player = player.as_ref().unwrap(); + let p1 = player.pos; let p2 = p1 + Point::new(player.sprite.width(), player.sprite.height()); diff --git a/simple-invaders/src/geo.rs b/simple-invaders/src/geo.rs index da157402..91c645f1 100644 --- a/simple-invaders/src/geo.rs +++ b/simple-invaders/src/geo.rs @@ -1,21 +1,36 @@ //! Simple geometry primitives. use crate::sprites::Drawable; +use arrayvec::ArrayVec; -/// A tiny position vector. +/// A tiny absolute 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. +/// A tiny absolute rectangle based on two `Point`s. #[derive(Copy, Clone, Debug, Default)] pub(crate) struct Rect { pub(crate) p1: Point, pub(crate) p2: Point, } +/// A tiny 2D vector with floating point coordinates. +#[derive(Copy, Clone, Debug, Default)] +pub(crate) struct Vec2D { + pub(crate) x: f32, + pub(crate) y: f32, +} + +/// A tiny 2D line segment based on `Vec2D`s. +#[derive(Copy, Clone, Debug, Default)] +pub(crate) struct LineSegment { + pub(crate) p: Vec2D, + pub(crate) q: Vec2D, +} + impl Point { /// Create a new point. pub(crate) const fn new(x: usize, y: usize) -> Point { @@ -39,21 +54,24 @@ impl std::ops::Mul for Point { } } +/// Saturates to 0.0 +impl From for Point { + fn from(v: Vec2D) -> Point { + Point::new(v.x.round().max(0.0) as usize, v.y.round().max(0.0) as usize) + } +} + impl Rect { /// Create a rectangle from two `Point`s. - pub(crate) fn new(p1: &Point, p2: &Point) -> Rect { - let p1 = *p1; - let p2 = *p2; - + pub(crate) fn new(p1: Point, p2: Point) -> Rect { Rect { p1, p2 } } /// Create a rectangle from a `Point` and a `Drawable`. - pub(crate) fn from_drawable(pos: &Point, drawable: &D) -> Rect + pub(crate) fn from_drawable(p1: Point, drawable: &D) -> Rect where D: Drawable, { - let p1 = *pos; let p2 = p1 + Point::new(drawable.width(), drawable.height()); Rect { p1, p2 } @@ -62,7 +80,7 @@ impl Rect { /// Test for intersections between two rectangles. /// /// Rectangles intersect when the geometry of either overlaps. - pub(crate) fn intersects(&self, other: &Rect) -> bool { + pub(crate) fn intersects(&self, other: Rect) -> bool { let (top1, right1, bottom1, left1) = self.get_bounds(); let (top2, right2, bottom2, left2) = other.get_bounds(); @@ -79,14 +97,167 @@ impl Rect { } } +impl Vec2D { + /// Create a 2D vector. + pub(crate) fn new(x: f32, y: f32) -> Vec2D { + Vec2D { x, y } + } + + /// Compute the squared length. + pub(crate) fn len_sq(&self) -> f32 { + self.x.powi(2) + self.y.powi(2) + } + + /// Compute the length. + pub(crate) fn len(&self) -> f32 { + self.len_sq().sqrt() + } + + /// Scale by a scalar. + pub(crate) fn scale(&mut self, scale: f32) { + self.x *= scale; + self.y *= scale; + } + + /// Normalize to a unit vector. + /// + /// # Panics + /// + /// Asserts that length of `self != 0.0` + pub(crate) fn normalize(&mut self) { + let l = self.len(); + assert!(l.abs() > std::f32::EPSILON); + + self.x /= l; + self.y /= l; + } +} + +impl std::ops::Add for Vec2D { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self::new(self.x + other.x, self.y + other.y) + } +} + +impl std::ops::AddAssign for Vec2D { + fn add_assign(&mut self, other: Self) { + self.x += other.x; + self.y += other.y; + } +} + +impl std::ops::Sub for Vec2D { + type Output = Vec2D; + + fn sub(self, other: Vec2D) -> Vec2D { + Vec2D::new(self.x - other.x, self.y - other.y) + } +} + +impl std::ops::SubAssign for Vec2D { + fn sub_assign(&mut self, other: Vec2D) { + self.x -= other.x; + self.y -= other.y; + } +} + +impl std::ops::Mul for Vec2D { + type Output = Vec2D; + + fn mul(self, other: Vec2D) -> Vec2D { + Vec2D::new(self.x * other.x, self.y * other.y) + } +} + +impl std::ops::MulAssign for Vec2D { + fn mul_assign(&mut self, other: Vec2D) { + self.x *= other.x; + self.y *= other.y; + } +} + +impl From for Vec2D { + fn from(p: Point) -> Vec2D { + Vec2D::new(p.x as f32, p.y as f32) + } +} + +impl LineSegment { + /// Create a new `LineSegment` from two `Vec2D`s. + pub(crate) fn new(p: Vec2D, q: Vec2D) -> LineSegment { + LineSegment { p, q } + } + + /// Cross product between `self` and `other`. + pub(crate) fn cross(&self, other: LineSegment) -> f32 { + let v = self.q - self.p; + let w = other.q - other.p; + + (v.x * w.y) - (v.y * w.x) + } +} + +/// Find the convex hull around some set of `vertices` using the Jarvis march, aka gift wrapping +/// algorithm. +/// +/// The first item in the list must be on the convex hull. +/// +/// # Panics +/// +/// This function will panic if `vertices.len() < 2` or if more than 16 vertices are in the convex +/// hull. +pub(crate) fn convex_hull(vertices: &[Vec2D]) -> ArrayVec<[Vec2D; 16]> { + assert!(vertices.len() >= 2); + let mut a = (0, vertices[0]); + let mut b = (1, vertices[1]); + + let mut output = ArrayVec::new(); + output.push(a.1); + + loop { + for c in vertices.iter().enumerate() { + // Recompute the `ab` line on each iteration, since `b` may be updated. + let ab = LineSegment::new(a.1, b.1); + let ac = LineSegment::new(a.1, *c.1); + + // The sign of the cross product tells us which side of the `a, b` line that the `a, c` + // line will fall; Negative to the left, positive to the right. + let cross = ab.cross(ac); + + // To handle colinear points, we compare vector lengths; longest wins. + let ab_len = (b.1 - a.1).len_sq(); + let ac_len = (*c.1 - a.1).len_sq(); + + // Record the left-most pointing vector from point `a` or the longest vector when + // comparing the angle between colinear points. + if cross < 0.0 || (cross.abs() <= std::f32::EPSILON && ac_len > ab_len) { + b = (c.0, *c.1); + } + } + + // When we find the first vertex in the set, we have completed the convex hull. + if b.0 == 0 { + return output; + } else if output.is_full() { + panic!("Too many vertices in the convex hull."); + } + + // Update `a` and push the next vertex + a = b; + output.push(a.1); + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_rect_intersect() { + fn rect_intersect() { let rect_size = Point::new(10, 10); - let r1 = Rect::new(&rect_size, &(rect_size + rect_size)); + let r1 = Rect::new(rect_size, rect_size + rect_size); // Test intersection between equal-sized rectangles for y in 0..3 { @@ -94,10 +265,10 @@ mod tests { let x = x * 5 + 5; let y = y * 5 + 5; - let r2 = Rect::new(&Point::new(x, y), &(Point::new(x, y) + rect_size)); + 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"); + assert!(r1.intersects(r2), "Should intersect"); + assert!(r2.intersects(r1), "Should intersect"); } } @@ -111,17 +282,188 @@ mod tests { let x = x * 10; let y = y * 10; - let r2 = Rect::new(&Point::new(x, y), &(Point::new(x, y) + rect_size)); + 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"); + 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)); + let r2 = Rect::new(Point::new(0, 0), Point::new(30, 30)); + + assert!(r1.intersects(r2), "Should intersect"); + assert!(r2.intersects(r1), "Should intersect"); + } + + #[test] + fn vector2d_point_conversion() { + let v = Vec2D::new(-2.0, 4.0); + let p = Point::new(10, 10); + + // Point + Vec2D + let t = Point::from(Vec2D::from(p) + v); + assert!(t.x == 8); + assert!(t.y == 14); + + // Point - Vec2D + let t = Point::from(Vec2D::from(p) - v); + assert!(t.x == 12); + assert!(t.y == 6); + + // Point * Vec2D + let t = Point::from(Vec2D::from(p) * v); + assert!(t.x == 0); + assert!(t.y == 40); + } + + #[test] + fn convex_hull_clockwise() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + Vec2D::new(0.5, 0.5), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; - assert!(r1.intersects(&r2), "Should intersect"); - assert!(r2.intersects(&r1), "Should intersect"); + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } + } + + #[test] + fn convex_hull_counter_clockwise() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(0.0, 1.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(0.5, 0.5), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; + + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } + } + + #[test] + fn convex_hull_unsorted() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(0.5, 0.5), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + Vec2D::new(1.0, 0.0), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; + + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } + } + + #[test] + fn convex_hull_colinear_clockwise() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(0.5, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 0.5), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.5, 1.0), + Vec2D::new(0.0, 1.0), + Vec2D::new(0.0, 0.5), + Vec2D::new(0.5, 0.5), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; + + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } + } + + #[test] + fn convex_hull_colinear_counter_clockwise() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(0.0, 0.5), + Vec2D::new(0.0, 1.0), + Vec2D::new(0.5, 1.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(1.0, 0.5), + Vec2D::new(1.0, 0.0), + Vec2D::new(0.5, 0.0), + Vec2D::new(0.5, 0.5), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; + + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } + } + + #[test] + fn convex_hull_colinear_unsorted() { + let actual = convex_hull(&[ + Vec2D::new(0.0, 0.0), + Vec2D::new(0.5, 1.0), + Vec2D::new(0.0, 0.5), + Vec2D::new(1.0, 0.5), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.5, 0.5), + Vec2D::new(0.0, 1.0), + Vec2D::new(0.5, 0.0), + Vec2D::new(1.0, 0.0), + ]); + let expected = vec![ + Vec2D::new(0.0, 0.0), + Vec2D::new(1.0, 0.0), + Vec2D::new(1.0, 1.0), + Vec2D::new(0.0, 1.0), + ]; + + assert_eq!(actual.len(), expected.len()); + for (&expected, &actual) in expected.iter().zip(actual.iter()) { + assert!((actual.x - expected.x).abs() <= std::f32::EPSILON); + assert!((actual.y - expected.y).abs() <= std::f32::EPSILON); + } } } diff --git a/simple-invaders/src/lib.rs b/simple-invaders/src/lib.rs index bcb0afd5..9acf4d10 100644 --- a/simple-invaders/src/lib.rs +++ b/simple-invaders/src/lib.rs @@ -4,20 +4,23 @@ //! this in practice. That said, the game is fully functional, and it should not be too difficult //! to understand the code. +use arrayvec::ArrayVec; use rand_core::{OsRng, RngCore}; use std::time::Duration; +use crate::collision::Collision; pub use crate::controls::{Controls, Direction}; use crate::geo::Point; use crate::loader::{load_assets, Assets}; -use crate::sprites::{blit, Animation, Drawable, Frame, Sprite, SpriteRef}; -use collision::Collision; +use crate::particles::Particle; +use crate::sprites::{blit, line, Animation, Drawable, Frame, Sprite, SpriteRef}; mod collision; mod controls; mod debug; mod geo; mod loader; +mod particles; mod sprites; /// The screen width is constant (units are in pixels) @@ -40,18 +43,19 @@ const BULLET_OFFSET: Point = Point::new(7, 0); #[derive(Debug)] pub struct World { - invaders: Invaders, + invaders: Option, lasers: Vec, shields: Vec, - player: Player, + player: Option, bullet: Option, + particles: Vec, collision: Collision, score: u32, assets: Assets, screen: Vec, dt: Duration, gameover: bool, - random: OsRng, + prng: OsRng, debug: bool, } @@ -126,36 +130,46 @@ impl World { let assets = load_assets(); // TODO: Create invaders one-at-a-time - let invaders = Invaders { - grid: make_invader_grid(&assets), - stepper: Point::new(COLS - 1, 0), - direction: Direction::Right, - descend: false, - bounds: Bounds::default(), - }; - let lasers = Vec::new(); + // let invaders = Some(Invaders { + // grid: make_invader_grid(&assets), + // stepper: Point::new(COLS - 1, 0), + // direction: Direction::Right, + // descend: false, + // bounds: Bounds::default(), + // }); + let invaders = None; // DEBUG + + let lasers = Vec::with_capacity(3); let shields = (0..4) .map(|i| Shield { sprite: Sprite::new(&assets, Shield1), pos: Point::new(i * 45 + 32, 192), }) .collect(); - let player = Player { - sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), - pos: PLAYER_START, - dt: 0, - }; + + // let player = Some(Player { + // sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), + // pos: PLAYER_START, + // dt: 0, + // }); + let player = None; // DEBUG + let bullet = None; - let collision = Collision::default(); + let particles = Vec::with_capacity(1024); + let mut collision = Collision::default(); + collision.pixel_mask = Vec::with_capacity(SCREEN_WIDTH * SCREEN_HEIGHT * 4); + collision + .pixel_mask + .resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default); let score = 0; // Create a screen with the correct size - let mut screen = Vec::new(); + let mut screen = Vec::with_capacity(SCREEN_WIDTH * SCREEN_HEIGHT * 4); screen.resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default); let dt = Duration::default(); let gameover = false; - let random = OsRng; + let prng = OsRng; World { invaders, @@ -163,13 +177,14 @@ impl World { shields, player, bullet, + particles, collision, score, assets, screen, dt, gameover, - random, + prng, debug, } } @@ -181,11 +196,6 @@ impl World { /// * `dt`: The time delta since last update. /// * `controls`: The player inputs. 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 @@ -194,14 +204,54 @@ impl World { // Clear the collision details self.collision.clear(); - // Step the invaders one by one - while self.dt >= one_frame { - self.dt -= one_frame; - self.step_invaders(); + // Simulate particles + let destroy = particles::update(&mut self.particles, dt, &self.collision); + for &i in destroy.iter().rev() { + self.particles.remove(i); } - // Handle player movement and animation - self.step_player(controls, dt); + // DEBUG + if controls.fire { + // Create some particles + let particles = { + let pos = Point::new(SCREEN_WIDTH / 2 - 5, SCREEN_HEIGHT / 2 - 4); + let drawable = SpriteRef::new(&self.assets, Frame::Blipjoy1, Duration::default()); + let rect = geo::Rect::new( + Point::new(0, 0), + Point::new(drawable.width(), drawable.height()), + ); + let force = 4.0; + let center = geo::Vec2D::new(5.5, 6.5); + &mut particles::drawable_to_particles( + &mut self.prng, + &pos, + &drawable, + &rect, + force, + ¢er, + ) + }; + + // Add them to the world + for particle in particles.drain(..) { + self.particles.push(particle); + } + } + + if !self.gameover { + // Step the invaders one by one + if self.invaders.is_some() { + while self.dt >= one_frame { + self.dt -= one_frame; + self.step_invaders(); + } + } + + // Handle player movement and animation + if self.player.is_some() { + self.step_player(controls, dt); + } + } if let Some(bullet) = &mut self.bullet { // Handle bullet movement @@ -212,12 +262,16 @@ impl World { bullet.sprite.animate(&self.assets, dt); // Handle collisions - if self - .collision - .bullet_to_invader(&mut self.bullet, &mut self.invaders) + if self.invaders.is_some() + && self + .collision + .bullet_to_invader(&mut self.bullet, &mut self.invaders.as_mut().unwrap()) { // One of the end scenarios - self.gameover = self.invaders.shrink_bounds(); + if self.invaders.as_mut().unwrap().shrink_bounds() { + self.gameover = true; + self.invaders = None; + } } else { self.collision .bullet_to_shield(&mut self.bullet, &mut self.shields); @@ -228,18 +282,23 @@ impl World { } // Handle laser movement - let mut destroy = Vec::new(); + let mut destroy = ArrayVec::<[_; 3]>::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 { + if laser.pos.y < PLAYER_START.y { laser.pos.y += velocity; laser.sprite.animate(&self.assets, dt); // Handle collisions - if self.collision.laser_to_player(laser, &self.player) { + if self.player.is_some() + && self + .collision + .laser_to_player(laser, &self.player.as_ref().unwrap()) + { // One of the end scenarios self.gameover = true; + self.player = None; destroy.push(i); } else if self.collision.laser_to_bullet(laser, &mut self.bullet) @@ -265,11 +324,22 @@ impl World { // Clear the screen self.clear(); + // Draw the ground + { + // TODO: Draw cracks where lasers hit + let p1 = Point::new(0, PLAYER_START.y + 17); + let p2 = Point::new(SCREEN_WIDTH, PLAYER_START.y + 17); + + line(&mut self.screen, &p1, &p2, [255, 255, 255, 255]); + } + // Draw the invaders - for row in &self.invaders.grid { - for col in row { - if let Some(invader) = col { - blit(&mut self.screen, &invader.pos, &invader.sprite); + if self.invaders.is_some() { + for row in &self.invaders.as_ref().unwrap().grid { + for col in row { + if let Some(invader) = col { + blit(&mut self.screen, &invader.pos, &invader.sprite); + } } } } @@ -280,7 +350,10 @@ impl World { } // Draw the player - blit(&mut self.screen, &self.player.pos, &self.player.sprite); + if self.player.is_some() { + let player = self.player.as_ref().unwrap(); + blit(&mut self.screen, &player.pos, &player.sprite); + } // Draw the bullet if let Some(bullet) = &self.bullet { @@ -292,6 +365,12 @@ impl World { blit(&mut self.screen, &laser.pos, &laser.sprite); } + // Copy screen to the backbuffer for particle simulation + self.collision.pixel_mask.copy_from_slice(&self.screen); + + // Draw particles + particles::draw(&mut self.screen, &self.particles); + // Draw debug information if self.debug { debug::draw_invaders(&mut self.screen, &self.invaders, &self.collision); @@ -305,35 +384,35 @@ impl World { } fn step_invaders(&mut self) { - let (_, right, _, left) = self.invaders.get_bounds(); - let (invader, is_leader) = - next_invader(&mut self.invaders.grid, &mut self.invaders.stepper); + let invaders = self.invaders.as_mut().unwrap(); + let (_, right, _, left) = invaders.get_bounds(); + let (invader, is_leader) = next_invader(&mut invaders.grid, &mut invaders.stepper); // The leader controls the fleet if is_leader { // The leader first commands the fleet to stop descending - self.invaders.descend = false; + invaders.descend = false; // Then the leader redirects the fleet when they reach the boundaries - match self.invaders.direction { + match invaders.direction { Direction::Left => { if left < 2 { - self.invaders.bounds.pos.x += 2; - self.invaders.bounds.pos.y += 8; - self.invaders.descend = true; - self.invaders.direction = Direction::Right; + invaders.bounds.pos.x += 2; + invaders.bounds.pos.y += 8; + invaders.descend = true; + invaders.direction = Direction::Right; } else { - self.invaders.bounds.pos.x -= 2; + invaders.bounds.pos.x -= 2; } } Direction::Right => { if right > SCREEN_WIDTH - 2 { - self.invaders.bounds.pos.x -= 2; - self.invaders.bounds.pos.y += 8; - self.invaders.descend = true; - self.invaders.direction = Direction::Left; + invaders.bounds.pos.x -= 2; + invaders.bounds.pos.y += 8; + invaders.descend = true; + invaders.direction = Direction::Left; } else { - self.invaders.bounds.pos.x += 2; + invaders.bounds.pos.x += 2; } } _ => unreachable!(), @@ -341,19 +420,22 @@ impl World { } // Every invader in the fleet moves 2px per frame - match self.invaders.direction { + match 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 { + if invaders.descend { invader.pos.y += 8; // One of the end scenarios - if invader.pos.y + 8 >= self.player.pos.y { + if self.player.is_some() && invader.pos.y + 8 >= self.player.as_ref().unwrap().pos.y { self.gameover = true; + self.player = None; + + // TODO: Explosion! } } @@ -361,12 +443,12 @@ impl World { 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 r = self.prng.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 invader = invaders.get_closest_invader(col); let laser = Laser { sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)), @@ -378,21 +460,22 @@ impl World { } fn step_player(&mut self, controls: &Controls, dt: &Duration) { - let frames = update_dt(&mut self.player.dt, dt); - let width = self.player.sprite.width(); + let player = self.player.as_mut().unwrap(); + let frames = update_dt(&mut player.dt, dt); + let width = player.sprite.width(); match controls.direction { Direction::Left => { - if self.player.pos.x > width { - self.player.pos.x -= frames; - self.player.sprite.animate(&self.assets, dt); + if player.pos.x > width { + player.pos.x -= frames; + player.sprite.animate(&self.assets, dt); } } Direction::Right => { - if self.player.pos.x < SCREEN_WIDTH - width * 2 { - self.player.pos.x += frames; - self.player.sprite.animate(&self.assets, dt); + if player.pos.x < SCREEN_WIDTH - width * 2 { + player.pos.x += frames; + player.sprite.animate(&self.assets, dt); } } _ => (), @@ -401,7 +484,7 @@ impl World { if controls.fire && self.bullet.is_none() { self.bullet = Some(Bullet { sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)), - pos: self.player.pos + BULLET_OFFSET, + pos: player.pos + BULLET_OFFSET, dt: 0, }); } diff --git a/simple-invaders/src/particles.rs b/simple-invaders/src/particles.rs new file mode 100644 index 00000000..35c97997 --- /dev/null +++ b/simple-invaders/src/particles.rs @@ -0,0 +1,194 @@ +//! Particle simulation primitives. + +use crate::collision::Collision; +use crate::geo::{Point, Rect, Vec2D}; +use crate::sprites::Drawable; +use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; +use arrayvec::ArrayVec; +use rand_core::RngCore; +use std::time::Duration; + +/// Particles a 1x1 pixels that fly around all crazy like. +#[derive(Debug)] +pub(crate) struct Particle { + /// Position in the simulation, relative to upper-left corner. (For physics). + pos: Vec2D, + /// Absolute position (for drawing). + abs_pos: Point, + /// Direction and magnitude of motion. + velocity: Vec2D, + /// This is how long the particle remains alive at full brightness. It will countdown to zero + /// then start fading. + alive: Duration, + /// This is how long the particle remains visible while fading. It is an absolute duration, + /// not a countdown; see `dt`. + fade: Duration, + /// The delta time for the fade counter. When the particle is no longer "alive", this will + /// countdown to zero then the particle will die. + dt: Duration, +} + +/// Run particle simulation. +pub(crate) fn update( + particles: &mut [Particle], + dt: &Duration, + collision: &Collision, +) -> ArrayVec<[usize; 1024]> { + // TODO: + // - [x] Move particles. + // - [x] Apply gravity. + // - [x] Apply friction. + // - [ ] Detect collisions. + // - [ ] Apply collision reaction. + // - [ ] Particle decay. + // - [ ] Particle fade. + // - [ ] Particle death. + // - [ ] Scale by `dt`. + + let mut destroy = ArrayVec::new(); + + for (i, particle) in particles.iter_mut().enumerate() { + // Apply gravity + particle.velocity.y += 0.20; + + // Apply damping / friction + particle.velocity.x *= 0.985; + particle.velocity.y *= 0.985; + + // Apply velocity + let prediction = particle.pos + particle.velocity; + + // Ensure the position is within view. Destroys particles that are on the screen's border. + if prediction.x >= 1.0 + && prediction.x < (SCREEN_WIDTH - 1) as f32 + && prediction.y >= 1.0 + && prediction.y < (SCREEN_HEIGHT - 1) as f32 + { + // Apply collision detection and update particle state + // TODO: Apply collision detection multiple times until the particle stops bouncing + if let Some((pos, velocity)) = collision.trace(particle.pos, prediction, particle.velocity) { + // TODO + particle.pos = pos; + particle.velocity = velocity; + } else { + // Update position + particle.pos = prediction; + } + + // Convert to absolute position + particle.abs_pos = Point::from(particle.pos); + } else { + destroy.push(i); + } + } + + destroy +} + +/// Draw particles. +/// +/// # Panics +/// +/// Asserts that the particle's absolute position is within the screen. +pub(crate) fn draw(screen: &mut [u8], particles: &[Particle]) { + for particle in particles { + assert!(particle.abs_pos.x <= SCREEN_WIDTH); + assert!(particle.abs_pos.y <= SCREEN_HEIGHT); + + // Generate a shade of gray based on the particle lifetime and fade + let shade = if particle.alive > Duration::new(0, 0) { + 255 + } else { + let dt = particle.dt.subsec_nanos() as f32; + let fade = particle.fade.subsec_nanos() as f32; + + ((dt / fade).min(1.0) * 255.0) as u8 + }; + let color = [shade, shade, shade, 255]; + let i = particle.abs_pos.x * 4 + particle.abs_pos.y * SCREEN_WIDTH * 4; + + screen[i..i + 4].copy_from_slice(&color); + } +} + +/// Create particles from a `Drawable`. +/// +/// The particles are copied from a sprite, pixel-by-pixel. Forces are applied independently to +/// each particle, based on the `force` vector and size/position of the `other` rectangle. +/// +/// # Arguments +/// +/// * `prng` - A PRNG for providing some variance to emitted particles. +/// * `pos` - The screen position for the `Drawable`. +/// * `drawable` - The sprite that is being copied. +/// * `src` - A rectangle subset of the sprite to copy. +/// * `force` - An impulse force applied to all particles. +/// * `center` - Center of mass for impulse `force`. +/// +/// # Panics +/// +/// The `center` should be offset by 0.5 on each axis to prevent dividing by zero. This function +/// panics if `center.x.fract() == 0.0 || center.y.fract() == 0.0`. +/// +/// It also asserts that the `src` rectangle is fully contained within the `drawable`. +pub(crate) fn drawable_to_particles( + prng: &mut R, + pos: &Point, + drawable: &D, + src: &Rect, + force: f32, + center: &Vec2D, +) -> ArrayVec<[Particle; 1024]> +where + D: Drawable, + R: RngCore, +{ + let width = drawable.width(); + let height = drawable.height(); + assert!(src.p1.x < width && src.p2.x <= width && src.p1.x < src.p2.x); + assert!(src.p1.y < height && src.p2.y <= height && src.p1.y < src.p2.y); + assert!(center.x.fract().abs() > std::f32::EPSILON); + assert!(center.y.fract().abs() > std::f32::EPSILON); + + // The "extreme" is the longest side of the sprite multiplied by the square root of 2 with some + // fudge factor. In other words, it's the longest vector length expected between the center of + // mass and any other pixel. This value is used to approximate how much influence the force has + // on each particle. + let extreme = if width > height { width } else { height } as f32 * 1.28; + + let mut particles = ArrayVec::new(); + let pixels = drawable.pixels(); + + for y in src.p1.y..src.p2.y { + for x in src.p1.x..src.p2.x { + let i = x * 4 + y * width * 4; + + // Only checking the red channel, that's all we really need + if pixels[i] > 0 { + // Initialize velocity using force and center of mass + let mut velocity = Vec2D::new(x as f32, y as f32) - *center; + let scale = (extreme - velocity.len()) / extreme; + velocity.normalize(); + velocity.scale(scale * force); + + // Add some random variance [-0.5, 0.5) to the velocity + let rx = prng.next_u32() as f32 / std::u32::MAX as f32 - 0.5; + let ry = prng.next_u32() as f32 / std::u32::MAX as f32 - 0.5; + velocity += Vec2D::new(rx, ry); + + let abs_pos = *pos + Point::new(x, y); + + particles.push(Particle { + pos: Vec2D::from(abs_pos), + abs_pos, + velocity, + alive: Duration::new(2, 0), + fade: Duration::new(5, 0), + dt: Duration::default(), + }); + } + } + } + + particles +} diff --git a/simple-invaders/src/sprites.rs b/simple-invaders/src/sprites.rs index 510995dd..dd9fe535 100644 --- a/simple-invaders/src/sprites.rs +++ b/simple-invaders/src/sprites.rs @@ -2,8 +2,9 @@ use std::cmp::min; use std::rc::Rc; use std::time::Duration; +use crate::geo::Point; use crate::loader::Assets; -use crate::{Point, SCREEN_HEIGHT, SCREEN_WIDTH}; +use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; use line_drawing::Bresenham; // This is the type stored in the `Assets` hash map @@ -187,6 +188,10 @@ impl Animation for SpriteRef { } /// Blit a drawable to the pixel buffer. +/// +/// # Panics +/// +/// Asserts that the drawable is fully contained within the screen. pub(crate) fn blit(screen: &mut [u8], dest: &Point, sprite: &S) where S: Drawable, @@ -215,8 +220,8 @@ where /// 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); + let p1 = (p1.x as i32, p1.y as i32); + let p2 = (p2.x as i32, p2.y as i32); for (x, y) in Bresenham::new(p1, p2) { let x = min(x as usize, SCREEN_WIDTH - 1); From 399349de5d7d5813f689e57e7690d6ea3113035c Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Sun, 20 Oct 2019 17:38:00 -0700 Subject: [PATCH 2/6] fmt and clippy --- simple-invaders/src/collision.rs | 19 ++++++++++++------- simple-invaders/src/particles.rs | 4 +++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index a3eacf67..3ed57107 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -188,7 +188,12 @@ impl Collision { /// velocity vector representing how the ray will proceed after bounding. /// /// In the case of no hits, returns `None`. - pub(crate) fn trace(&self, start: Vec2D, end: Vec2D, velocity: Vec2D) -> Option<(Vec2D, Vec2D)> { + pub(crate) fn trace( + &self, + start: Vec2D, + end: Vec2D, + velocity: Vec2D, + ) -> Option<(Vec2D, Vec2D)> { let p1 = (start.x.round() as i32, start.y.round() as i32); let p2 = (end.x.round() as i32, end.y.round() as i32); let stride = SCREEN_WIDTH * 4; @@ -199,14 +204,14 @@ impl Collision { for (x, y) in Bresenham::new(p1, p2) { let x = x as usize; let y = y as usize; - let i = x * 4 + y * stride; + let index = x * 4 + y * stride; // Only checking the red channel, that's all we really need if x > 0 && y > 0 && x < SCREEN_WIDTH - 1 && y < SCREEN_HEIGHT - 1 - && self.pixel_mask[i] > 0 + && self.pixel_mask[index] > 0 { // A 3x3 pixel grid with four points surrounding each pixel center needs 24 points max. let mut points = ArrayVec::<[_; 24]>::new(); @@ -214,10 +219,10 @@ impl Collision { // Create a list of vertices representing neighboring pixels. for v in y - 1..=y + 1 { for u in x - 1..=x + 1 { - let i = u * 4 + v * stride; + let index = u * 4 + v * stride; // Only checking the red channel, again - if self.pixel_mask[i] > 0 { + if self.pixel_mask[index] > 0 { let s = u as f32; let t = v as f32; @@ -227,11 +232,11 @@ impl Collision { // Inspect neighrboring pixels to determine whether we need to also add the // bottom and right sides of the pixel. This de-dupes overlapping points. - if u == x + 1 || self.pixel_mask[i + 4] == 0 { + if u == x + 1 || self.pixel_mask[index + 4] == 0 { // Right side points.push(Vec2D::new(s + 0.5, t)); } - if v == y + 1 || self.pixel_mask[i + stride] == 0 { + if v == y + 1 || self.pixel_mask[index + stride] == 0 { // Bottom side points.push(Vec2D::new(s, t + 0.5)); } diff --git a/simple-invaders/src/particles.rs b/simple-invaders/src/particles.rs index 35c97997..357cdeae 100644 --- a/simple-invaders/src/particles.rs +++ b/simple-invaders/src/particles.rs @@ -66,7 +66,9 @@ pub(crate) fn update( { // Apply collision detection and update particle state // TODO: Apply collision detection multiple times until the particle stops bouncing - if let Some((pos, velocity)) = collision.trace(particle.pos, prediction, particle.velocity) { + if let Some((pos, velocity)) = + collision.trace(particle.pos, prediction, particle.velocity) + { // TODO particle.pos = pos; particle.velocity = velocity; From ad71439234e88fb6006d51976ef02aeed363de2d Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Sun, 20 Oct 2019 21:17:16 -0700 Subject: [PATCH 3/6] Enable explosions on invaders, bullets, lasers, and player entities --- simple-invaders/src/collision.rs | 191 ++++++++++++++++++++++++++----- simple-invaders/src/geo.rs | 29 +++-- simple-invaders/src/lib.rs | 116 +++++++++---------- simple-invaders/src/particles.rs | 34 +++--- simple-invaders/src/sprites.rs | 11 +- 5 files changed, 262 insertions(+), 119 deletions(-) diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index 3ed57107..0fd3317b 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -1,11 +1,16 @@ //! Collision detection primitives. +use std::collections::HashSet; + use crate::geo::{convex_hull, Point, Rect, Vec2D}; -use crate::{Bullet, Invaders, Laser, Player, Shield, COLS, GRID, ROWS}; -use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; +use crate::particles::{drawable_to_particles, Particle}; +use crate::sprites::Drawable; +use crate::{ + Bullet, Invaders, Laser, Player, Shield, COLS, GRID, ROWS, SCREEN_HEIGHT, SCREEN_WIDTH, +}; use arrayvec::ArrayVec; use line_drawing::Bresenham; -use std::collections::HashSet; +use rand_core::RngCore; /// Store information about collisions (for debug mode). #[derive(Debug, Default)] @@ -43,11 +48,15 @@ impl Collision { } /// Handle collisions between bullets and invaders. - pub(crate) fn bullet_to_invader( + pub(crate) fn bullet_to_invader( &mut self, bullet: &mut Option, invaders: &mut Invaders, - ) -> bool { + prng: &mut R, + ) -> Option> + where + R: RngCore, + { // 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)); @@ -80,19 +89,55 @@ impl Collision { 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! + // TODO: Score! + + // Create a spectacular explosion! + let mut particles = { + let bullet = bullet.as_ref().unwrap(); + let force = 4.0; + let center = Vec2D::from(bullet.pos) - Vec2D::from(invader.pos) + + Vec2D::new(0.9, 1.9); + + drawable_to_particles( + prng, + invader.pos, + &invader.sprite, + invader.sprite.rect(), + force, + center, + ) + }; + let mut bullet_particles = { + let bullet = bullet.as_ref().unwrap(); + let force = 4.0; + let center = Vec2D::new(0.9, 4.1); + + drawable_to_particles( + prng, + bullet.pos, + &bullet.sprite, + bullet.sprite.rect(), + force, + center, + ) + }; + for particle in bullet_particles.drain(..) { + particles.push(particle); + } + + // Destroy invader invaders.grid[y][x] = None; // Destroy bullet *bullet = None; - return true; + return Some(particles); } } } } - false + None } /// Handle collisions between bullets and shields. @@ -122,14 +167,55 @@ impl Collision { } /// Handle collisions between lasers and the player. - pub(crate) fn laser_to_player(&mut self, laser: &Laser, player: &Player) -> bool { + pub(crate) fn laser_to_player( + &mut self, + laser: &Laser, + player: &Player, + prng: &mut R, + ) -> Option> + where + R: RngCore, + { 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 + + // Create a spectacular explosion! + let mut particles = { + let force = 8.0; + let center = + Vec2D::from(laser.pos) - Vec2D::from(player.pos) + Vec2D::new(2.5, 3.5); + + drawable_to_particles( + prng, + player.pos, + &player.sprite, + player.sprite.rect(), + force, + center, + ) + }; + let mut bullet_particles = { + let force = 8.0; + let center = Vec2D::new(2.5, -0.5); + + drawable_to_particles( + prng, + laser.pos, + &laser.sprite, + laser.sprite.rect(), + force, + center, + ) + }; + for particle in bullet_particles.drain(..) { + particles.push(particle); + } + + Some(particles) } else { - false + None } } @@ -188,6 +274,12 @@ impl Collision { /// velocity vector representing how the ray will proceed after bounding. /// /// In the case of no hits, returns `None`. + /// + /// # Arguments + /// + /// * `start` - Particle's current position. + /// * `end` - Particle's predicted position (must be `start + velocity`) + /// * `velocity` - Particle's vector of motion. pub(crate) fn trace( &self, start: Vec2D, @@ -200,7 +292,7 @@ impl Collision { let mut hit = start; - // Trace the particle's trajectory, checking each pixel in the collision mask along the way. + // Trace the particle's trajectory, checking each pixel in the collision mask along the way for (x, y) in Bresenham::new(p1, p2) { let x = x as usize; let y = y as usize; @@ -213,12 +305,14 @@ impl Collision { && y < SCREEN_HEIGHT - 1 && self.pixel_mask[index] > 0 { - // A 3x3 pixel grid with four points surrounding each pixel center needs 24 points max. - let mut points = ArrayVec::<[_; 24]>::new(); + // TODO: Split this into its own function! + + // A 5x5 grid with four points surrounding each pixel center needs 60 points max + let mut points = ArrayVec::<[_; 64]>::new(); // Create a list of vertices representing neighboring pixels. - for v in y - 1..=y + 1 { - for u in x - 1..=x + 1 { + for v in y - 2..=y + 2 { + for u in x - 2..=x + 2 { let index = u * 4 + v * stride; // Only checking the red channel, again @@ -230,13 +324,14 @@ impl Collision { points.push(Vec2D::new(s, t - 0.5)); points.push(Vec2D::new(s - 0.5, t)); - // Inspect neighrboring pixels to determine whether we need to also add the - // bottom and right sides of the pixel. This de-dupes overlapping points. - if u == x + 1 || self.pixel_mask[index + 4] == 0 { + // Inspect neighboring pixels to determine whether we need to also add + // the bottom and right sides of the pixel. This de-dupes overlapping + // points. + if u == x + 2 || self.pixel_mask[index + 4] == 0 { // Right side points.push(Vec2D::new(s + 0.5, t)); } - if v == y + 1 || self.pixel_mask[index + stride] == 0 { + if v == y + 2 || self.pixel_mask[index + stride] == 0 { // Bottom side points.push(Vec2D::new(s, t + 0.5)); } @@ -247,21 +342,63 @@ impl Collision { // Compute the convex hull of the set of points. let hull = convex_hull(&points); - // TODO: For each line segment in the convex hull, compute the intersection between the + // For each line segment in the convex hull, compute the intersection between the // line segment and the particle trajectory, keeping only the line segment that - // intersects closest to the particle's current position. In other words, find which - // slope the particle collides with. - // dbg!(x, y, hull); + // intersects closest to the particle's current position. In other words, find + // which slope the particle collides with. + let mut closest = end; + let mut slope = Vec2D::default(); + for (&p1, &p2) in hull.iter().zip(hull.iter().skip(1)) { + // The cross product between two line segments can tell use whether they + // intersect and where. This is adapted from "Intersection of two lines in + // three-space" by Ronald Goldman, published in Graphics Gems, page 304. + + // First we take the cross product between the velocity vector and the + // difference between the two points on the hull. + let magnitude = p2 - p1; + let cross = velocity.cross(magnitude); + + if cross.abs() < std::f32::EPSILON { + // Line segments are colinear or parallel + continue; + } + + // Interpolate the velocity vector toward the intersection + let t = (p1 - start).cross(magnitude) / cross; + let candidate = velocity.scale(t); + + // Record the closest intersecting line segment + if candidate.len_sq() < closest.len_sq() { + closest = candidate; + slope = magnitude; + } + } + + // We now have a slope along the particle's trajectory. All that is left to do is + // reflecting the velocity around the slope's angle. + + // Compute the angles of the velocity and slope vectors. + let theta = velocity.y.atan2(velocity.x); + let alpha = slope.y.atan2(slope.x); + + // Reflect theta around alpha. + // https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Reflections + let theta_prime = alpha * 2.0 - theta; - // TODO: Return updated velocity - // HAXX: For now, just invert the vector and dampen it. - let velocity = Vec2D::new(-velocity.x, -velocity.y) * Vec2D::new(0.8, 0.8); + // Update velocity and apply friction. + let magnitude = velocity.len(); + let velocity = + Vec2D::new(theta_prime.cos() * magnitude, theta_prime.sin() * magnitude); + let velocity = velocity * Vec2D::new(0.8, 0.8); return Some((hit, velocity)); } // Defer the hit location by 1 pixel. A fudge factor to prevent particles from getting // stuck inside solids. + // TODO: I would like to instead walk the ray from the hit point through the updated + // velocity until the particle stops colliding. This will prevent "unpredictable" + // movements in the collision mask from capturing particles. hit.x = x as f32; hit.y = y as f32; } diff --git a/simple-invaders/src/geo.rs b/simple-invaders/src/geo.rs index 91c645f1..05098a54 100644 --- a/simple-invaders/src/geo.rs +++ b/simple-invaders/src/geo.rs @@ -104,19 +104,21 @@ impl Vec2D { } /// Compute the squared length. - pub(crate) fn len_sq(&self) -> f32 { + pub(crate) fn len_sq(self) -> f32 { self.x.powi(2) + self.y.powi(2) } /// Compute the length. - pub(crate) fn len(&self) -> f32 { + pub(crate) fn len(self) -> f32 { self.len_sq().sqrt() } - /// Scale by a scalar. - pub(crate) fn scale(&mut self, scale: f32) { - self.x *= scale; - self.y *= scale; + /// Scale `self` by a scalar. + pub(crate) fn scale(self, scale: f32) -> Vec2D { + Vec2D { + x: self.x * scale, + y: self.y * scale, + } } /// Normalize to a unit vector. @@ -124,12 +126,19 @@ impl Vec2D { /// # Panics /// /// Asserts that length of `self != 0.0` - pub(crate) fn normalize(&mut self) { + pub(crate) fn normalize(self) -> Vec2D { let l = self.len(); assert!(l.abs() > std::f32::EPSILON); - self.x /= l; - self.y /= l; + Vec2D { + x: self.x / l, + y: self.y / l, + } + } + + /// Compute the cross product between `self` and `other`. + pub(crate) fn cross(self, other: Vec2D) -> f32 { + (self.x * other.y) - (self.y * other.x) } } @@ -195,7 +204,7 @@ impl LineSegment { let v = self.q - self.p; let w = other.q - other.p; - (v.x * w.y) - (v.y * w.x) + v.cross(w) } } diff --git a/simple-invaders/src/lib.rs b/simple-invaders/src/lib.rs index 9acf4d10..abd3e8e8 100644 --- a/simple-invaders/src/lib.rs +++ b/simple-invaders/src/lib.rs @@ -4,8 +4,8 @@ //! this in practice. That said, the game is fully functional, and it should not be too difficult //! to understand the code. -use arrayvec::ArrayVec; -use rand_core::{OsRng, RngCore}; +#![deny(clippy::all)] + use std::time::Duration; use crate::collision::Collision; @@ -14,6 +14,8 @@ use crate::geo::Point; use crate::loader::{load_assets, Assets}; use crate::particles::Particle; use crate::sprites::{blit, line, Animation, Drawable, Frame, Sprite, SpriteRef}; +use arrayvec::ArrayVec; +use rand_core::{OsRng, RngCore}; mod collision; mod controls; @@ -130,14 +132,13 @@ impl World { let assets = load_assets(); // TODO: Create invaders one-at-a-time - // let invaders = Some(Invaders { - // grid: make_invader_grid(&assets), - // stepper: Point::new(COLS - 1, 0), - // direction: Direction::Right, - // descend: false, - // bounds: Bounds::default(), - // }); - let invaders = None; // DEBUG + let invaders = Some(Invaders { + grid: make_invader_grid(&assets), + stepper: Point::new(COLS - 1, 0), + direction: Direction::Right, + descend: false, + bounds: Bounds::default(), + }); let lasers = Vec::with_capacity(3); let shields = (0..4) @@ -147,12 +148,11 @@ impl World { }) .collect(); - // let player = Some(Player { - // sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), - // pos: PLAYER_START, - // dt: 0, - // }); - let player = None; // DEBUG + let player = Some(Player { + sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), + pos: PLAYER_START, + dt: 0, + }); let bullet = None; let particles = Vec::with_capacity(1024); @@ -210,34 +210,6 @@ impl World { self.particles.remove(i); } - // DEBUG - if controls.fire { - // Create some particles - let particles = { - let pos = Point::new(SCREEN_WIDTH / 2 - 5, SCREEN_HEIGHT / 2 - 4); - let drawable = SpriteRef::new(&self.assets, Frame::Blipjoy1, Duration::default()); - let rect = geo::Rect::new( - Point::new(0, 0), - Point::new(drawable.width(), drawable.height()), - ); - let force = 4.0; - let center = geo::Vec2D::new(5.5, 6.5); - &mut particles::drawable_to_particles( - &mut self.prng, - &pos, - &drawable, - &rect, - force, - ¢er, - ) - }; - - // Add them to the world - for particle in particles.drain(..) { - self.particles.push(particle); - } - } - if !self.gameover { // Step the invaders one by one if self.invaders.is_some() { @@ -262,17 +234,25 @@ impl World { bullet.sprite.animate(&self.assets, dt); // Handle collisions - if self.invaders.is_some() - && self - .collision - .bullet_to_invader(&mut self.bullet, &mut self.invaders.as_mut().unwrap()) - { - // One of the end scenarios - if self.invaders.as_mut().unwrap().shrink_bounds() { - self.gameover = true; - self.invaders = None; + if self.invaders.is_some() { + if let Some(mut particles) = self.collision.bullet_to_invader( + &mut self.bullet, + &mut self.invaders.as_mut().unwrap(), + &mut self.prng, + ) { + // Add particles to the world + for particle in particles.drain(..) { + self.particles.push(particle); + } + + // One of the end scenarios + if self.invaders.as_mut().unwrap().shrink_bounds() { + self.gameover = true; + self.invaders = None; + } } - } else { + } + if self.bullet.is_some() { self.collision .bullet_to_shield(&mut self.bullet, &mut self.shields); } @@ -291,17 +271,25 @@ impl World { laser.sprite.animate(&self.assets, dt); // Handle collisions - if self.player.is_some() - && self - .collision - .laser_to_player(laser, &self.player.as_ref().unwrap()) - { - // One of the end scenarios - self.gameover = true; - self.player = None; + if self.player.is_some() { + if let Some(mut particles) = self.collision.laser_to_player( + laser, + &self.player.as_ref().unwrap(), + &mut self.prng, + ) { + // Add particles to the world + for particle in particles.drain(..) { + self.particles.push(particle); + } + + // One of the end scenarios + self.gameover = true; + self.player = None; - destroy.push(i); - } else if self.collision.laser_to_bullet(laser, &mut self.bullet) + destroy.push(i); + } + } + if self.collision.laser_to_bullet(laser, &mut self.bullet) || self.collision.laser_to_shield(laser, &mut self.shields) { destroy.push(i); diff --git a/simple-invaders/src/particles.rs b/simple-invaders/src/particles.rs index 357cdeae..570573d0 100644 --- a/simple-invaders/src/particles.rs +++ b/simple-invaders/src/particles.rs @@ -1,12 +1,13 @@ //! Particle simulation primitives. +use std::time::Duration; + use crate::collision::Collision; use crate::geo::{Point, Rect, Vec2D}; use crate::sprites::Drawable; use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; use arrayvec::ArrayVec; use rand_core::RngCore; -use std::time::Duration; /// Particles a 1x1 pixels that fly around all crazy like. #[derive(Debug)] @@ -31,15 +32,15 @@ pub(crate) struct Particle { /// Run particle simulation. pub(crate) fn update( particles: &mut [Particle], - dt: &Duration, + _dt: &Duration, collision: &Collision, ) -> ArrayVec<[usize; 1024]> { // TODO: // - [x] Move particles. // - [x] Apply gravity. // - [x] Apply friction. - // - [ ] Detect collisions. - // - [ ] Apply collision reaction. + // - [x] Detect collisions. + // - [x] Apply collision reaction. // - [ ] Particle decay. // - [ ] Particle fade. // - [ ] Particle death. @@ -52,17 +53,17 @@ pub(crate) fn update( particle.velocity.y += 0.20; // Apply damping / friction - particle.velocity.x *= 0.985; - particle.velocity.y *= 0.985; + particle.velocity = particle.velocity.scale(0.985); // Apply velocity let prediction = particle.pos + particle.velocity; // Ensure the position is within view. Destroys particles that are on the screen's border. - if prediction.x >= 1.0 - && prediction.x < (SCREEN_WIDTH - 1) as f32 - && prediction.y >= 1.0 - && prediction.y < (SCREEN_HEIGHT - 1) as f32 + // This prevents the 5x5 pixel "collision slope" check from wrapping around the screen. + if prediction.x >= 2.0 + && prediction.x < (SCREEN_WIDTH - 2) as f32 + && prediction.y >= 2.0 + && prediction.y < (SCREEN_HEIGHT - 2) as f32 { // Apply collision detection and update particle state // TODO: Apply collision detection multiple times until the particle stops bouncing @@ -135,11 +136,11 @@ pub(crate) fn draw(screen: &mut [u8], particles: &[Particle]) { /// It also asserts that the `src` rectangle is fully contained within the `drawable`. pub(crate) fn drawable_to_particles( prng: &mut R, - pos: &Point, + pos: Point, drawable: &D, - src: &Rect, + src: Rect, force: f32, - center: &Vec2D, + center: Vec2D, ) -> ArrayVec<[Particle; 1024]> where D: Drawable, @@ -168,17 +169,16 @@ where // Only checking the red channel, that's all we really need if pixels[i] > 0 { // Initialize velocity using force and center of mass - let mut velocity = Vec2D::new(x as f32, y as f32) - *center; + let velocity = Vec2D::new(x as f32, y as f32) - center; let scale = (extreme - velocity.len()) / extreme; - velocity.normalize(); - velocity.scale(scale * force); + let mut velocity = velocity.normalize().scale(scale * force); // Add some random variance [-0.5, 0.5) to the velocity let rx = prng.next_u32() as f32 / std::u32::MAX as f32 - 0.5; let ry = prng.next_u32() as f32 / std::u32::MAX as f32 - 0.5; velocity += Vec2D::new(rx, ry); - let abs_pos = *pos + Point::new(x, y); + let abs_pos = pos + Point::new(x, y); particles.push(Particle { pos: Vec2D::from(abs_pos), diff --git a/simple-invaders/src/sprites.rs b/simple-invaders/src/sprites.rs index dd9fe535..5f53f692 100644 --- a/simple-invaders/src/sprites.rs +++ b/simple-invaders/src/sprites.rs @@ -2,7 +2,7 @@ use std::cmp::min; use std::rc::Rc; use std::time::Duration; -use crate::geo::Point; +use crate::geo::{Point, Rect}; use crate::loader::Assets; use crate::{SCREEN_HEIGHT, SCREEN_WIDTH}; use line_drawing::Bresenham; @@ -71,6 +71,7 @@ pub(crate) struct SpriteRef { pub(crate) trait Drawable { fn width(&self) -> usize; fn height(&self) -> usize; + fn rect(&self) -> Rect; fn pixels(&self) -> &[u8]; } @@ -153,6 +154,10 @@ impl Drawable for Sprite { self.height } + fn rect(&self) -> Rect { + Rect::new(Point::default(), Point::new(self.width, self.height)) + } + fn pixels(&self) -> &[u8] { &self.pixels } @@ -167,6 +172,10 @@ impl Drawable for SpriteRef { self.height } + fn rect(&self) -> Rect { + Rect::new(Point::default(), Point::new(self.width, self.height)) + } + fn pixels(&self) -> &[u8] { &self.pixels } From fd8486c6b0f77eca39d6d29d29f78569324c0745 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Thu, 31 Oct 2019 21:12:12 -0700 Subject: [PATCH 4/6] laser-to-bullet explosions --- simple-invaders/src/collision.rs | 78 ++++++++++++++++++++++++-------- simple-invaders/src/lib.rs | 20 ++++++-- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index 0fd3317b..bf2decd7 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -196,7 +196,7 @@ impl Collision { center, ) }; - let mut bullet_particles = { + let mut laser_particles = { let force = 8.0; let center = Vec2D::new(2.5, -0.5); @@ -209,7 +209,7 @@ impl Collision { center, ) }; - for particle in bullet_particles.drain(..) { + for particle in laser_particles.drain(..) { particles.push(particle); } @@ -220,29 +220,69 @@ impl Collision { } /// Handle collisions between lasers and bullets. - pub(crate) fn laser_to_bullet(&mut self, laser: &Laser, bullet: &mut Option) -> bool { - let mut destroy = false; - if bullet.is_some() { + pub(crate) fn laser_to_bullet( + &mut self, + laser: &Laser, + bullet: &mut Option, + prng: &mut R, + ) -> Option> + where + R: RngCore, + { + let particles = if let Some(bullet) = bullet { 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; + let bullet_rect = Rect::from_drawable(bullet.pos, &bullet.sprite); + + if bullet_rect.intersects(laser_rect) { + let detail = BulletDetail::Laser; + self.bullet_details.insert(detail); + + // Create a spectacular explosion! + let mut particles = { + let force = 4.0; + let center = + Vec2D::from(bullet.pos) - Vec2D::from(laser.pos) + Vec2D::new(0.9, 1.9); + + drawable_to_particles( + prng, + laser.pos, + &laser.sprite, + laser.sprite.rect(), + force, + center, + ) + }; + let mut bullet_particles = { + let force = 4.0; + let center = + Vec2D::from(laser.pos) - Vec2D::from(bullet.pos) + Vec2D::new(2.5, 3.5); + + drawable_to_particles( + prng, + bullet.pos, + &bullet.sprite, + bullet.sprite.rect(), + force, + center, + ) + }; + for particle in bullet_particles.drain(..) { + particles.push(particle); } - } - if destroy { - *bullet = None; + Some(particles) + } else { + None } + } else { + None + }; + + if particles.is_some() { + *bullet = None; } - destroy + particles } /// Handle collisions between lasers and shields. diff --git a/simple-invaders/src/lib.rs b/simple-invaders/src/lib.rs index c2d48405..0dad84e7 100644 --- a/simple-invaders/src/lib.rs +++ b/simple-invaders/src/lib.rs @@ -280,14 +280,24 @@ impl World { self.gameover = true; self.player = None; + destroy.push(i); + } else if let Some(mut particles) = + self.collision + .laser_to_bullet(laser, &mut self.bullet, &mut self.prng) + { + // Laser and bullet obliterate each other + + // Add particles to the world + for particle in particles.drain(..) { + self.particles.push(particle); + } + + destroy.push(i); + } else if self.collision.laser_to_shield(laser, &mut self.shields) { + // TODO destroy.push(i); } } - if self.collision.laser_to_bullet(laser, &mut self.bullet) - || self.collision.laser_to_shield(laser, &mut self.shields) - { - destroy.push(i); - } } else { destroy.push(i); } From afcbb5533f6c237c93848ac7d0522063960b19f8 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Thu, 31 Oct 2019 21:42:07 -0700 Subject: [PATCH 5/6] Fix the particle velocities on laser-to-bullet explosions - Particles were being thrown away from the center of contact, which looks wrong. - This changes the velocities to ignore the difference in the Y dimension, allowing the particles of each object to be thrown "through" each other. --- simple-invaders/src/collision.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index bf2decd7..3a915060 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -240,8 +240,7 @@ impl Collision { // Create a spectacular explosion! let mut particles = { let force = 4.0; - let center = - Vec2D::from(bullet.pos) - Vec2D::from(laser.pos) + Vec2D::new(0.9, 1.9); + let center = Vec2D::new(bullet.pos.x as f32 - laser.pos.x as f32 + 2.5, -0.5); drawable_to_particles( prng, @@ -254,8 +253,7 @@ impl Collision { }; let mut bullet_particles = { let force = 4.0; - let center = - Vec2D::from(laser.pos) - Vec2D::from(bullet.pos) + Vec2D::new(2.5, 3.5); + let center = Vec2D::new(laser.pos.x as f32 - bullet.pos.x as f32 + 0.9, 4.5); drawable_to_particles( prng, From 1627fe28d798274f05e5fe8114c6a42885198b54 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Fri, 1 Nov 2019 21:29:07 -0700 Subject: [PATCH 6/6] Whoops, that is a bug! --- simple-invaders/src/particles.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple-invaders/src/particles.rs b/simple-invaders/src/particles.rs index 570573d0..f3725156 100644 --- a/simple-invaders/src/particles.rs +++ b/simple-invaders/src/particles.rs @@ -95,8 +95,8 @@ pub(crate) fn update( /// Asserts that the particle's absolute position is within the screen. pub(crate) fn draw(screen: &mut [u8], particles: &[Particle]) { for particle in particles { - assert!(particle.abs_pos.x <= SCREEN_WIDTH); - assert!(particle.abs_pos.y <= SCREEN_HEIGHT); + assert!(particle.abs_pos.x < SCREEN_WIDTH); + assert!(particle.abs_pos.y < SCREEN_HEIGHT); // Generate a shade of gray based on the particle lifetime and fade let shade = if particle.alive > Duration::new(0, 0) {