rustenstein/src/engine.rs

449 lines
14 KiB
Rust

// 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<Tile>,
}
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<Movement>,
}
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<u32>) {
// 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);
}
}
}