├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── res ├── 297.png ├── barrel.png ├── bina.png ├── carl.png ├── ceiling-tile.png ├── door.png ├── floor-tile.png ├── gravestone.png ├── maps │ ├── dungeon.png │ └── temple.png ├── schindler.png ├── statue.png ├── textbook.jpg └── wall-stone.png ├── rustcaster-lib ├── Cargo.toml └── src │ └── lib.rs └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.dll 3 | .vscode 4 | Cargo.lock 5 | .DS_STORE 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustcaster-demo" 3 | version = "0.1.0" 4 | authors = ["Declan Hopkins "] 5 | 6 | [dependencies] 7 | rustcaster = { path = "rustcaster-lib" } 8 | time = "0.1.38" 9 | 10 | [dependencies.sdl2] 11 | version = "0.31" 12 | default-features = false 13 | features = ["image"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Declan Hopkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rustcaster 2 | 3 | This is a fairly lightweight raycaster, which supports ceilings, floors, and sprites. It uses SDL2 for windowing, rendering, and image loading. It's the first actual program I've written in Rust, so it's likely that it isn't idiomatic. Feel free to leave any feedback! 4 | 5 | Most of the actual raycasting code is contained within the internal `rustcaster-lib` library. By supplying the `render` function with a map, camera values, and some other information, it will render the view into a buffer that can be displayed on the screen. The map and textures are loaded from the `res` directory. 6 | 7 | ### Building and Running 8 | 9 | If you have the SDL2 libraries on your computer, then you should be good to go with a simple `cargo run`. It is recommended that you specify the `--release` flag, as the unoptimized code for this project runs quite slow. 10 | 11 | If you are on Windows, you will need the SDL2 and SDL2 image .dlls in the working directory of the executable. 12 | 13 | ### Resources 14 | 15 | I do not own the example textures used, as they are from the game Blood, and are owned by Monolith Productions. Find more [here](http://www.bghq.com/textures.php?game=blood). 16 | 17 | ![Rustcaster](https://i.imgur.com/92rrixn.png) 18 | -------------------------------------------------------------------------------- /res/297.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/297.png -------------------------------------------------------------------------------- /res/barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/barrel.png -------------------------------------------------------------------------------- /res/bina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/bina.png -------------------------------------------------------------------------------- /res/carl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/carl.png -------------------------------------------------------------------------------- /res/ceiling-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/ceiling-tile.png -------------------------------------------------------------------------------- /res/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/door.png -------------------------------------------------------------------------------- /res/floor-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/floor-tile.png -------------------------------------------------------------------------------- /res/gravestone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/gravestone.png -------------------------------------------------------------------------------- /res/maps/dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/maps/dungeon.png -------------------------------------------------------------------------------- /res/maps/temple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/maps/temple.png -------------------------------------------------------------------------------- /res/schindler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/schindler.png -------------------------------------------------------------------------------- /res/statue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/statue.png -------------------------------------------------------------------------------- /res/textbook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/textbook.jpg -------------------------------------------------------------------------------- /res/wall-stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dooskington/Rustcaster/94ce5fd63d0ffa41d48ba6f51e88ee71f87d25ac/res/wall-stone.png -------------------------------------------------------------------------------- /rustcaster-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustcaster" 3 | version = "0.1.0" 4 | authors = ["Declan Hopkins "] 5 | 6 | [dependencies] 7 | time = "0.1.38" 8 | -------------------------------------------------------------------------------- /rustcaster-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub const TWO_PI: f64 = 2.0 * std::f64::consts::PI; 2 | 3 | pub const COLOR_BLACK: Color = Color { r: 0, g: 0, b: 0, a: 255 }; 4 | pub const COLOR_WHITE: Color = Color { r: 255, g: 255, b: 255, a: 255 }; 5 | pub const COLOR_RED: Color = Color { r: 255, g: 0, b: 0, a: 255 }; 6 | pub const COLOR_GREEN: Color = Color { r: 0, g: 255, b: 0, a: 255 }; 7 | pub const COLOR_BLUE: Color = Color { r: 0, g: 0, b: 255, a: 255 }; 8 | pub const COLOR_MAGENTA: Color = Color { r: 255, g: 0, b: 255, a: 255 }; 9 | 10 | pub struct Map { 11 | pub width: u32, 12 | pub height: u32, 13 | pub floor_texture_id: u32, 14 | pub ceiling_texture_id: u32, 15 | pub cells: Vec>, 16 | pub sprites: Vec 17 | } 18 | 19 | impl Map { 20 | pub fn get_cell(&self, x: u32, y: u32) -> Option { 21 | self.cells[((y * self.width) + x) as usize] 22 | } 23 | } 24 | 25 | #[derive(Copy, Clone)] 26 | pub struct Cell { 27 | pub x: u32, 28 | pub y: u32, 29 | pub texture_id: u32 30 | } 31 | 32 | pub struct RayIntersection { 33 | pub x: f64, 34 | pub y: f64, 35 | pub cell_x: u32, 36 | pub cell_y: u32, 37 | pub cell_side: u8, 38 | pub distance: f64 39 | } 40 | 41 | pub struct Sprite { 42 | pub x: f64, 43 | pub y: f64, 44 | pub texture_id: u32 45 | } 46 | 47 | impl Sprite { 48 | pub fn new(x: f64, y: f64, texture_id: u32) -> Sprite { 49 | Sprite { 50 | x: x, 51 | y: y, 52 | texture_id: texture_id 53 | } 54 | } 55 | } 56 | 57 | pub struct Texture { 58 | pub texture_id: u32, 59 | pub width: u32, 60 | pub height: u32, 61 | pub pixels: Vec 62 | } 63 | 64 | impl Texture { 65 | pub fn new(texture_id: u32, width: u32, height: u32, pixels: Vec) -> Texture { 66 | Texture { 67 | texture_id: texture_id, 68 | width: width, 69 | height: height, 70 | pixels: pixels 71 | } 72 | } 73 | 74 | pub fn get_pixel(&self, x: u32, y: u32) -> Color { 75 | self.pixels[((y * self.width) + x) as usize] 76 | } 77 | } 78 | 79 | #[derive(Copy, Clone)] 80 | pub struct Color { 81 | pub r: u8, 82 | pub g: u8, 83 | pub b: u8, 84 | pub a: u8 85 | } 86 | 87 | impl PartialEq for Color { 88 | fn eq(&self, other: &Color) -> bool { 89 | (self.r == other.r) && 90 | (self.g == other.g) && 91 | (self.b == other.b) && 92 | (self.a == other.a) 93 | } 94 | } 95 | 96 | pub struct Engine { 97 | pub field_of_view: f64, 98 | pub projection_width: u32, 99 | pub projection_height: u32, 100 | pub projection_distance: f64, 101 | pub depth_buffer: Vec 102 | } 103 | 104 | impl Engine { 105 | pub fn new(field_of_view: f64, projection_width: u32, projection_height: u32) -> Engine { 106 | Engine { 107 | field_of_view: field_of_view, 108 | projection_width: projection_width, 109 | projection_height: projection_height, 110 | projection_distance: (projection_width as f64 / 2.0) / f64::tan(field_of_view / 2.0), 111 | depth_buffer: Vec::with_capacity(projection_width as usize) 112 | } 113 | } 114 | 115 | pub fn render(&mut self, render_buffer: &mut [u8], pitch: usize, map: &mut Map, textures: &Vec, origin_x: f64, origin_y: f64, rotation: f64) { 116 | let light_radius: f32 = 5.0; 117 | 118 | // Cast rays to render map 119 | for x in 0..(self.projection_width as usize) { 120 | // Where on the screen the ray goes through 121 | let ray_screen_x: f64 = -(self.projection_width as f64) / 2.0 + x as f64; 122 | 123 | // The distance from the viewer to the point on the screen 124 | let ray_view_dist = (ray_screen_x.powi(2) + self.projection_distance.powi(2)).sqrt(); 125 | 126 | // Calculate the angle of the ray, and cast it against the map 127 | let ray_angle: f64 = (ray_screen_x / ray_view_dist).asin() + rotation; 128 | let intersection: RayIntersection = self.cast_ray(origin_x, origin_y, ray_angle, &map); 129 | 130 | // Calculate the actual distance 131 | let intersection_distance = intersection.distance.sqrt() * (rotation - ray_angle).cos(); 132 | self.depth_buffer.push(intersection_distance); 133 | 134 | let cell_width = 1.0; 135 | let cell_height = 1.0; 136 | 137 | let cell = map.get_cell(intersection.cell_x, intersection.cell_y) 138 | .expect("There was an intersection, but no cell!"); 139 | 140 | let ref wall_texture: Texture = textures[cell.texture_id as usize]; 141 | let ref ceiling_texture: Texture = textures[map.ceiling_texture_id as usize]; 142 | let ref floor_texture: Texture = textures[map.floor_texture_id as usize]; 143 | 144 | // Calculate the x texel of this wall strip 145 | let wall_texture_x: u32 = if intersection.cell_side == 0 { 146 | (((intersection.y - (intersection.cell_y as f64 * cell_width)) % cell_width) * (wall_texture.width - 1) as f64).round() as u32 147 | } else { 148 | (((intersection.x - (intersection.cell_x as f64 * cell_width)) % cell_width) * (wall_texture.width - 1) as f64).round() as u32 149 | }; 150 | 151 | // Calculate the position and height of the wall strip. 152 | // The wall height is 1 unit, the distance from the player to the screen is viewDist, 153 | // thus the height on the screen is equal to 154 | // wallHeight * viewDist / dist 155 | let line_height: i32 = ((cell_height * self.projection_distance) / intersection_distance).round() as i32; 156 | let line_screen_start: i32 = (self.projection_height as i32 / 2) - (line_height / 2); 157 | let line_screen_end: i32 = line_screen_start + line_height; 158 | 159 | for y in 0..(self.projection_height as usize) { 160 | let offset: usize = (y * pitch) + (x * 4); 161 | 162 | if (y as i32) < line_screen_start { 163 | // Ceiling casting 164 | 165 | let player_height: f64 = 0.5; 166 | let ceiling_row: i32 = (y as i32) - (self.projection_height as i32 / 2); 167 | 168 | let ceiling_straight_distance = (player_height / ceiling_row as f64) * self.projection_distance; 169 | let angle_beta_radians = rotation - ray_angle; 170 | 171 | let ceiling_actual_distance = ceiling_straight_distance / angle_beta_radians.cos(); 172 | 173 | let mut ceiling_hit_x: f64 = origin_x - (ceiling_actual_distance * ray_angle.cos()); 174 | let mut ceiling_hit_y: f64 = origin_y - (ceiling_actual_distance * ray_angle.sin()); 175 | 176 | ceiling_hit_x -= ceiling_hit_x.floor(); 177 | ceiling_hit_y -= ceiling_hit_y.floor(); 178 | 179 | let texture_x: u32 = f64::floor(ceiling_hit_x * (ceiling_texture.width - 1) as f64) as u32; 180 | let texture_y: u32 = f64::floor(ceiling_hit_y * (ceiling_texture.height - 1) as f64) as u32; 181 | 182 | let mut pixel = ceiling_texture.get_pixel(texture_x, texture_y); 183 | let shade: f32 = self.calculate_shade(ceiling_straight_distance.abs() as f32, light_radius); 184 | pixel.r = (pixel.r as f32 * shade) as u8; 185 | pixel.g = (pixel.g as f32 * shade) as u8; 186 | pixel.b = (pixel.b as f32 * shade) as u8; 187 | 188 | render_buffer[offset] = pixel.a; 189 | render_buffer[offset + 1] = pixel.b; 190 | render_buffer[offset + 2] = pixel.g; 191 | render_buffer[offset + 3] = pixel.r; 192 | } 193 | else if ((y as i32) >= line_screen_start) && ((y as i32) < line_screen_end) { 194 | // Wall casting 195 | 196 | let line_y: i32 = y as i32 - line_screen_start; 197 | let texture_y: u32 = f64::floor((line_y as f64 / line_height as f64) * (wall_texture.height - 1) as f64) as u32; 198 | 199 | let mut pixel = wall_texture.get_pixel(wall_texture_x, texture_y); 200 | 201 | let shade: f32 = self.calculate_shade(intersection_distance as f32, light_radius); 202 | pixel.r = (pixel.r as f32 * shade) as u8; 203 | pixel.g = (pixel.g as f32 * shade) as u8; 204 | pixel.b = (pixel.b as f32 * shade) as u8; 205 | 206 | render_buffer[offset] = pixel.a; 207 | render_buffer[offset + 1] = if intersection.cell_side == 1 { pixel.b } else { pixel.g / 2 }; 208 | render_buffer[offset + 2] = if intersection.cell_side == 1 { pixel.g } else { pixel.b / 2 }; 209 | render_buffer[offset + 3] = if intersection.cell_side == 1 { pixel.r } else { pixel.r / 2 }; 210 | } 211 | else if (y as i32) >= line_screen_end { 212 | // Floor casting 213 | 214 | let player_height: f64 = 0.5; 215 | let floor_row: i32 = (y as i32) - (self.projection_height as i32 / 2); 216 | 217 | let floor_straight_distance = (player_height / floor_row as f64) * self.projection_distance; 218 | let angle_beta_radians = rotation - ray_angle; 219 | 220 | let floor_actual_distance = floor_straight_distance / f64::cos(angle_beta_radians); 221 | 222 | let mut floor_hit_x: f64 = origin_x + (floor_actual_distance * f64::cos(ray_angle)); 223 | let mut floor_hit_y: f64 = origin_y + (floor_actual_distance * f64::sin(ray_angle)); 224 | 225 | floor_hit_x -= floor_hit_x.floor(); 226 | floor_hit_y -= floor_hit_y.floor(); 227 | 228 | let texture_x: u32 = f64::floor(floor_hit_x * (floor_texture.width - 1) as f64) as u32; 229 | let texture_y: u32 = f64::floor(floor_hit_y * (floor_texture.height - 1) as f64) as u32; 230 | 231 | let mut pixel = floor_texture.get_pixel(texture_x, texture_y); 232 | let shade: f32 = self.calculate_shade(floor_straight_distance as f32, light_radius); 233 | pixel.r = (pixel.r as f32 * shade) as u8; 234 | pixel.g = (pixel.g as f32 * shade) as u8; 235 | pixel.b = (pixel.b as f32 * shade) as u8; 236 | 237 | render_buffer[offset] = pixel.a; 238 | render_buffer[offset + 1] = pixel.b; 239 | render_buffer[offset + 2] = pixel.g; 240 | render_buffer[offset + 3] = pixel.r; 241 | } 242 | else { 243 | let pixel = COLOR_MAGENTA; 244 | render_buffer[offset] = pixel.a; 245 | render_buffer[offset + 1] = pixel.b; 246 | render_buffer[offset + 2] = pixel.g; 247 | render_buffer[offset + 3] = pixel.r; 248 | } 249 | } 250 | } 251 | 252 | // Sort sprites (far to near) 253 | map.sprites.sort_by(|a, b| { 254 | let a_distance: f64 = (a.x - origin_x).powi(2) + (a.y - origin_y).powi(2); 255 | let b_distance: f64 = (b.x - origin_x).powi(2) + (b.y - origin_y).powi(2); 256 | 257 | b_distance.partial_cmp(&a_distance).unwrap() 258 | }); 259 | 260 | // Render sprites 261 | for sprite in map.sprites.iter() { 262 | let distance_x: f64 = sprite.x - origin_x; 263 | let distance_y: f64 = sprite.y - origin_y; 264 | 265 | // The angle between the player and the sprite 266 | let mut theta: f64 = f64::atan2(distance_y, distance_x); 267 | theta = wrap_angle(theta); 268 | 269 | // The angle between the player and the sprite, relative to the player rotation 270 | let mut gamma: f64 = theta - rotation; 271 | gamma = wrap_angle(gamma); 272 | 273 | let sprite_distance: f64 = f64::sqrt(distance_x.powi(2) + distance_y.powi(2)) * f64::cos(rotation - theta); 274 | let shade: f32 = self.calculate_shade(sprite_distance as f32, light_radius); 275 | 276 | // The number of pixels to offset from the center of the screen 277 | let sprite_pixel_offset: f64 = f64::tan(gamma) * self.projection_distance; 278 | let sprite_screen_x: i32 = f64::round((self.projection_width as f64 / 2.0) + sprite_pixel_offset) as i32; 279 | 280 | let sprite_height: i32 = (f64::round(self.projection_distance / sprite_distance) as i32).wrapping_abs(); 281 | let sprite_width: i32 = (f64::round(self.projection_distance / sprite_distance) as i32).wrapping_abs(); 282 | 283 | if (sprite_height <= 0) || (sprite_width <= 0) { 284 | continue; 285 | } 286 | 287 | let sprite_screen_start_x: i32 = sprite_screen_x - (sprite_width / 2); 288 | let sprite_screen_end_x: i32 = sprite_screen_x + (sprite_width / 2); 289 | let sprite_screen_start_y: i32 = -(sprite_height / 2) + (self.projection_height as i32 / 2); 290 | let sprite_screen_end_y: i32 = (sprite_height / 2) + (self.projection_height as i32 / 2); 291 | 292 | let mut camera_min_angle: f64 = -self.field_of_view / 2.0; 293 | camera_min_angle = wrap_angle(camera_min_angle); 294 | 295 | let mut camera_max_angle: f64 = self.field_of_view / 2.0; 296 | camera_max_angle = wrap_angle(camera_max_angle); 297 | 298 | let ref texture: Texture = textures[sprite.texture_id as usize]; 299 | 300 | for sprite_screen_row in sprite_screen_start_x..sprite_screen_end_x { 301 | if (sprite_screen_row < 0) || (sprite_screen_row >= self.projection_width as i32) { 302 | continue; 303 | } 304 | 305 | // If the sprite is not visible, don't render it. 306 | if ((gamma < camera_min_angle) && (gamma > camera_max_angle)) || 307 | (self.depth_buffer[sprite_screen_row as usize] < sprite_distance) { 308 | continue; 309 | } 310 | 311 | for sprite_screen_col in sprite_screen_start_y..sprite_screen_end_y { 312 | if (sprite_screen_col < 0) || (sprite_screen_col >= self.projection_height as i32) { 313 | continue; 314 | } 315 | 316 | let sprite_row = sprite_screen_row - sprite_screen_start_x; 317 | let sprite_col = sprite_screen_col - sprite_screen_start_y; 318 | 319 | let texture_x: u32 = f64::round((sprite_row as f64 / sprite_width as f64) * (texture.width - 1) as f64) as u32; 320 | let texture_y: u32 = f64::round((sprite_col as f64 / sprite_height as f64) * (texture.height - 1) as f64) as u32; 321 | 322 | let offset = ((sprite_screen_col * pitch as i32) + (sprite_screen_row * 4)) as usize; 323 | let mut pixel = texture.get_pixel(texture_x, texture_y); 324 | 325 | if pixel.a == 0 { 326 | continue; 327 | } 328 | 329 | pixel.r = (pixel.r as f32 * shade) as u8; 330 | pixel.g = (pixel.g as f32 * shade) as u8; 331 | pixel.b = (pixel.b as f32 * shade) as u8; 332 | 333 | render_buffer[offset] = pixel.a; 334 | render_buffer[offset + 1] = pixel.b; 335 | render_buffer[offset + 2] = pixel.g; 336 | render_buffer[offset + 3] = pixel.r; 337 | } 338 | } 339 | } 340 | 341 | self.depth_buffer.clear(); 342 | } 343 | 344 | fn cast_ray(&self, origin_x: f64, origin_y: f64, angle: f64, map: &Map) -> RayIntersection { 345 | // TODO 346 | // There is a bug where this function will crash the program if it never hits a cell. 347 | 348 | let mut intersection_distance: f64 = 0.0; // Distance from origin to intersection 349 | let mut x: f64 = 0.0; // Intersection point x 350 | let mut y: f64 = 0.0; // Intersection point y 351 | let mut cell_x: u32 = 0; // Intersected cell x 352 | let mut cell_y: u32 = 0; // Intersected cell y 353 | let mut cell_edge: u8 = 0; // 0 for y-axis, or 1 for x-axis 354 | 355 | let cell_size: f64 = 1.0; 356 | 357 | // Calculate the quadrant up the ray 358 | let angle = wrap_angle(angle); 359 | let is_ray_right: bool = angle > (TWO_PI * 0.75) || angle < (TWO_PI * 0.25); 360 | let is_ray_up: bool = angle < 0.0 || angle > std::f64::consts::PI; 361 | 362 | // Check for vertical (y axis) intersections 363 | 364 | let mut slope: f64 = angle.sin() / angle.cos(); 365 | let mut delta_x: f64 = if is_ray_right { cell_size } else { -cell_size }; // Horizontal step amount 366 | let mut delta_y: f64 = delta_x * slope; // Vertical step amount 367 | 368 | // Calculate the ray starting position 369 | let mut ray_position_x: f64 = if is_ray_right { f64::ceil(origin_x) } else { f64::floor(origin_x) }; 370 | let mut ray_position_y: f64 = origin_y + (ray_position_x - origin_x) * slope; 371 | 372 | while (ray_position_x >= 0.0) && (ray_position_x < map.width as f64) && (ray_position_y >= 0.0) && (ray_position_y < map.height as f64) { 373 | let tile_map_x: u32 = f64::floor(ray_position_x + (if is_ray_right { 0.0 } else { -cell_size })) as u32; 374 | let tile_map_y: u32 = f64::floor(ray_position_y) as u32; 375 | 376 | if let Some(cell) = map.get_cell(tile_map_x, tile_map_y) { 377 | let mut distance_x: f64 = ray_position_x - origin_x; 378 | let mut distance_y: f64 = ray_position_y - origin_y; 379 | 380 | if cell.texture_id == 6 { 381 | let new_tile_map_x: u32 = f64::floor((ray_position_x + 0.5) + (if is_ray_right { 0.0 } else { -cell_size })) as u32; 382 | let new_tile_map_y: u32 = f64::floor(ray_position_y + (0.5 * slope)) as u32; 383 | 384 | // If we are not still within the door cell 385 | if ((new_tile_map_x != tile_map_x) || (new_tile_map_y != tile_map_y)) { 386 | ray_position_x += delta_x; 387 | ray_position_y += delta_y; 388 | 389 | continue; 390 | } 391 | 392 | ray_position_x += 0.5; 393 | ray_position_y += (0.5 * slope); 394 | 395 | // if door is open at this point 396 | let opened_percent : f64 = ray_position_y - (tile_map_y as f64); 397 | if opened_percent >= 0.25 { 398 | continue; 399 | } 400 | 401 | distance_x = ray_position_x - origin_x; 402 | distance_y = ray_position_y - origin_y; 403 | } 404 | 405 | intersection_distance = distance_x.powi(2) + distance_y.powi(2); 406 | 407 | cell_edge = 0; 408 | 409 | cell_x = cell.x; 410 | cell_y = cell.y; 411 | 412 | x = ray_position_x; 413 | y = ray_position_y; 414 | 415 | break; 416 | } 417 | 418 | ray_position_x += delta_x; 419 | ray_position_y += delta_y; 420 | } 421 | 422 | // Check for horizontal (x axis) intersections 423 | 424 | slope = angle.cos() / angle.sin(); 425 | delta_y = if is_ray_up { -cell_size } else { cell_size }; // Vertical step amount 426 | delta_x = delta_y * slope; // Horizontal step amount 427 | 428 | // Calculate the ray starting position 429 | ray_position_y = if is_ray_up { f64::floor(origin_y) } else { f64::ceil(origin_y) }; 430 | ray_position_x = origin_x + (ray_position_y - origin_y) * slope; 431 | 432 | while (ray_position_x >= 0.0) && (ray_position_x < map.width as f64) && (ray_position_y >= 0.0) && (ray_position_y < map.height as f64) { 433 | let tile_map_x: u32 = f64::floor(ray_position_x) as u32; 434 | let tile_map_y: u32 = f64::floor(ray_position_y + (if is_ray_up { -cell_size } else { 0.0 })) as u32; 435 | 436 | if let Some(cell) = map.get_cell(tile_map_x, tile_map_y) { 437 | let distance_x: f64 = ray_position_x - origin_x; 438 | let distance_y: f64 = ray_position_y - origin_y; 439 | let x_intersection_distance = distance_x.powi(2) + distance_y.powi(2); 440 | 441 | if (intersection_distance == 0.0) || (x_intersection_distance < intersection_distance) { 442 | intersection_distance = x_intersection_distance; 443 | cell_edge = 1; 444 | 445 | cell_x = cell.x; 446 | cell_y = cell.y; 447 | 448 | x = ray_position_x; 449 | y = ray_position_y; 450 | } 451 | 452 | break; 453 | } 454 | 455 | ray_position_x += delta_x; 456 | ray_position_y += delta_y; 457 | } 458 | 459 | RayIntersection { 460 | x: x, 461 | y: y, 462 | cell_x: cell_x, 463 | cell_y: cell_y, 464 | cell_side: cell_edge, 465 | distance: intersection_distance 466 | } 467 | } 468 | 469 | fn calculate_shade(&self, distance: f32, light_radius: f32) -> f32 { 470 | ((light_radius - (distance as f32)) * (1.0 / light_radius)).max(0.0).min(1.0) 471 | } 472 | } 473 | 474 | pub fn wrap_angle(angle: f64) -> f64 { 475 | if angle < 0.0 { 476 | return angle + TWO_PI; 477 | } 478 | else if angle >= TWO_PI { 479 | return angle - TWO_PI; 480 | } 481 | 482 | angle 483 | } 484 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate rustcaster; 2 | extern crate sdl2; 3 | extern crate time; 4 | 5 | use std::str; 6 | use std::path::*; 7 | use rustcaster::*; 8 | use sdl2::pixels::PixelFormatEnum; 9 | use sdl2::surface::*; 10 | use sdl2::image::*; 11 | 12 | pub const WINDOW_TITLE: &'static str = "Rustcaster"; 13 | pub const WINDOW_WIDTH: u32 = 640; 14 | pub const WINDOW_HEIGHT: u32 = 480; 15 | pub const FIELD_OF_VIEW: f64 = 90.0; 16 | 17 | pub const TEXTURE_ID_BARREL: u32 = 0; 18 | pub const TEXTURE_ID_STATUE: u32 = 1; 19 | pub const TEXTURE_ID_GRAVESTONE: u32 = 2; 20 | pub const TEXTURE_ID_WALL: u32 = 3; 21 | pub const TEXTURE_ID_FLOOR: u32 = 4; 22 | pub const TEXTURE_ID_CEILING: u32 = 5; 23 | pub const TEXTURE_ID_DOOR: u32 = 6; 24 | 25 | fn main() { 26 | // Initialize SDL2 27 | let sdl_context = ::sdl2::init().unwrap(); 28 | let sdl_video = sdl_context.video().unwrap(); 29 | 30 | // Create window 31 | let sdl_window = sdl_video.window(WINDOW_TITLE, WINDOW_WIDTH, WINDOW_HEIGHT) 32 | .position_centered() 33 | .opengl() 34 | .build() 35 | .unwrap(); 36 | 37 | // Get the window canvas 38 | let mut canvas = sdl_window 39 | .into_canvas() 40 | .target_texture() 41 | .build() 42 | .unwrap(); 43 | 44 | canvas.set_draw_color(sdl2::pixels::Color { r: 0, g: 0, b: 0, a: 0 }); 45 | 46 | // Create the render texture for the canvas 47 | let texture_creator = canvas.texture_creator(); 48 | let mut render_texture = texture_creator.create_texture_streaming(PixelFormatEnum::RGBA8888, WINDOW_WIDTH, WINDOW_HEIGHT).unwrap(); 49 | 50 | let start_time = time::now(); 51 | let mut last_tick_time = start_time; 52 | let mut render_timer = time::Duration::zero(); 53 | let sixty_hz = time::Duration::nanoseconds(16666667); 54 | 55 | let mut input_left: bool = false; 56 | let mut input_right: bool = false; 57 | let mut input_up: bool = false; 58 | let mut input_down: bool = false; 59 | let mut input_q: bool = false; 60 | let mut input_e: bool = false; 61 | 62 | let move_speed: f64 = 3.5; 63 | let rotation_speed: f64 = f64::to_radians(150.0); 64 | let mut player_x: f64 = 1.5; 65 | let mut player_y: f64 = 1.5; 66 | let mut player_rotation: f64 = 0.0; 67 | 68 | let mut textures: Vec = Vec::new(); 69 | textures.push(load_texture(TEXTURE_ID_BARREL, "res/barrel.png")); 70 | textures.push(load_texture(TEXTURE_ID_STATUE, "res/schindler.png")); 71 | textures.push(load_texture(TEXTURE_ID_GRAVESTONE, "res/gravestone.png")); 72 | textures.push(load_texture(TEXTURE_ID_WALL, "res/wall-stone.png")); 73 | textures.push(load_texture(TEXTURE_ID_FLOOR, "res/floor-tile.png")); 74 | textures.push(load_texture(TEXTURE_ID_CEILING, "res/ceiling-tile.png")); 75 | textures.push(load_texture(TEXTURE_ID_DOOR, "res/door.png")); 76 | 77 | let mut map = load_map("res/maps/temple.png") 78 | .expect("Failed to load map!"); 79 | 80 | let mut engine = Engine::new(FIELD_OF_VIEW.to_radians(), WINDOW_WIDTH, WINDOW_HEIGHT); 81 | 82 | // Engine loop 83 | let mut sdl_event_pump = sdl_context.event_pump().unwrap(); 84 | 'running: loop { 85 | // Calculate elapsed time 86 | let current_time = time::now(); 87 | let elapsed_time = current_time - last_tick_time; 88 | let delta_time: f64 = (elapsed_time.num_nanoseconds().unwrap() as f64) / 1_000_000_000_f64; 89 | render_timer = render_timer + elapsed_time; 90 | 91 | for event in sdl_event_pump.poll_iter() { 92 | use sdl2::event::*; 93 | use sdl2::keyboard::*; 94 | 95 | match event { 96 | // If the window is closed, or ESC is pressed, exit 97 | Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { 98 | break 'running; 99 | }, 100 | 101 | // Game input 102 | Event::KeyDown { keycode: Some(Keycode::Left), .. } | Event::KeyDown { keycode: Some(Keycode::A), .. } => { 103 | input_left = true; 104 | }, 105 | Event::KeyUp { keycode: Some(Keycode::Left), .. } | Event::KeyUp { keycode: Some(Keycode::A), .. } => { 106 | input_left = false; 107 | }, 108 | Event::KeyDown { keycode: Some(Keycode::Right), .. } | Event::KeyDown { keycode: Some(Keycode::D), .. } => { 109 | input_right = true; 110 | }, 111 | Event::KeyUp { keycode: Some(Keycode::Right), .. } | Event::KeyUp { keycode: Some(Keycode::D), .. } => { 112 | input_right = false; 113 | }, 114 | Event::KeyDown { keycode: Some(Keycode::Q), .. } => { 115 | input_q = true; 116 | }, 117 | Event::KeyUp { keycode: Some(Keycode::Q), .. } => { 118 | input_q = false; 119 | }, 120 | Event::KeyDown { keycode: Some(Keycode::E), .. } => { 121 | input_e = true; 122 | }, 123 | Event::KeyUp { keycode: Some(Keycode::E), .. } => { 124 | input_e = false; 125 | }, 126 | Event::KeyDown { keycode: Some(Keycode::Up), .. } | Event::KeyDown { keycode: Some(Keycode::W), .. } => { 127 | input_up = true; 128 | }, 129 | Event::KeyUp { keycode: Some(Keycode::Up), .. } | Event::KeyUp { keycode: Some(Keycode::W), .. } => { 130 | input_up = false; 131 | }, 132 | Event::KeyDown { keycode: Some(Keycode::Down), .. } | Event::KeyDown { keycode: Some(Keycode::S), .. } => { 133 | input_down = true; 134 | }, 135 | Event::KeyUp { keycode: Some(Keycode::Down), .. } | Event::KeyUp { keycode: Some(Keycode::S), .. } => { 136 | input_down = false; 137 | }, 138 | 139 | _ => {} 140 | } 141 | } 142 | 143 | // Calculate velocity based on input 144 | let mut velocity_x: f64 = 0.0; 145 | let mut velocity_y: f64 = 0.0; 146 | 147 | if input_up { 148 | velocity_x += player_rotation.cos() * move_speed; 149 | velocity_y += player_rotation.sin() * move_speed; 150 | } 151 | if input_down { 152 | velocity_x -= player_rotation.cos() * move_speed; 153 | velocity_y -= player_rotation.sin() * move_speed; 154 | } 155 | if input_q { 156 | velocity_x -= f64::cos(player_rotation + (std::f64::consts::PI / 2.0)) * move_speed; 157 | velocity_y -= f64::sin(player_rotation + (std::f64::consts::PI / 2.0)) * move_speed; 158 | } 159 | if input_e { 160 | velocity_x += f64::cos(player_rotation + (std::f64::consts::PI / 2.0)) * move_speed; 161 | velocity_y += f64::sin(player_rotation + (std::f64::consts::PI / 2.0)) * move_speed; 162 | } 163 | if input_left { 164 | player_rotation = wrap_angle(player_rotation - (rotation_speed * delta_time)); 165 | } 166 | if input_right { 167 | player_rotation = wrap_angle(player_rotation + (rotation_speed * delta_time)); 168 | } 169 | 170 | // Apply velocity 171 | if (velocity_x != 0.0) || (velocity_y != 0.0) { 172 | let new_position_x = player_x + (velocity_x * delta_time); 173 | let new_position_y = player_y + (velocity_y * delta_time); 174 | 175 | if map.get_cell(new_position_x.trunc() as u32, player_y.trunc() as u32).is_none() { 176 | player_x = new_position_x; 177 | } 178 | 179 | if map.get_cell(player_x.trunc() as u32, new_position_y.trunc() as u32).is_none() { 180 | player_y = new_position_y; 181 | } 182 | } 183 | 184 | last_tick_time = current_time; 185 | 186 | // Render 187 | if render_timer >= sixty_hz { 188 | render_timer = render_timer - sixty_hz; 189 | 190 | canvas.clear(); 191 | 192 | render_texture.with_lock(None, |buffer: &mut [u8], pitch: usize| { 193 | engine.render(buffer, pitch, &mut map, &textures, player_x, player_y, player_rotation); 194 | }).unwrap(); 195 | 196 | canvas.copy_ex(&render_texture, None, None, 0.0, None, false, false).unwrap(); 197 | canvas.present(); 198 | } 199 | } 200 | } 201 | 202 | pub fn load_texture(texture_id: u32, file_name: &str) -> Texture { 203 | let surface = Surface::from_file(Path::new(file_name)).unwrap(); 204 | let mut pixels: Vec = Vec::new(); 205 | pixels.resize((surface.width() * surface.height()) as usize, COLOR_MAGENTA); 206 | 207 | surface.with_lock(|surface_buffer: &[u8]| { 208 | for x in 0..surface.width() { 209 | for y in 0..surface.height() { 210 | let texture_pixel_index = 211 | (y as usize * surface.pitch() as usize) + 212 | (x as usize * surface.pixel_format_enum().byte_size_per_pixel()); 213 | 214 | let color = Color { 215 | r: surface_buffer[texture_pixel_index], 216 | g: surface_buffer[texture_pixel_index + 1], 217 | b: surface_buffer[texture_pixel_index + 2], 218 | a: surface_buffer[texture_pixel_index + 3] 219 | }; 220 | 221 | pixels[((y * surface.width()) + x) as usize] = color; 222 | } 223 | } 224 | }); 225 | 226 | Texture::new(texture_id, surface.width(), surface.height(), pixels) 227 | } 228 | 229 | pub fn load_map(file_name: &str) -> std::io::Result { 230 | let texture: Texture = load_texture(0, file_name); 231 | 232 | let mut cells: Vec> = Vec::new(); 233 | cells.resize((texture.width * texture.height) as usize, None); 234 | 235 | let mut sprites: Vec = Vec::new(); 236 | 237 | for x in 0..texture.width { 238 | for y in 0..texture.height { 239 | let index: usize = ((y * texture.width) + x) as usize; 240 | let pixel: Color = texture.pixels[index]; 241 | 242 | if pixel == COLOR_BLACK { 243 | let cell: Cell = Cell {x: x as u32, y: y as u32, texture_id: TEXTURE_ID_WALL}; // Wall 244 | cells[index] = Some(cell); 245 | } 246 | else if pixel == COLOR_WHITE { 247 | let cell: Cell = Cell {x: x as u32, y: y as u32, texture_id: TEXTURE_ID_DOOR}; 248 | cells[index] = Some(cell); 249 | } 250 | else if pixel == COLOR_RED { 251 | sprites.push(Sprite::new(x as f64 + 0.5, y as f64 + 0.5, TEXTURE_ID_STATUE)); 252 | } 253 | else if pixel == COLOR_GREEN { 254 | sprites.push(Sprite::new(x as f64 + 0.5, y as f64 + 0.5, TEXTURE_ID_BARREL)); 255 | } 256 | else if pixel == COLOR_BLUE { 257 | sprites.push(Sprite::new(x as f64 + 0.5, y as f64 + 0.5, TEXTURE_ID_GRAVESTONE)); 258 | } 259 | } 260 | } 261 | 262 | Ok(Map { 263 | width: texture.width, 264 | height: texture.height, 265 | floor_texture_id: TEXTURE_ID_FLOOR, 266 | ceiling_texture_id: TEXTURE_ID_CEILING, 267 | cells: cells, 268 | sprites: sprites 269 | }) 270 | } 271 | --------------------------------------------------------------------------------