// This file is part of ZiRC // // Copyright (C) 2021, tslil clingman // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . const std = @import("std"); const RenderWindow = @import("sfml").graphics.RenderWindow; const Sprite = @import("sfml").graphics.Sprite; usingnamespace @import("map.zig"); pub const RenderWallFunction: type = fn ( window: RenderWindow, sprite: Sprite, col: i32, // which column we're in top: f32, // top of wall draw_frac: f32, length: f32, // length of slice to be drawn texfrac: f32, texture: u8, // which texture index ) void; pub const Player = struct { pos_x: f32, pos_y: f32, ang: f32, vel_x: f32 = 0, vel_y: f32 = 0, acc_x: f32 = 0, acc_y: f32 = 0, fov: f32 = std.math.pi / 3.0, height: f32 = 1.7, // TODO plane_height: f32, plane_width: i32, plane_dist: f32, pub fn new(pos_x: f32, pos_y: f32, ang: f32, plane_width: i32, plane_height: i32) Player { const fov: f32 = std.math.pi / 3.0; return Player{ // standing still at the given location, looking in direction ang, .pos_x = pos_x, .pos_y = pos_y, .ang = ang, // plane of projection .plane_width = plane_width, .plane_height = @intToFloat(f32, plane_height), // given the desired width of the image, how far away must // the projection plane be from the camera? .plane_dist = @intToFloat(f32, plane_width) / (2 * std.math.tan(fov / 2)), }; } pub fn tick(self: *Player) void { const dt = 1 / 30.0; const v_min = 0.8; const v_decay = 1.25; self.pos_x += self.vel_x * dt; self.pos_y += self.vel_y * dt; self.vel_x /= v_decay; if (std.math.fabs(self.vel_x) < v_min) self.vel_x = 0; self.vel_y /= v_decay; if (std.math.fabs(self.vel_y) < v_min) self.vel_y = 0; self.vel_x += self.acc_x * dt; self.vel_y += self.acc_y * dt; } pub fn renderMapUsing( self: Player, window: RenderWindow, wallSprite: Sprite, map: Map, // the abstract the rendering call renderWall: RenderWallFunction, ) void { self.renderWalls(window, wallSprite, map, renderWall); } fn renderWalls(self: Player, window: RenderWindow, wallSprite: Sprite, map: Map, renderWall: RenderWallFunction) void { const floor = std.math.floor; var col: i32 = 0; while (col < self.plane_width) : (col += 1) { const horiz_frac = @intToFloat(f32, col) / (@intToFloat(f32, self.plane_width) - 1); const ra = (0.5 - horiz_frac) * self.fov + self.ang; const cosra = std.math.cos(ra); const sinra = std.math.sin(ra); // Observe that sqrt(1+tan^2) = abs(1/cos) sqrt(cos^2+sin^2) = // abs(1/cos). Similarly so for cot, hence we obtain the following // lengths for the hypotenuses assuming that x (respectively y) are // unit length and the angle is ra. const dy_for_x_step = std.math.fabs(1 / cosra); const dx_for_y_step = std.math.fabs(1 / sinra); var step_x: i32 = -1; var step_y: i32 = -1; var dist_x: f32 = undefined; var dist_y: f32 = undefined; var ipos_x: i32 = @floatToInt(i32, floor(self.pos_x)); var ipos_y: i32 = @floatToInt(i32, floor(self.pos_y)); // looking right if (cosra >= 0) { step_x = 1; // assuming unit size grid cells dist_y = (@intToFloat(f32, ipos_x) + 1 - self.pos_x) * dy_for_x_step; } else { dist_y = (self.pos_x - @intToFloat(f32, ipos_x)) * dy_for_x_step; } if (sinra >= 0) { step_y = 1; dist_x = (@intToFloat(f32, ipos_y) + 1 - self.pos_y) * dx_for_y_step; } else { dist_x = (self.pos_y - @intToFloat(f32, ipos_y)) * dx_for_y_step; } var distance: f32 = 0; var still_drawing = true; var highest_point: f32 = 0; var horizontal_hit: bool = undefined; while (still_drawing) { // Find the next cell on our path if (dist_y < dist_x) { horizontal_hit = false; distance = dist_y; dist_y += dy_for_x_step; ipos_x += step_x; } else { horizontal_hit = true; distance = dist_x; dist_x += dx_for_y_step; ipos_y += step_y; } if (!map.inBounds(ipos_x, ipos_y)) break; var cell = map.lookup(ipos_x, ipos_y); // the correct distance is the shortest distance from the plane // of projection to the point, that is, perpendicular distance const perp_distance = distance * std.math.cos(self.ang - ra); // project the top of the wall const top = self.plane_height / 2 + self.plane_dist * (cell.height - self.height) / perp_distance; // We have a wall to draw if it protrudes above what we have so far drawn if (top > highest_point) { // did we extend beyond the top of the plane? if (top > self.plane_height) still_drawing = false; // compute the height of this wall const total_length = self.plane_dist * cell.height / perp_distance; // as well as the fraction we'll be drawing const draw_frac = std.math.min(1, (top - highest_point) / total_length); // we need the raw Euclidean distance to calculate the // fractional part of the relevant coordinate for texture // mapping of the walls const hit_coordinate = if (horizontal_hit) distance * cosra + self.pos_x else distance * sinra + self.pos_y; var texfrac = std.math.modf(hit_coordinate).fpart; // we also want to be sure that we're consistently orienting // textures, in this case clockwise if ((horizontal_hit and sinra < 0) or (!horizontal_hit and cosra > 0)) texfrac = 1 - texfrac; renderWall(window, wallSprite, col, top, total_length, draw_frac, texfrac, cell.wall_texture); highest_point = top; } } } } };