Open Your Eyes

3 minute read

78 Days Until I Can Walk

Success!

The ray cast algorithm appears to be working. At least from a static viewpoint in the middle of a simple square room. It is entirely possible that there are hidden bugs which I won’t see until I start letting the player walk around the environment.

A scene spins around in a circle showing four solid red walls in a square room

I started out by looking at F. Permadi’s Ray Cast implementation and refactored my code a little to reflect his. This highlighted a silly oversight on my part where I wasn’t correctly setting the step increment for the ray if the user was facing up or to the left. Still, even after correcting this, the render was completely wrong.

In the end, I set about writing some unit tests so I could break the application up and really inspect each individual piece to determine where I was going wrong. Unit tests in Rust are quite nice, I have to say! I like that they live at the bottom of the source file so you can more fluidly update your code and then scroll down to modify your tests accordingly. I did have a little bit of trouble comparing floating point numbers in the unit tests, however. To handle this, I added the float_cmp crate to my project which allows me to perform approximate comparisons between floating point numbers.

After setting up some incredibly simple tests, I realised that I was computing the distance between the point of origin of the ray and the point of intersection with the wall incorrectly. I had the vertical wall intersect distance in the horizontal function and vice versa. So yeah, unit tests. Well worth your time.

Implementing the unit tests involved refactoring quite a lot of my code, as my initial implementation just dumped a lot of data into global variables. This was always temporary, and now many of those variables are somewhat sensibly encapsulated by struct World. I expect this struct will hold most, if not all information about a level—walls, objects, enemies, doors, etc. But for now it simply stores the horizontal and vertical wall locations, as well as the width and height of the map.

pub enum Tile {
  Empty,
  Wall,
}

pub struct World {
  tile_size: i32,
  width: i32,
  height: i32,
  h_walls: Vec<Tile>,
  v_walls: Vec<Tile>,
}

impl World {
  pub fn new(width: i32, height: i32, map_str: &str) -> Result<World, &str> {
    if width < 0 || height < 0 {
      return Err("Width and height must be positive values");
    }

    if (width * height) as usize != map_str.chars().count() {
      return Err("Width and height parameters do not match size of serialized map string");
    }

    let h_walls: Vec<Tile> = map_str.chars()
      .map(|c| {
        if c == 'W' || c == 'H' {
          Tile::Wall
        } else {
          Tile::Empty
        }
      })
      .collect();

    let v_walls: Vec<Tile> = map_str.chars()
      .map(|c| {
        if c == 'W' || c == 'V' {
          Tile::Wall
        } else {
          Tile::Empty
        }
      })
      .collect();

    Ok(World { tile_size: consts::TILE_SIZE, width, height, h_walls, v_walls })
  }
}

// Sample world initialization from a serialized string
// O = open space
// V = vertical wall slice
// H = horizontal wall slice
// W = horizontal and vertical wall slice
let world = raycast::World::new(7, 7, "WHHHHHWVOOOOOVVOOOOOVVOOOOOVVOOOOOVVOOOOOVWHHHHHW").unwrap();

When implementing struct World I wrote a simple initialization function (seen in the code extract above) that takes a height, width, and string parameter which together can be parsed into horizontal and vertical wall slices. It is now much easier for me to generate simple levels for testing purposes, although some sort of level editor is definitely on the horizon for this project.

Right now struct World contains the tile size for the map, but I suspect I will soon move that into a global context. It feels too much like one of the fundamental assumptions that the engine will make and should not be subject to modification in any sort of local scope.

One Rust feature that I am somewhat excited about is the ability to attach data to enum values. Each vertical/horizontal wall in my map is represented by enum Tile which either has the value Empty or Wall. I will be able to attach texture information to the Wall value so that when a ray intersects with a wall slice, the rendering engine will know what texture to apply to the screen. Nice!

First thing tomorrow morning I plan to implement some simple controls so the user can walk around the environment instead of having to look at this static spinning camera. I don’t expect that will take long, but it is possible that some rendering glitches will start to show themselves once I relinquish control of the camera’s position. We’ll see what happens, but for now I am very happy with today’s progress.