diff --git a/src/cache.rs b/src/cache.rs index c727d99..39163cc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -190,7 +190,7 @@ impl Cache { } pub fn get_sprite(&self, index: usize) -> &(CompShape, Vec) { - &self.sprites[index] + &self.sprites[index - 21] } pub fn get_sound(&self, index: usize) -> &Vec { diff --git a/src/constants.rs b/src/constants.rs index 9986743..1264788 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -14,6 +14,9 @@ pub const STATUS_LINES: u32 = 40; pub const BASE_WIDTH: u32 = 320; pub const BASE_HEIGHT: u32 = 200; pub const WALLPIC_WIDTH: usize = 64; +// where does this value come from? +pub const TILE_SIZE: f64 = 4.8; +pub const FIELD_OF_VIEW: f64 = PI / 2.0; // ok this is not a constant, we may move it to an util module later, or rename this pub fn norm_angle(a: f64) -> f64 { diff --git a/src/main.rs b/src/main.rs index 1221c3b..4c70bfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,15 @@ #![allow(dead_code)] use cache::Picture; use core::slice::Iter; -use std::time::Instant; +use map::Actor; +use ray_caster::RayHit; +use std::{ + collections::{HashMap, HashSet}, + time::Instant, +}; use clap::Parser; +use std::f64::consts::PI; use minifb::{Key, KeyRepeat, Window, WindowOptions}; @@ -60,6 +66,7 @@ struct Game { level: usize, start_time: Instant, cache: cache::Cache, + static_objects: HashMap<(usize, usize), u16>, } pub fn main() { @@ -80,7 +87,11 @@ pub fn main() { show_title(&game, &mut video, &mut window); while process_input(&window, &mut game.player).is_ok() { - draw_world(&game, &mut video); + let (ray_hits, visible_tiles) = + ray_caster::draw_rays(video.pix_width, video.pix_height, &game.map, &game.player); + + draw_world(&game, &mut video, &ray_hits); + draw_actors(&game, &mut video, &ray_hits, &visible_tiles); draw_weapon(&game, &mut video); draw_status(&game, &mut video); @@ -122,11 +133,7 @@ fn show_title(game: &Game, video: &mut Video, window: &mut Window) { } } -fn draw_world(game: &Game, video: &mut Video) { - // TODO consider passing game as param here - let ray_hits = - ray_caster::draw_rays(video.pix_width, video.pix_height, &game.map, &game.player); - +fn draw_world(game: &Game, video: &mut Video, ray_hits: &[RayHit]) { // draw floor and ceiling for x in 0..video.pix_width { for y in 0..video.pix_height / 2 { @@ -173,11 +180,61 @@ fn draw_world(game: &Game, video: &mut Video) { } } +fn draw_actors( + game: &Game, + video: &mut Video, + ray_hits: &[RayHit], + visible: &HashSet<(usize, usize)>, +) { + let mut statics = Vec::new(); + for &(tx, ty) in visible { + if let Some(shapenum) = game.static_objects.get(&(tx, ty)) { + // https://dev.opera.com/articles/3d-games-with-canvas-and-raycasting-part-2/ + // FIXME way too much calculation here, a lot of this should be fixed + + // FIXME this area has some bugs I can't yet figure out, + // the heights are not correct, there's some sprite sliding when camera rotats + // and --probably related-- the sprites are hidden before they are occluded by a wall + let dx = (tx as f64 + 0.5) * MAP_SCALE_W as f64 - game.player.x; + let dy = (ty as f64 + 0.5) * MAP_SCALE_H as f64 - game.player.y; + let view_dist = TILE_SIZE * video.pix_width as f64; + + let angle = dy.atan2(dx); + let offset = FIELD_OF_VIEW/2.0 - angle; + let ytemp = norm_angle(game.player.angle - offset); + let viewx = ytemp * video.pix_width as f64 / FIELD_OF_VIEW; + + let distance = dx * offset.cos() - dy * offset.sin(); + let height = (view_dist / distance)as u32; + + let pos = statics + .binary_search_by_key(&height, |&(h, _, _)| h) + .unwrap_or_else(|x| x); + statics.insert(pos, (height, viewx, shapenum)); + } + } + + for (height, viewx, shapenum) in statics { + let (shape, data) = game.cache.get_sprite(*shapenum as usize); + + // TODO pass the shape num instead of pieces of the shape? + video.scale_shape( + viewx as u32, + height, + shape.left_pix, + shape.right_pix, + &shape.dataofs, + data, + ray_hits, + ); + } +} + fn draw_weapon(game: &Game, video: &mut Video) { // FIXME use a constant for that 209 - let (weapon_shape, weapon_data) = game.cache.get_sprite(209); + let (weapon_shape, weapon_data) = game.cache.get_sprite(230); - // TODO pass the shape num instead of pieces of the shape + // TODO pass the shape num instead of pieces of the shape? video.simple_scale_shape( weapon_shape.left_pix, weapon_shape.right_pix, @@ -208,6 +265,16 @@ impl Game { let cache = cache::init(); let map = cache.get_map(0, level); let player = map.find_player(); + + let mut static_objects = HashMap::new(); + for x in 0..MAP_WIDTH { + for y in 0..MAP_HEIGHT { + if let Some(Actor::Static(shapenum)) = map.actor_at(x, y) { + static_objects.insert((x, y), shapenum); + } + } + } + Self { cache, map, @@ -216,6 +283,7 @@ impl Game { episode: 0, level, start_time: Instant::now(), + static_objects, } } } @@ -382,6 +450,107 @@ impl Video { i += 1; } } + + // FIXME this is almost identical to simple_scale_shape, except for the height check. + // refactor to reuse the code + fn scale_shape( + &mut self, + xcenter: u32, + height: u32, + left_pix: u16, + right_pix: u16, + dataofs: &[u16], + shape_bytes: &[u8], + ray_hits: &[RayHit], + ) { + if height > self.pix_center { + return; + } + + let sprite_scale_factor = 2; + let scale = height; + let pixheight = scale * sprite_scale_factor; + let actx = xcenter - scale; + let upperedge = self.pix_height / 2 - scale; + // cmdptr=(word *) shape->dataofs; + // cmdptr = iter(shape.dataofs) + let mut cmdptr = dataofs.iter(); + + let mut i = left_pix; + let mut pixcnt = i as u32 * pixheight; + let mut rpix = (pixcnt >> 6) + actx; + + while i <= right_pix { + let mut lpix = rpix; + if lpix >= self.pix_width { + break; + } + + pixcnt += pixheight; + rpix = (pixcnt >> 6) + actx; + + if lpix != rpix && rpix > 0 { + if rpix > self.pix_width { + rpix = self.pix_width; + i = right_pix + 1; + } + let read_word = |line: &mut Iter| { + u16::from_le_bytes([*line.next().unwrap_or(&0), *line.next().unwrap_or(&0)]) + }; + let read_word_signed = |line: &mut Iter| { + i16::from_le_bytes([*line.next().unwrap_or(&0), *line.next().unwrap_or(&0)]) + }; + + let cline = &shape_bytes[*cmdptr.next().unwrap() as usize..]; + while lpix < rpix { + if ray_hits[lpix as usize].height <= height { + let mut line = cline.iter(); + let mut endy = read_word(&mut line); + while endy > 0 { + endy >>= 1; + let newstart = read_word_signed(&mut line); + let starty = read_word(&mut line) >> 1; + let mut j = starty; + let mut ycnt = j as u32 * pixheight; + let mut screndy: i32 = (ycnt >> 6) as i32 + upperedge as i32; + + let mut pixy = screndy as u32; + while j < endy { + let mut scrstarty = screndy; + ycnt += pixheight; + screndy = (ycnt >> 6) as i32 + upperedge as i32; + if scrstarty != screndy && screndy > 0 { + let index = newstart + j as i16; + let col = if index >= 0 { + shape_bytes[index as usize] + } else { + 0 + }; + if scrstarty < 0 { + scrstarty = 0; + } + if screndy > self.pix_height as i32 { + screndy = self.pix_height as i32; + j = endy; + } + + while scrstarty < screndy { + self.put_darkened_pixel(lpix, pixy, col as usize, height); + pixy += 1; + scrstarty += 1; + } + } + j += 1; + } + endy = read_word(&mut line); + } + } + lpix += 1; + } + } + i += 1; + } + } } /// Returns an array of colors that maps indexes as used by wolf3d graphics diff --git a/src/map.rs b/src/map.rs index 3342f65..7504a03 100644 --- a/src/map.rs +++ b/src/map.rs @@ -18,9 +18,8 @@ pub enum Direction { pub enum Actor { Player(Direction), - Enemy, // TODO differentiate enemy types - Item, // TODO differentiate item types - DeadGuard, + Enemy(u16), // TODO differentiate enemy types + Static(u16), // TODO differentiate item types PushWall, } @@ -61,16 +60,17 @@ impl Map { } } - pub fn actor_at(&self, x: u8, y: u8) -> Option { - match self.plane1[x as usize][y as usize] { + pub fn actor_at(&self, x: usize, y: usize) -> Option { + match self.plane1[x][y] { 19 => Some(Actor::Player(Direction::North)), 20 => Some(Actor::Player(Direction::East)), 21 => Some(Actor::Player(Direction::South)), 22 => Some(Actor::Player(Direction::West)), - n if (23..=72).contains(&n) => Some(Actor::Item), + n if (23..=72).contains(&n) => Some(Actor::Static(n)), + // FIXME why - 8 ? + 124 => Some(Actor::Static(124 - 8)), //dead guard 98 => Some(Actor::PushWall), - 124 => Some(Actor::DeadGuard), - n if n >= 108 => Some(Actor::Enemy), + n if n >= 108 => Some(Actor::Enemy(n)), _ => None, } } @@ -94,9 +94,9 @@ impl Map { } } - pub fn find_player_start(&self) -> (u8, u8, Direction) { - for x in 0..MAP_WIDTH as u8 { - for y in 0..MAP_HEIGHT as u8 { + fn find_player_start(&self) -> (usize, usize, Direction) { + for x in 0..MAP_WIDTH { + for y in 0..MAP_HEIGHT { if let Some(Actor::Player(direction)) = self.actor_at(x, y) { return (x, y, direction); } diff --git a/src/player.rs b/src/player.rs index 230b9b9..5f38d1e 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,4 +1,5 @@ use crate::constants; +use num::pow; const ROTATE_SPEED: f64 = 0.02; const MOVE_SPEED: f64 = 2.5; @@ -29,4 +30,8 @@ impl Player { self.angle -= ROTATE_SPEED; self.angle = constants::norm_angle(self.angle); } + + pub fn distance_to(&self, x: f64, y: f64) -> f64 { + (pow(x - self.x, 2) + pow(y - self.y, 2)).sqrt() + } } diff --git a/src/ray_caster.rs b/src/ray_caster.rs index b716a11..4454aaf 100644 --- a/src/ray_caster.rs +++ b/src/ray_caster.rs @@ -1,15 +1,8 @@ use crate::constants::*; use crate::map::{Map, Tile}; use crate::player::Player; -use num::pow; use std::cmp::min; -use std::f64::consts::PI; - -const PLAYER_DIAM: i32 = 6; -const PLAYER_LEN: f64 = 40.0; -const FIELD_OF_VIEW: f64 = PI / 2.0; - -const TILE_SIZE: f64 = 4.8; +use std::collections::HashSet; // FIXME this is suspicious, probably use Option or Result? struct Nothing; @@ -21,20 +14,31 @@ pub struct RayHit { pub tex_x: usize, } -pub fn draw_rays(n_rays: u32, height: u32, map: &Map, player: &Player) -> Vec { +pub fn draw_rays( + n_rays: u32, + height: u32, + map: &Map, + player: &Player, +) -> (Vec, HashSet<(usize, usize)>) { let fov_delta = FIELD_OF_VIEW / (n_rays as f64); let mut hits: Vec = Vec::new(); + let mut visited_tiles = HashSet::new(); for i in 0..n_rays { let fov_angle = fov_delta * (i as f64); // transformation from cylindrical screen to flat screen (prevents fisheye effect) let offset = (FIELD_OF_VIEW / 2.0 - fov_angle).atan(); - let ray_h = cast_ray_h(map, player, offset); - let ray_v = cast_ray_v(map, player, offset); - let (hit, horiz) = match (ray_h, ray_v) { - ((_, _, d1, _), (_, _, d2, _)) if d1 <= d2 => (ray_h, false), - _ => (ray_v, true), + let mut ray_h = cast_ray_h(map, player, offset); + let mut ray_v = cast_ray_v(map, player, offset); + + visited_tiles.extend(ray_h.4.drain()); + visited_tiles.extend(ray_v.4.drain()); + + let (hit, horiz) = if ray_h.2 <= ray_v.2 { + (ray_h, false) + } else { + (ray_v, true) }; - let (_, _, distance, tile) = hit; + let (_, _, distance, tile, _) = hit; let adj_distance = distance * offset.cos(); let ray_height = TILE_SIZE * n_rays as f64 / adj_distance; @@ -46,16 +50,20 @@ pub fn draw_rays(n_rays: u32, height: u32, map: &Map, player: &Player) -> Vec (f64, f64, f64, u16) { +fn cast_ray_v( + map: &Map, + player: &Player, + ray_offset: f64, +) -> (f64, f64, f64, u16, HashSet<(usize, usize)>) { let ray_angle = norm_angle(player.angle + ray_offset); //looking to the side -- cannot hit a horizontal line if ray_angle == ANGLE_LEFT || ray_angle == ANGLE_RIGHT { - return (0.0, 0.0, f64::INFINITY, 0); + return (0.0, 0.0, f64::INFINITY, 0, HashSet::new()); } let (rx, ry, xo, yo) = if !(ANGLE_RIGHT..=ANGLE_LEFT).contains(&ray_angle) { @@ -79,12 +87,16 @@ fn cast_ray_v(map: &Map, player: &Player, ray_offset: f64) -> (f64, f64, f64, u1 follow_ray(map, player, rx, ry, xo, yo) } -fn cast_ray_h(map: &Map, player: &Player, ray_offset: f64) -> (f64, f64, f64, u16) { +fn cast_ray_h( + map: &Map, + player: &Player, + ray_offset: f64, +) -> (f64, f64, f64, u16, HashSet<(usize, usize)>) { let ray_angle = norm_angle(player.angle + ray_offset); //looking up/down -- cannot hit a vertical line if ray_angle == ANGLE_UP || ray_angle == ANGLE_DOWN { - return (0.0, 0.0, f64::INFINITY, 0); + return (0.0, 0.0, f64::INFINITY, 0, HashSet::new()); } let (rx, ry, xo, yo) = if ray_angle < ANGLE_UP { @@ -116,33 +128,25 @@ fn follow_ray( y: f64, xo: f64, yo: f64, -) -> (f64, f64, f64, u16) { +) -> (f64, f64, f64, u16, HashSet<(usize, usize)>) { let (mut rx, mut ry) = (x, y); + let mut visited = HashSet::new(); for _ in 1..MAP_HEIGHT { - match read_map(map, rx, ry) { - Ok(Tile::Wall(tile)) => { - return (rx, ry, distance(player, rx, ry), tile); - } - Err(_) => { - return (rx, ry, distance(player, rx, ry), 0); - } - _ => {} + let mx = cdiv(rx, MAP_SCALE_W, 0.0); + let my = cdiv(ry, MAP_SCALE_H, 0.0); + if mx >= MAP_WIDTH || my >= MAP_HEIGHT { + return (rx, ry, player.distance_to(rx, ry), 0, visited); + } + visited.insert((mx, my)); + + if let Tile::Wall(tile) = map.tile_at(mx as u8, my as u8) { + return (rx, ry, player.distance_to(rx, ry), tile, visited); } rx += xo; ry += yo; } - (rx, ry, distance(player, rx, ry), 0) -} - -fn read_map(map: &Map, x: f64, y: f64) -> Result { - let mx = cdiv(x, MAP_SCALE_W, 0.0); - let my = cdiv(y, MAP_SCALE_H, 0.0); - if mx >= MAP_WIDTH || my >= MAP_HEIGHT { - Err(Nothing) - } else { - Ok(map.tile_at(mx as u8, my as u8)) - } + (rx, ry, player.distance_to(rx, ry), 0, visited) } /// Turn the ray hit (x, y) coordinates in to the x-coordinate within the texture. @@ -175,7 +179,3 @@ fn cdiv(x: f64, scale: u32, updown: f64) -> usize { fn ctrunc(x: f64, scale: u32, updown: f64) -> f64 { (x / scale as f64 + updown).trunc() * scale as f64 } - -fn distance(player: &Player, x: f64, y: f64) -> f64 { - (pow(x - player.x, 2) + pow(y - player.y, 2)).sqrt() -}