diff --git a/Cargo.lock b/Cargo.lock index 9a1b3c16..1903b5e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,6 +1046,7 @@ dependencies = [ name = "simple-invaders" version = "0.1.0" dependencies = [ + "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/simple-invaders/Cargo.toml b/simple-invaders/Cargo.toml index 0b3c63c1..0afd5ef9 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" randomize = "3.0" diff --git a/simple-invaders/src/collision.rs b/simple-invaders/src/collision.rs index e72afcde..698bee45 100644 --- a/simple-invaders/src/collision.rs +++ b/simple-invaders/src/collision.rs @@ -1,14 +1,23 @@ //! Collision detection primitives. -use crate::geo::{Point, Rect}; -use crate::{Bullet, Invaders, Laser, Player, Shield, COLS, GRID, ROWS}; use std::collections::HashSet; +use crate::geo::{convex_hull, Point, Rect, Vec2D}; +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 randomize::PCG32; + /// Store information about collisions (for debug mode). #[derive(Debug, Default)] 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. @@ -43,15 +52,16 @@ impl Collision { &mut self, bullet: &mut Option, invaders: &mut Invaders, - ) -> bool { + prng: &mut PCG32, + ) -> Option> { // 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,21 +84,57 @@ 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) { - // TODO: Explosion! Score! + let invader_rect = Rect::from_drawable(invader.pos, &invader.sprite); + if bullet_rect.intersects(invader_rect) { + // 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. @@ -97,12 +143,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 @@ -118,52 +164,125 @@ 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) { + pub(crate) fn laser_to_player( + &mut self, + laser: &Laser, + player: &Player, + prng: &mut PCG32, + ) -> Option> { + 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 laser_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 laser_particles.drain(..) { + particles.push(particle); + } + + Some(particles) } else { - false + None } } /// 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() { - let laser_rect = Rect::from_drawable(&laser.pos, &laser.sprite); + pub(crate) fn laser_to_bullet( + &mut self, + laser: &Laser, + bullet: &mut Option, + prng: &mut PCG32, + ) -> Option> { + let particles = if let Some(bullet) = bullet { + let laser_rect = Rect::from_drawable(laser.pos, &laser.sprite); + let bullet_rect = Rect::from_drawable(bullet.pos, &bullet.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); + if bullet_rect.intersects(laser_rect) { + let detail = BulletDetail::Laser; + self.bullet_details.insert(detail); - // Destroy laser and bullet - destroy = true; + // Create a spectacular explosion! + let mut particles = { + let force = 4.0; + let center = Vec2D::new(bullet.pos.x as f32 - laser.pos.x as f32 + 2.5, -0.5); + + drawable_to_particles( + prng, + laser.pos, + &laser.sprite, + laser.sprite.rect(), + force, + center, + ) + }; + let mut bullet_particles = { + let force = 4.0; + let center = Vec2D::new(laser.pos.x as f32 - bullet.pos.x as f32 + 0.9, 4.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. 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 +297,152 @@ 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`. + /// + /// # 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, + 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 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[index] > 0 + { + // 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 - 2..=y + 2 { + for u in x - 2..=x + 2 { + let index = u * 4 + v * stride; + + // Only checking the red channel, again + if self.pixel_mask[index] > 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 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 + 2 || self.pixel_mask[index + 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); + + // 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. + 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; + + // 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; + } + + 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..05098a54 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,176 @@ 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 `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. + /// + /// # Panics + /// + /// Asserts that length of `self != 0.0` + pub(crate) fn normalize(self) -> Vec2D { + let l = self.len(); + assert!(l.abs() > std::f32::EPSILON); + + 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) + } +} + +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.cross(w) + } +} + +/// 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 +274,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 +291,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"); + } - 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_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 f05df65e..5beafaac 100644 --- a/simple-invaders/src/lib.rs +++ b/simple-invaders/src/lib.rs @@ -4,13 +4,17 @@ //! this in practice. That said, the game is fully functional, and it should not be too difficult //! to understand the code. +#![deny(clippy::all)] + 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 crate::particles::Particle; +use crate::sprites::{blit, line, Animation, Drawable, Frame, Sprite, SpriteRef}; +use arrayvec::ArrayVec; use randomize::PCG32; mod collision; @@ -18,6 +22,7 @@ mod controls; mod debug; mod geo; mod loader; +mod particles; mod sprites; /// The screen width is constant (units are in pixels) @@ -40,11 +45,12 @@ 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, @@ -148,27 +154,35 @@ impl World { let assets = load_assets(); // TODO: Create invaders one-at-a-time - let invaders = Invaders { + 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::new(); + }); + + 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 { + + let player = Some(Player { sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), pos: PLAYER_START, dt: 0, - }; + }); + 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; let dt = Duration::default(); @@ -181,6 +195,7 @@ impl World { shields, player, bullet, + particles, collision, score, assets, @@ -198,11 +213,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 @@ -211,14 +221,26 @@ 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); + 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 @@ -229,13 +251,25 @@ impl World { 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 { + 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; + } + } + } + if self.bullet.is_some() { self.collision .bullet_to_shield(&mut self.bullet, &mut self.shields); } @@ -245,24 +279,47 @@ 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) { - // 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); + 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 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); + } } } else { destroy.push(i); @@ -282,11 +339,22 @@ impl World { // Clear the screen clear(screen); + // 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(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(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(screen, &invader.pos, &invader.sprite); + } } } } @@ -297,7 +365,10 @@ impl World { } // Draw the player - blit(screen, &self.player.pos, &self.player.sprite); + if self.player.is_some() { + let player = self.player.as_ref().unwrap(); + blit(screen, &player.pos, &player.sprite); + } // Draw the bullet if let Some(bullet) = &self.bullet { @@ -309,6 +380,12 @@ impl World { blit(screen, &laser.pos, &laser.sprite); } + // Copy screen to the backbuffer for particle simulation + self.collision.pixel_mask.copy_from_slice(screen); + + // Draw particles + particles::draw(screen, &self.particles); + // Draw debug information if self.debug { debug::draw_invaders(screen, &self.invaders, &self.collision); @@ -320,35 +397,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!(), @@ -356,19 +433,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! } } @@ -381,7 +461,7 @@ impl World { 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)), @@ -393,21 +473,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); } } _ => (), @@ -416,7 +497,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..e1af1757 --- /dev/null +++ b/simple-invaders/src/particles.rs @@ -0,0 +1,195 @@ +//! 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 randomize::PCG32; + +/// 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. + // - [x] Detect collisions. + // - [x] 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 = 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. + // 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 + 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 PCG32, + pos: Point, + drawable: &D, + src: Rect, + force: f32, + center: Vec2D, +) -> ArrayVec<[Particle; 1024]> +where + D: Drawable, +{ + 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 velocity = Vec2D::new(x as f32, y as f32) - center; + let scale = (extreme - velocity.len()) / extreme; + 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); + + 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..5f53f692 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, Rect}; 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 @@ -70,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]; } @@ -152,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 } @@ -166,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 } @@ -187,6 +197,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 +229,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);