// extern crate piston_window; // use piston_window::*; use std::f64::consts::*; #[derive(Copy, Clone, PartialEq, Debug)] pub struct Position { x: f64, y: f64, } impl Position { pub fn distance(&self, other: Position) -> f64 { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() } // pub fn distance_sqr(&self, other: Position) -> f64 { // (self.x - other.x).powi(2) + (self.y - other.y.powi(2)) // } } type Degree = f64; type Radian = f64; #[derive(Debug)] struct Player { pos: Position, angle: Degree, } impl Player { pub fn player_space_distance(&self, other: Position) -> f64 { let rad = self.angle.to_radians(); let dx = other.x - self.pos.x; let dy = other.y - self.pos.y; let x = dx * rad.cos(); let y = dy * rad.sin(); let distance = (x + y).abs(); // println!("angle: {}, dx: {}, dy: {}, x; {}, y: {}", self.angle, dx, dy, x, y); assert!(distance.is_sign_positive()); distance } } #[derive(Debug, Copy, Clone, PartialEq)] pub enum Tile { Empty, Wall, } pub struct Level { pub width: usize, pub height: usize, pub tiles: Vec, } impl Level { pub fn contains(&self, pos: Position) -> bool { 0.0 <= pos.x && pos.x <= self.width as f64 && 0.0 <= pos.y && pos.y <= self.height as f64 } fn tile_at(&self, pos: Position) -> Tile { assert!(self.contains(pos)); self.tiles[(pos.x.trunc() as usize) + (pos.y.trunc() as usize) * self.width] } } #[derive(Copy, Clone, PartialEq, Debug)] pub enum Movement { Forward, Backward, TurnLeft, TurnRight, } fn step_for_angle(angle: Radian) -> (f64, f64) { match angle { x if x == 0.0 => (0.0, std::f64::INFINITY), x if x == PI * 0.5 => (std::f64::INFINITY, 0.0), x if x == PI => (0.0, std::f64::INFINITY), x if x == PI * 1.5 => (std::f64::INFINITY, 0.0), x if (0.0..(PI * 0.5)).contains(&x) => (angle.tan(), ((PI * 0.5) - angle).tan()), x if ((PI * 0.5)..PI).contains(&x) => (-x.tan(), -((x - (PI * 0.5)).tan())), x if (PI..(PI * 1.5)).contains(&x) => (-x.tan(), -(((PI * 1.5) - x).tan())), x if ((PI * 1.5)..(PI * 2.0)).contains(&x) => (x.tan(), ((x - (PI * 1.5)).tan())), _ => panic!("Invalid angle value {}.", angle), } } fn closest_point(level: &Level, pos: &Position, angle: Radian) -> (Tile, Position) { assert!((0.0..(PI * 2.0)).contains(&angle)); let (y_step, x_step) = step_for_angle(angle); // println!("step: ({}, {})", x_step, y_step); let (x_remain, y_remain) = match (x_step, y_step) { (x, y) if x >= 0.0 && y >= 0.0 => (1.0 - pos.x.fract(), 1.0 - pos.y.fract()), (x, y) if x <= 0.0 && y >= 0.0 => (-pos.x.fract(), 1.0 - pos.y.fract()), (x, y) if x >= 0.0 && y <= 0.0 => (1.0 - pos.x.fract(), -pos.y.fract()), (x, y) if x <= 0.0 && y <= 0.0 => (-pos.x.fract(), -pos.y.fract()), _ => panic!("Invalid steps"), }; let x_dist_factor = x_remain / x_step; let y_dist_factor = y_remain / y_step; let mut x_candidate = Position { x: pos.x + x_remain, // x_remain = x_step * x_dist_factor y: pos.y + y_step * x_dist_factor, }; let mut y_candidate = Position { x: pos.x + x_step * y_dist_factor, y: pos.y + y_remain, // y_remain = y_step * y_dist_factor }; let mut next_point: Position = *pos; let mut tile = Tile::Empty; while tile == Tile::Empty && level.contains(next_point) { if next_point.distance(x_candidate) < next_point.distance(y_candidate) { next_point = x_candidate; x_candidate = Position { x: x_candidate.x + x_step.signum(), y: x_candidate.y + y_step, }; } else { next_point = y_candidate; y_candidate = Position { x: y_candidate.x + x_step, y: y_candidate.y + y_step.signum(), }; } // println!("next candidate: {:?}", next_point); tile = if next_point.x.fract() == 0.0 { let mut position = next_point; position.x += 0.5 * x_step.signum(); level.tile_at(position) } else { let mut position = next_point; position.y += 0.5 * y_step.signum(); level.tile_at(position) }; } assert!(tile != Tile::Empty); (tile, next_point) } pub struct Engine { w: f64, h: f64, horiz_fov: Degree, player: Player, level: Level, inputs: Vec, } impl Engine { pub fn new(width: usize, height: usize) -> Engine { Engine { w: width as f64, h: height as f64, horiz_fov: 90., player: Player { pos: Position { x: 1.5, y: 2. }, angle: 0., }, level: Level { width: 0, height: 0, tiles: vec![], }, inputs: [].to_vec(), } } pub fn render(&mut self, buffer: &mut Vec) { // clear([1.0; 4], graphics); // // Ceiling // let ceiling_color = [0.3, 0.3, 0.3, 1.0]; // rectangle( // ceiling_color, // [0.0, 0.0, self.w, self.h / 2.0], // context.transform, // graphics, // ); // // Floor // let floor_color = [0.5, 0.5, 0.5, 1.0]; // rectangle( // floor_color, // [0.0, self.h / 2.0, self.w, self.h / 2.0], // context.transform, // graphics, // ); let left = self.player.angle + (self.horiz_fov / 2.0); let step = self.horiz_fov / self.w; let width = self.w as i32; for n in 0..width { let ray_angle = ((left - (n as f64) * step) + 360.0) % 360.0; let ray_radian = ray_angle.to_radians(); let (tile, pos) = closest_point(&self.level, &self.player.pos, ray_radian); let distance = self.player.player_space_distance(pos); if tile == Tile::Wall { let wall_height = (self.h / (distance * 3.0)).min(self.h); let wall_color = match pos { p if (p.x.trunc() + p.y.trunc()) % 4.0 == 0.0 => [0.2, 0.2, 0.9, 1.0], p if (p.x.trunc() + p.y.trunc()) % 4.0 == 1.0 => [0.4, 0.4, 0.9, 1.0], p if (p.x.trunc() + p.y.trunc()) % 4.0 == 2.0 => [0.6, 0.6, 0.9, 1.0], p if (p.x.trunc() + p.y.trunc()) % 4.0 == 3.0 => [0.7, 0.3, 0.9, 1.0], _ => [1.0, 0.0, 0.0, 1.0], }; println!( "ray: {}, angle: {}, wall at {:?}, distance: {}", n, ray_angle, pos, distance ); // rectangle( // wall_color, // [n as f64, (self.h - wall_height) / 2.0, 1.0, wall_height], // context.transform, // graphics, // ); }; } //std::process::exit(0); } pub fn load_level(&mut self, level: Level) { self.level = level; } pub fn add_movement(&mut self, movement: Movement) { self.inputs.push(movement); } pub fn update(&mut self, dt: f64) { for input in &self.inputs { let previous = self.player.pos; match input { Movement::Forward => { self.player.pos.x += self.player.angle.to_radians().cos() * dt; self.player.pos.y += self.player.angle.to_radians().sin() * dt; } Movement::Backward => { self.player.pos.x -= self.player.angle.to_radians().cos() * dt; self.player.pos.y -= self.player.angle.to_radians().sin() * dt; } Movement::TurnLeft => { self.player.angle += 90.0 * dt; self.player.angle = (self.player.angle + 360.0) % 360.0; } Movement::TurnRight => { self.player.angle -= 90.0 * dt; self.player.angle = (self.player.angle + 360.0) % 360.0; } } if !self.level.contains(self.player.pos) || self.level.tile_at(self.player.pos) == Tile::Wall { println!( "Invalid position {:?}, tile = {:?}", self.player.pos, self.level.tile_at(self.player.pos) ); self.player.pos = previous; } } self.inputs.clear(); println!("player: {:?}", &self.player); } } #[cfg(test)] mod tests { use super::*; fn fcmp(a: f64, b: f64) { let epsilon = 1e-5; assert!((a - b).abs() < epsilon); } #[test] fn player_space_distance() { let mut player = Player { pos: super::Position { x: 2., y: 2. }, angle: 0., }; fcmp( player.player_space_distance(super::Position { x: 4., y: 1. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 4., y: 2. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 4., y: 3. }), 2., ); player.angle = 90.; fcmp( player.player_space_distance(super::Position { x: 1., y: 4. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 2., y: 4. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 3., y: 4. }), 2., ); player.angle = 135.; fcmp( player.player_space_distance(super::Position { x: 0., y: 2. }), 2., ); player.angle = 180.; fcmp( player.player_space_distance(super::Position { x: 0., y: 1. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 0., y: 2. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 0., y: 3. }), 2., ); player.angle = 270.; fcmp( player.player_space_distance(super::Position { x: 1., y: 0. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 2., y: 0. }), 2., ); fcmp( player.player_space_distance(super::Position { x: 3., y: 0. }), 2., ); } #[test] fn tile_at() { #[rustfmt::skip] let tiles = vec![ Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, ]; let level = Level { width: 5, height: 5, tiles, }; assert_eq!( level.tile_at(super::Position { x: 2.0, y: 2.0 }), Tile::Empty ); } #[test] fn step_for_angle() { let step_cmp = |angle: Degree, y: f64, x: f64| { let (step_y, step_x) = super::step_for_angle(angle.to_radians()); println!("angle: {}, step: {}, {}", angle, step_y, step_x); fcmp(step_y, y); fcmp(step_x, x); }; step_cmp(30.0, 0.5773502691896257, 1.7320508075688776); step_cmp(45.0, 1.0, 1.0); step_cmp(90.0 + 45.0, 1.0, -1.0); step_cmp(180.0 + 45.0, -1.0, -1.0); step_cmp(270.0 + 45.0, -1.0, 1.0); } #[test] fn closest_point() { #[rustfmt::skip] let tiles = vec![ Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, ]; let level = Level { width: 5, height: 5, tiles, }; let position = super::Position { x: 2.5, y: 2.0 }; { let radian = 0.1; let (tile, pos) = super::closest_point(&level, &position, radian); println!("pos: {:?}", pos); assert_eq!(tile, Tile::Wall); fcmp(pos.x, 4.0); assert!(2.0 <= pos.y && pos.y <= 2.2); } { let radian = 2.0 * PI - 0.1; let (tile, pos) = super::closest_point(&level, &position, radian); println!("pos: {:?}", pos); assert_eq!(tile, Tile::Wall); fcmp(pos.x, 4.0); assert!(1.8 <= pos.y && pos.y <= 2.0); } let fov = 90.0; let step = fov / 40.0; for n in 0..20 { let left_ray_angle = (((n as f64) * step) + 360.0) % 360.0; let left_ray_radian = left_ray_angle.to_radians(); let (_left_tile, left_pos) = super::closest_point(&level, &position, left_ray_radian); let right_ray_angle = ((-(n as f64) * step) + 360.0) % 360.0; let right_ray_radian = right_ray_angle.to_radians(); let (_right_tile, right_pos) = super::closest_point(&level, &position, right_ray_radian); println!("left: {:?}, angle: {}", left_pos, left_ray_angle); println!("right: {:?}, angle: {}", right_pos, right_ray_angle); assert_eq!((left_ray_angle + right_ray_angle) % 360.0, 0.0); fcmp(left_pos.x, right_pos.x); fcmp(left_pos.y - 2.0, 2.0 - right_pos.y); } } }