From ff054cad993bb89a2179358a71681ce45a9cccf3 Mon Sep 17 00:00:00 2001 From: tslil clingman <> Date: Thu, 16 Sep 2021 13:02:52 -0400 Subject: Corrected movement --- src/player.zig | 480 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 src/player.zig (limited to 'src/player.zig') diff --git a/src/player.zig b/src/player.zig new file mode 100644 index 0000000..6082b74 --- /dev/null +++ b/src/player.zig @@ -0,0 +1,480 @@ +// 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; +const Texture = @import("sfml").graphics.Texture; +const Image = @import("sfml").graphics.Image; +const Colour = @import("sfml").graphics.Color; + +const level = @import("level.zig"); +const constants = @import("constants.zig"); + +fn renderSlice( + window: RenderWindow, + sprite: Sprite, + col: i32, // which column + top: f32, // top of wall + height: f32, // how tall + draw_frac: f32, // how much to draw, as a fraction; > 1 means repeat texture + texfrac: f32, // how far along the texture + texture: u8, // which texture index +) void { + // we need ceil here so that we draw always to or past the edge of the screen + const draw_height = @floatToInt(c_int, std.math.ceil(draw_frac * constants.TextureDim)); + const total_height = height * constants.VFact; + + const xpos = @intToFloat(f32, col) * constants.HFact; + const ypos = constants.ScreenHeight / 2 + (constants.PlaneHeight / 2 - top) * constants.VFact; + + const tind = texture * @floatToInt(c_int, constants.TextureDim); + const left = tind + @floatToInt(c_int, texfrac * constants.TextureDim); + + sprite.setPosition(.{ .x = xpos, .y = ypos }); + sprite.setScale(.{ .x = constants.HFact, .y = total_height / constants.TextureDim }); + sprite.setTextureRect(.{ .top = 0, .left = left, .width = 1, .height = draw_height }); + window.draw(sprite, null); +} + +fn playerDistComp(pos: [2]f32, lhs: level.Object, rhs: level.Object) bool { + const lx = lhs.pos_x - pos[0]; + const ly = lhs.pos_y - pos[1]; + const rx = rhs.pos_x - pos[0]; + const ry = rhs.pos_y - pos[1]; + + return (lx * lx + ly * ly > rx * rx + ry * ry); +} + +pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { + const FOV: f32 = std.math.pi / 3.0; + const PlanePixels = PlaneWidth * PlaneHeight; + // given the desired width of the image, how far away must + // the projection plane be from the camera? + const FOV_SCALE = 2 * std.math.tan(FOV / 2); + const PlaneDist = PlaneWidth / FOV_SCALE; + + return 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, + height: f32 = 2.0 * (1.8 / 2.5), // TODO + z_buffer: std.BoundedArray(f32, PlanePixels), + + anim_step: f32 = 0, + + // standing still at the given location, looking in direction ang + pub fn new(pos_x: f32, pos_y: f32, ang: f32) !@This() { + const infs = [_]f32{std.math.inf(f32)} ** PlanePixels; + return Player(PlaneWidth, PlaneHeight){ + .pos_x = pos_x, + .pos_y = pos_y, + .ang = ang, + // TODO: is there some clever way to avoid this long name? + .z_buffer = try std.BoundedArray(f32, PlanePixels).fromSlice(&infs), + }; + } + + pub fn tick(self: *@This(), map: level.Map) void { + const dt = 1 / 30.0; + const v_min = 0.8; + const v_decay = 1 / 1.25; + + var next_x = self.pos_x + self.vel_x * dt; + var next_y = self.pos_y + self.vel_y * dt; + + // Collision detection + const min_dist: f32 = 0.1; + const fx = std.math.floor(self.pos_x); + const fy = std.math.floor(self.pos_y); + const ix = @floatToInt(i32, fx); + const iy = @floatToInt(i32, fy); + const nix = @floatToInt(i32, std.math.floor(next_x + if (self.vel_x > 0) min_dist else -min_dist)); + const niy = @floatToInt(i32, std.math.floor(next_y + if (self.vel_y > 0) min_dist else -min_dist)); + + if (!map.inBounds(nix, iy) or map.lookup(nix, iy).height > 0) { + next_x = fx + if (self.vel_x > 0) 1 - min_dist else min_dist; + self.vel_x = 0; + self.acc_x = 0; + } + + if (!map.inBounds(ix, niy) or map.lookup(ix, niy).height > 0) { + next_y = fy + if (self.vel_y > 0) 1 - min_dist else min_dist; + self.vel_y = 0; + self.acc_y = 0; + } + + // Update position + self.pos_x = next_x; + self.pos_y = next_y; + + // Update velocity + self.vel_x += self.acc_x * dt; + self.vel_y += self.acc_y * dt; + + const vd = v_decay * std.math.sqrt(self.vel_x * self.vel_x + self.vel_y * self.vel_y); + if (vd < v_min) { + self.vel_x = 0; + self.vel_y = 0; + } else { + // If we're moving update our animation state + self.anim_step += 1; + self.height -= std.math.sin(self.anim_step / 10 * std.math.pi) * 0.015; + + self.vel_x *= v_decay; + self.vel_y *= v_decay; + } + } + + pub fn renderWorld( + self: *@This(), + window: RenderWindow, + walls_sprite: Sprite, + objects_sprite: Sprite, + surfaces_image: Image, + rendered_surfaces_texture: Texture, + rendered_surfaces_sprite: Sprite, + map: level.Map, + ) !void { + // Fist reset the z_buffer + var i: usize = 0; + while (i < self.z_buffer.len) : (i += 1) { + self.z_buffer.set(i, std.math.inf(f32)); + } + + var pixels = [_]Colour{Colour.fromRGBA(0, 0, 0, 0)} ** (PlaneWidth * PlaneHeight); + + // then draw all the walls and populate the z_buffer, while also + // rendering the surfaces below the horizon to the pixel array + self.renderCells(window, walls_sprite, surfaces_image, map, &pixels); + + // then render the ceilings to our pixel array + self.renderCeilingsToTexture(surfaces_image, map, &pixels); + + // we're now ready to draw the surfaces + try rendered_surfaces_texture.updateFromPixels(&pixels, null); + window.draw(rendered_surfaces_sprite, null); + + // use the z_buffer to render sprites + self.renderObjects(window, objects_sprite, map); + } + + fn renderObjects( + self: @This(), + window: RenderWindow, + objects_sprite: Sprite, + map: level.Map, + ) void { + std.sort.sort(level.Object, map.objects.items, + // Wow, context with an arbitrary type! No macros, just + // Zig all the way down! + [2]f32{ self.pos_x, self.pos_y }, playerDistComp); + + const self_cos = std.math.cos(self.ang); + const self_sin = std.math.sin(self.ang); + + for (map.objects.items) |obj| { + const ox = obj.pos_x - self.pos_x; + const oy = obj.pos_y - self.pos_y; + + // We compute the two coordinates of rotating by -self.ang, the + // first of which gives the perpendicular distance to the plane + // of projection, and the second of which gives the + // (unprojected) centre of the object. + const perp_distance = self_cos * ox + self_sin * oy; + const centre = self_sin * ox - self_cos * oy; + + // NOTE: in the below we have applied the magic scaling factor + // of FOV_SCALE. I don't understand how this compensates for the + // linear interpolation incorrectness we do elsewhere, but + // somehow it scales the *correct* values we compute above into + // whatever agrees with the wall and floor rendering voodoo. + + // This quantity is independent of FOV_SCALE because it enters + // both via centre and perp_distance + const proj_centre = PlaneWidth / 2 + PlaneDist * centre / perp_distance; + + // Here's the magic adjustment + const scaled_perp_distance = FOV_SCALE * perp_distance; + const width = PlaneDist * obj.width / scaled_perp_distance; + const left = proj_centre - width / 2; + + // TODO: prune before this? + if (left + width < 0 or left >= PlaneWidth) continue; + + const height = PlaneDist * obj.height / scaled_perp_distance; + const top = PlaneHeight / 2 + PlaneDist * (obj.height - self.height + obj.pos_z) / scaled_perp_distance; + + // TODO: likewise? + if (top < 0 or top - height >= PlaneHeight) continue; + + // Something is on the screen, let's draw it! + const start = std.math.max(0, left); + const end = @floatToInt(i32, std.math.min(left + width, PlaneWidth - 1)); + + var tex_frac: f32 = std.math.clamp((start - left) / width, 0, 1); + var col: i32 = @floatToInt(i32, start); + const tex_frac_step = 1 / width; + while (col < end) : ({ + col += 1; + tex_frac += tex_frac_step; + }) { + var bottom = std.math.min(top, PlaneHeight); + while (bottom >= 0 and bottom >= top - height) : (bottom -= 1) { + const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + @floatToInt(i32, bottom)); + if (self.z_buffer.get(index) < scaled_perp_distance) { + bottom += 1; + break; + } + } + const draw_frac = std.math.clamp((top - bottom) / height, 0, 1); + renderSlice(window, objects_sprite, col, top, height, draw_frac, tex_frac, obj.texture); + } + } + } + + fn renderCells( + self: *@This(), + window: RenderWindow, + walls_sprite: Sprite, + floors_image: Image, + map: level.Map, + pixels: []Colour, + ) void { + // This is a TERRIBLE hack: for whatever reason *linearly* + // interpolating on the direction vectors gives + // perspective-correct-seeming walls! + const cos_first = std.math.cos(self.ang + 0.5 * FOV); + const cos_last = std.math.cos(self.ang - 0.5 * FOV); + const sin_first = std.math.sin(self.ang + 0.5 * FOV); + const sin_last = std.math.sin(self.ang - 0.5 * FOV); + + const cos_step = (cos_last - cos_first) / PlaneWidth; + const sin_step = (sin_last - sin_first) / PlaneWidth; + + var col: u16 = 0; + var cosra = cos_first; + var sinra = sin_first; + while (col < PlaneWidth) : ({ + col += 1; + cosra += cos_step; + sinra += sin_step; + }) { + // 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. This + // for whatever reasons still works when we linearly interpolate + // on cos and sin! + 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, std.math.floor(self.pos_x)); + var ipos_y: i32 = @floatToInt(i32, std.math.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 top: f32 = undefined; + var distance: f32 = 0; + var still_drawing = true; + var highest_point: f32 = 0; + var horizontal_hit: bool = undefined; + while (still_drawing and map.inBounds(ipos_x, ipos_y)) : ({ + // 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; + } + }) { + const cell = map.lookup(ipos_x, ipos_y); + + // Is there a wall? + if (cell.height > 0) { + // project the top of the wall + top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; + + // Does the wall extend above what we've draw? + if (top > highest_point) { + + // If we reach the top we have to stop! + if (top > PlaneHeight) { + still_drawing = false; + } + + // compute the height of this wall + const total_length = PlaneDist * cell.height / distance; + + // as well as the fraction we'll be drawing + const draw_length = top - highest_point; + const draw_frac = cell.height * std.math.clamp(draw_length / total_length, 0, 1); + + // we need the 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; + + // draw the wall + renderSlice(window, walls_sprite, col, top, total_length / cell.height, draw_frac, texfrac, cell.wall_texture); + + // record that there's a wall here in the z_buffer + var y = @floatToInt(i32, std.math.min(top, PlaneHeight - 1)); + while (y > @floatToInt(i32, highest_point)) : (y -= 1) { + const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + y); + self.z_buffer.set(index, distance); + } + highest_point = top; + } + } + + // do we potentially draw the top of this cell? + if (highest_point < PlaneHeight / 2) { + if (dist_y < dist_x) { + distance = dist_y; + } else { + distance = dist_x; + } + + // Note: next_top can never exceed PlaneHeight / 2 + // in the body of the next block. If the wall is + // taller than us the back edge is lower than the + // front one so this check will fail as we just drew + // it (or higher than it). If the wall is shorter + // then the back edge is at most the horizon. + const next_top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; + + // only if we can see some part of it + if (next_top > highest_point) { + top = highest_point; + while (top <= next_top and top < PlaneHeight / 2) : (top += 1) { + const row_dist = (self.height - cell.height) * PlaneDist / (PlaneHeight / 2 - top); + const ptop = @floatToInt(usize, top + 1); + const itop = @floatToInt(usize, PlaneHeight) - ptop; + + // draw the correct pixel + const sx = std.math.modf(self.pos_x + row_dist * cosra); + const sy = std.math.modf(self.pos_y + row_dist * sinra); + const toff = cell.floor_texture * @floatToInt(c_uint, constants.TextureDim); + const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart)); + const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart)); + const val = floors_image.getPixel(.{ .x = toff + px, .y = py }); + pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val; + + // record in the z_buffer only if we're above the floor! + if (cell.height > 0) { + const index = col * @floatToInt(usize, PlaneHeight) + ptop; + self.z_buffer.set(index, row_dist); + } + } + highest_point = next_top; + } + } + } + } + } + + fn renderCeilingsToTexture( + self: @This(), + surfaces_image: Image, + map: level.Map, + pixels: []Colour, + ) void { + // Again, another TERRIBLE hack: we do the same nasty linear + // interpolation trick and for whatever reason the floors look fine. + const cos_first = std.math.cos(self.ang + 0.5 * FOV); + const sin_first = std.math.sin(self.ang + 0.5 * FOV); + const cos_last = std.math.cos(self.ang - 0.5 * FOV); + const sin_last = std.math.sin(self.ang - 0.5 * FOV); + + var row: usize = 0; + while (row < PlaneHeight / 2) : (row += 1) { + const frow = (PlaneHeight / 2 - @intToFloat(f32, row)); + const row_dist = (constants.MAX_HEIGHT - self.height) * PlaneDist / frow; + + const dx_step = row_dist * (cos_last - cos_first) / PlaneWidth; + const dy_step = row_dist * (sin_last - sin_first) / PlaneWidth; + + var col: usize = 0; + var dx = row_dist * cos_first; + var dy = row_dist * sin_first; + + while (col < PlaneWidth) : ({ + col += 1; + dx += dx_step; + dy += dy_step; + }) { + const x = self.pos_x + dx; + const y = self.pos_y + dy; + + const sx = std.math.modf(x); + const sy = std.math.modf(y); + + const ix = @floatToInt(i32, sx.ipart); + const iy = @floatToInt(i32, sy.ipart); + + const index = col * @floatToInt(usize, PlaneHeight) + @floatToInt(usize, PlaneHeight - 1) - row; + if (map.inBounds(ix, iy) and row_dist < self.z_buffer.get(index)) { + const cell = map.lookup(ix, iy); + const toff = cell.ceiling_texture * @floatToInt(c_uint, constants.TextureDim); + const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart)); + const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart)); + + const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py }); + + pixels[row * @floatToInt(usize, PlaneWidth) + col] = val; + } + } + } + } + }; +} -- cgit v1.3.1