From 880cf8ab4e94e9b0551d724c7e3a39a870d70741 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Fri, 30 Jan 2026 03:48:02 +0200 Subject: [PATCH] implement collisions --- libs/tiled/src/layer.zig | 84 ++++--- libs/tiled/src/property.zig | 9 + libs/tiled/src/tilemap.zig | 20 ++ src/assets.zig | 2 +- src/assets/map.tmx | 193 ++++++++++++--- src/engine/frame.zig | 39 +-- src/engine/input.zig | 97 ++++---- src/engine/math.zig | 14 +- src/engine/root.zig | 23 +- src/game.zig | 444 +++++++++++++++++++++++++++++++--- src/raycast_tile_iterator.zig | 97 ++++++++ 11 files changed, 841 insertions(+), 181 deletions(-) create mode 100644 src/raycast_tile_iterator.zig diff --git a/libs/tiled/src/layer.zig b/libs/tiled/src/layer.zig index 970550e..184380a 100644 --- a/libs/tiled/src/layer.zig +++ b/libs/tiled/src/layer.zig @@ -8,6 +8,46 @@ const Position = @import("./position.zig"); const Layer = @This(); +pub const Bounds = struct { + left: i32, + right: i32, + top: i32, + bottom: i32, + + pub const zero = Bounds{ + .left = 0, + .right = 0, + .top = 0, + .bottom = 0, + }; + + pub fn initFromRect(x: i32, y: i32, width: i32, height: i32) Bounds { + return Bounds{ + .left = x, + .right = x + width, + .top = y, + .bottom = y + height, + }; + } + + pub fn getWidth(self: Bounds) u32 { + return @intCast(self.right - self.left + 1); + } + + pub fn getHeight(self: Bounds) u32 { + return @intCast(self.bottom - self.top + 1); + } + + pub fn combine(lhs: Bounds, rhs: Bounds) Bounds { + return Bounds{ + .left = @min(lhs.left, rhs.left), + .right = @max(lhs.right, rhs.right), + .top = @min(lhs.top, rhs.top), + .bottom = @max(lhs.bottom, rhs.bottom) + }; + } +}; + pub const TileVariant = struct { pub const Chunk = struct { x: i32, @@ -15,6 +55,10 @@ pub const TileVariant = struct { width: u32, height: u32, data: []u32, + + pub fn getBounds(self: Chunk) Bounds { + return Bounds.initFromRect(self.x, self.y, @intCast(self.width), @intCast(self.height)); + } }; pub const Data = union(enum) { @@ -143,48 +187,18 @@ pub const TileVariant = struct { return result; } - const Bounds = struct { - left: i32, - right: i32, - top: i32, - bottom: i32, - - pub fn width(self: Bounds) u32 { - return @intCast(self.right - self.left); - } - - pub fn height(self: Bounds) u32 { - return @intCast(self.bottom - self.top); - } - }; - pub fn getBounds(self: TileVariant) Bounds { if (self.data == .fixed) { - return Bounds{ - .left = 0, - .right = @intCast(self.width), - .top = 0, - .bottom = @intCast(self.height), - }; + return Bounds.initFromRect(0, 0, @intCast(self.width), @intCast(self.height)); + } else if (self.data == .chunks) { const chunks = self.data.chunks; - var result = Bounds{ - .left = 0, - .right = 0, - .top = 0, - .bottom = 0 - }; + var result: Bounds = .zero; if (chunks.len > 0) { - result.left = chunks[0].x; - result.right = chunks[0].x + @as(i32, @intCast(chunks[0].width)); - result.top = chunks[0].y; - result.bottom = chunks[0].y + @as(i32, @intCast(chunks[0].height)); + result = chunks[0].getBounds(); for (chunks[1..]) |chunk| { - result.left = @min(result.left, chunk.x); - result.right = @max(result.right, chunk.x + @as(i32, @intCast(chunk.width))); - result.top = @min(result.top, chunk.y); - result.bottom = @max(result.bottom, chunk.y + @as(i32, @intCast(chunk.height))); + result = result.combine(chunk.getBounds()); } } diff --git a/libs/tiled/src/property.zig b/libs/tiled/src/property.zig index 1fa29df..19af15b 100644 --- a/libs/tiled/src/property.zig +++ b/libs/tiled/src/property.zig @@ -74,6 +74,15 @@ pub const List = struct { } return null; } + + pub fn getBool(self: List, name: []const u8) ?bool { + if (self.get(name)) |value| { + if (value == .bool) { + return value.bool; + } + } + return null; + } }; pub fn init(name: []const u8, value: Value) Property { diff --git a/libs/tiled/src/tilemap.zig b/libs/tiled/src/tilemap.zig index 14d6bc4..3bf4e47 100644 --- a/libs/tiled/src/tilemap.zig +++ b/libs/tiled/src/tilemap.zig @@ -241,6 +241,26 @@ pub fn getTileByPosition(self: *const Tilemap, layer: *const Layer, tilesets: Ti }; } +pub fn getTileBounds(self: *const Tilemap) Layer.Bounds { + var result: ?Layer.Bounds = null; + + for (self.layers) |layer| { + if (layer.variant != .tile) { + continue; + } + + const layer_bounds = layer.variant.tile.getBounds(); + if (result == null) { + result = layer_bounds; + } else { + result = layer_bounds.combine(result.?); + } + } + + + return result orelse .zero; +} + pub fn deinit(self: *const Tilemap) void { self.arena.deinit(); } diff --git a/src/assets.zig b/src/assets.zig index e53a2e8..119f1f0 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -84,7 +84,7 @@ pub fn init(gpa: std.mem.Allocator) !Assets { ); var tilesets: tiled.Tileset.List = .empty; - try tilesets.add(gpa, "tileset.tsx", tileset); + try tilesets.add(gpa, "tilemap.tsx", tileset); const players_image = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Players/tilemap_packed.png")); defer players_image.deinit(); diff --git a/src/assets/map.tmx b/src/assets/map.tmx index e03bc97..302035f 100644 --- a/src/assets/map.tmx +++ b/src/assets/map.tmx @@ -1,8 +1,7 @@ - - - - + + + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -18,9 +17,9 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,325, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,343, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,343 +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -35,15 +34,15 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -326,326,326,326,326,326,326,327,0,0,0,0,0,0,0,0, -344,254,344,344,344,344,344,345,0,0,0,0,0,0,0,0, -344,344,344,344,254,344,344,345,0,0,0,0,0,0,0,0 +0,0,0,0,0,0,0,0,65,65,0,0,0,0,0,0, +92,92,92,92,92,92,92,93,65,65,0,0,0,0,0,0, +110,20,110,110,110,110,110,112,110,65,0,0,0,0,0,0, +110,110,110,110,20,110,110,110,110,65,65,0,0,0,0,0 -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,343, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,343, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,361, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -59,20 +58,20 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -344,344,254,344,344,344,344,345,0,0,0,0,0,0,0,0, -344,254,344,344,344,344,254,345,0,0,0,0,0,0,0,0, -362,362,362,362,362,362,362,363,0,0,0,0,299,299,0,0, -0,0,0,299,299,299,299,299,0,299,299,299,429,299,0,0, -0,0,0,299,299,299,299,299,299,299,299,299,299,299,0,0, -0,0,299,299,299,299,299,299,299,299,299,299,300,299,0,0, -0,0,299,299,299,299,300,299,299,300,299,299,299,0,0,0, -0,0,0,299,299,299,299,299,299,299,300,299,299,0,0,0, -0,0,0,299,299,429,299,299,299,299,299,299,299,299,0,299, -0,0,0,299,299,299,299,300,299,299,299,299,299,299,299,299, -0,0,0,299,299,299,299,299,299,300,299,299,429,299,299,299, -0,0,0,299,299,299,299,299,299,299,299,299,299,299,300,299, -0,299,299,299,299,299,300,299,299,299,299,299,299,299,299,299, -0,0,299,299,299,299,299,299,299,299,299,299,299,299,299,299, +110,110,20,110,110,110,110,94,110,65,65,0,0,0,65,0, +110,20,110,110,110,110,20,111,65,65,65,0,0,0,65,0, +128,128,128,128,128,128,128,129,65,65,65,0,65,65,65,0, +0,0,0,65,65,65,65,65,65,65,65,65,195,65,65,0, +0,0,0,65,65,65,65,65,65,65,65,65,65,65,65,0, +0,0,65,65,65,65,65,65,65,65,65,65,66,65,65,65, +0,0,65,65,65,65,66,65,65,66,65,65,65,65,65,65, +0,0,0,65,65,65,65,65,65,65,66,65,65,65,65,65, +0,0,0,65,65,195,65,65,65,65,65,65,65,65,65,65, +0,0,0,65,65,65,65,66,65,65,65,65,65,65,65,65, +0,0,0,65,65,65,65,65,65,66,65,65,195,65,65,65, +0,0,0,65,65,65,65,65,65,65,65,65,65,65,66,65, +0,65,65,65,65,65,66,65,65,65,65,65,65,65,65,65, +0,0,65,65,65,65,65,65,65,65,65,65,65,65,65,65, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 @@ -82,15 +81,137 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0, +65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,0, +65,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0, +65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,1,2,2,2,2,3,0,0, +0,0,0,0,0,0,0,0,19,110,20,110,20,21,0,0, +0,0,0,0,0,0,0,0,19,110,110,20,20,21,0,0, +0,0,0,0,0,0,0,0,37,38,38,38,38,39,0,0, +0,0,0,0,0,0,0,0,73,56,174,174,174,57,0,0, +0,0,0,0,0,0,0,0,73,174,174,56,174,57,0,0, +0,0,0,0,0,0,0,0,73,174,174,174,174,57,0,0 + + +0,0,0,0,0,0,0,0,73,174,56,56,174,57,0,0, +0,0,0,0,0,0,0,0,73,174,174,174,56,57,0,0, +0,0,0,0,0,0,0,0,73,74,74,74,74,75,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2, +0,0,0,0,0,0,0,0,0,0,0,0,0,37,38,38 + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +2,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0, +38,38,38,39,0,0,0,0,0,0,0,0,0,0,0,0 + + +0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174, +0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174, +0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,174, +0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,56, +0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,74, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + +56,56,174,75,0,0,0,0,0,0,0,0,0,0,0,0, +174,174,174,75,0,0,0,0,0,0,0,0,0,0,0,0, +174,174,56,75,0,0,0,0,0,0,0,0,0,0,0,0, +174,56,56,75,0,0,0,0,0,0,0,0,0,0,0,0, +74,74,74,75,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,299,299,299,0,0,0,0,0,0,0,0,0,0, -299,299,299,299,299,299,0,0,0,0,0,0,0,0,0,0, -0,0,299,299,299,299,299,0,0,0,0,0,0,0,0,0, -299,299,299,299,299,299,299,0,0,0,0,0,0,0,0,0, -299,299,299,299,299,299,0,0,0,0,0,0,0,0,0,0, -299,299,299,299,299,0,0,0,0,0,0,0,0,0,0,0, -299,299,299,0,0,0,0,0,0,0,0,0,0,0,0,0, -299,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 diff --git a/src/engine/frame.zig b/src/engine/frame.zig index 59a83bd..4b4f2bf 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -4,7 +4,7 @@ const log = std.log.scoped(.engine); const InputSystem = @import("./input.zig"); const KeyCode = InputSystem.KeyCode; -const Mouse = InputSystem.Mouse; +const MouseButton = InputSystem.MouseButton; const AudioSystem = @import("./audio/root.zig"); const AudioData = AudioSystem.Data; @@ -27,19 +27,14 @@ pub const Nanoseconds = u64; const Frame = @This(); pub const Input = struct { - down_keys: std.EnumSet(KeyCode), - pressed_keys: std.EnumSet(KeyCode), - released_keys: std.EnumSet(KeyCode), - pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds), - - mouse: Mouse, + keyboard: InputSystem.ButtonStateSet(KeyCode), + mouse_button: InputSystem.ButtonStateSet(MouseButton), + mouse_position: ?Vec2, pub const empty = Input{ - .down_keys = .initEmpty(), - .pressed_keys = .initEmpty(), - .released_keys = .initEmpty(), - .pressed_keys_at = .init(.{}), - .mouse = .empty + .keyboard = .empty, + .mouse_button = .empty, + .mouse_position = null, }; }; @@ -139,7 +134,8 @@ pub fn time(self: Frame) f64 { } pub fn isKeyDown(self: Frame, key_code: KeyCode) bool { - return self.input.down_keys.contains(key_code); + const keyboard = &self.input.keyboard; + return keyboard.down.contains(key_code); } pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 { @@ -147,18 +143,21 @@ pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 { return null; } - const pressed_at_ns = self.input.pressed_keys_at.get(key_code).?; + const keyboard = &self.input.keyboard; + const pressed_at_ns = keyboard.pressed_at.get(key_code).?; const duration_ns = self.time_ns - pressed_at_ns; return @as(f32, @floatFromInt(duration_ns)) / std.time.ns_per_s; } pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool { - return self.input.pressed_keys.contains(key_code); + const keyboard = &self.input.keyboard; + return keyboard.pressed.contains(key_code); } pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool { - return self.input.released_keys.contains(key_code); + const keyboard = &self.input.keyboard; + return keyboard.released.contains(key_code); } pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState { @@ -170,6 +169,14 @@ pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState { }; } +pub fn isMousePressed(self: Frame, button: MouseButton) bool { + return self.input.mouse_button.pressed.contains(button); +} + +pub fn isMouseDown(self: Frame, button: MouseButton) bool { + return self.input.mouse_button.down.contains(button); +} + fn pushAudioCommand(self: *Frame, command: AudioCommand) void { const arena = self.arena.allocator(); diff --git a/src/engine/input.zig b/src/engine/input.zig index 700c556..ee7ed33 100644 --- a/src/engine/input.zig +++ b/src/engine/input.zig @@ -19,38 +19,59 @@ pub const KeyCode = @Type(.{ } }); -pub const Mouse = struct { - pub const Button = enum { - left, - right, - middle, +pub const MouseButton = enum(i3) { + left = @intFromEnum(sokol.app.Mousebutton.LEFT), + right = @intFromEnum(sokol.app.Mousebutton.RIGHT), + middle = @intFromEnum(sokol.app.Mousebutton.MIDDLE), + _ +}; - pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?Button { - return switch(mouse_button) { - .LEFT => Button.left, - .RIGHT => Button.right, - .MIDDLE => Button.middle, - else => null - }; +pub fn ButtonStateSet(E: type) type { + return struct { + const Self = @This(); + + down: std.EnumSet(E), + pressed: std.EnumSet(E), + released: std.EnumSet(E), + pressed_at: std.EnumMap(E, Nanoseconds), + + pub const empty = Self{ + .down = .initEmpty(), + .pressed = .initEmpty(), + .released = .initEmpty(), + .pressed_at = .init(.{}), + }; + + fn press(self: *Self, button: E, now: Nanoseconds) void { + self.pressed_at.put(button, now); + self.pressed.insert(button); + self.down.insert(button); + } + + fn release(self: *Self, button: E) void { + self.down.remove(button); + self.released.insert(button); + self.pressed_at.remove(button); + } + + fn releaseAll(self: *Self) void { + var iter = self.down.iterator(); + while (iter.next()) |key_code| { + self.released.insert(key_code); + } + self.down = .initEmpty(); + self.pressed_at = .init(.{}); } }; - - position: ?Vec2, - buttons: std.EnumSet(Button), - - pub const empty = Mouse{ - .position = null, - .buttons = .initEmpty() - }; -}; +} pub const Event = union(enum) { mouse_pressed: struct { - button: Mouse.Button, + button: MouseButton, position: Vec2, }, mouse_released: struct { - button: Mouse.Button, + button: MouseButton, position: Vec2, }, mouse_move: Vec2, @@ -72,39 +93,31 @@ pub fn processEvent(frame: *Frame, event: Event) void { switch (event) { .key_pressed => |opts| { if (!opts.repeat) { - input.pressed_keys_at.put(opts.code, frame.time_ns); - input.pressed_keys.insert(opts.code); - input.down_keys.insert(opts.code); + input.keyboard.press(opts.code, frame.time_ns); } }, .key_released => |key_code| { - input.down_keys.remove(key_code); - input.released_keys.insert(key_code); - input.pressed_keys_at.remove(key_code); + input.keyboard.release(key_code); }, .mouse_leave => { - var iter = input.down_keys.iterator(); - while (iter.next()) |key_code| { - input.released_keys.insert(key_code); - } - input.down_keys = .initEmpty(); - input.pressed_keys_at = .init(.{}); + input.keyboard.releaseAll(); - input.mouse = .empty; + input.mouse_position = null; + input.mouse_button = .empty; }, .mouse_enter => |pos| { - input.mouse.position = pos; + input.mouse_position = pos; }, .mouse_move => |pos| { - input.mouse.position = pos; + input.mouse_position = pos; }, .mouse_pressed => |opts| { - input.mouse.position = opts.position; - input.mouse.buttons.insert(opts.button); + input.mouse_position = opts.position; + input.mouse_button.press(opts.button, frame.time_ns); }, .mouse_released => |opts| { - input.mouse.position = opts.position; - input.mouse.buttons.remove(opts.button); + input.mouse_position = opts.position; + input.mouse_button.release(opts.button); }, else => {} } diff --git a/src/engine/math.zig b/src/engine/math.zig index 301b334..95d0178 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -124,14 +124,22 @@ pub const Vec2 = extern struct { ); } + pub fn lengthSqr(self: Vec2) f32 { + return self.x*self.x + self.y*self.y; + } + pub fn length(self: Vec2) f32 { - return @sqrt(self.x*self.x + self.y*self.y); + return @sqrt(self.lengthSqr()); } pub fn distance(self: Vec2, other: Vec2) f32 { return self.sub(other).length(); } + pub fn distanceSqr(self: Vec2, other: Vec2) f32 { + return self.sub(other).lengthSqr(); + } + pub fn limitLength(self: Vec2, max_length: f32) Vec2 { const self_length = self.length(); if (self_length > max_length) { @@ -395,6 +403,10 @@ pub const Rect = struct { return self.pos.y + self.size.y; } + pub fn center(self: Rect) Vec2 { + return self.pos.add(self.size.multiplyScalar(0.5)); + } + pub fn multiply(self: Rect, xy: Vec2) Rect { return Rect{ .pos = self.pos.multiply(xy), diff --git a/src/engine/root.zig b/src/engine/root.zig index 162edf9..36b9e3e 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -165,11 +165,11 @@ fn sokolFrame(self: *Engine) !void { var revert_mouse_position: ?Vec2 = null; if (self.canvas_size) |canvas_size| { - if (self.frame.input.mouse.position) |mouse| { + if (self.frame.input.mouse_position) |mouse| { const transform = ScreenScalar.init(screen_size, canvas_size); revert_mouse_position = mouse; - self.frame.input.mouse.position = mouse.sub(transform.translation).divideScalar(transform.scale); + self.frame.input.mouse_position = mouse.sub(transform.translation).divideScalar(transform.scale); } } @@ -195,8 +195,10 @@ fn sokolFrame(self: *Engine) !void { try self.game.tick(&self.frame); - frame.input.pressed_keys = .initEmpty(); - frame.input.released_keys = .initEmpty(); + frame.input.keyboard.pressed = .initEmpty(); + frame.input.keyboard.released = .initEmpty(); + frame.input.mouse_button.pressed = .initEmpty(); + frame.input.mouse_button.released = .initEmpty(); } if (self.canvas_size) |canvas_size| { @@ -234,7 +236,7 @@ fn sokolFrame(self: *Engine) !void { } if (revert_mouse_position) |pos| { - self.frame.input.mouse.position = pos; + self.frame.input.mouse_position = pos; } } @@ -262,7 +264,6 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { defer zone.deinit(); const e = e_ptr.*; - const MouseButton = Input.Mouse.Button; if (imgui.handleEvent(e)) { if (self.mouse_inside) { @@ -274,13 +275,11 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; } - blk: switch (e.type) { + switch (e.type) { .MOUSE_DOWN => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - Input.processEvent(&self.frame, .{ .mouse_pressed = .{ - .button = mouse_button, + .button = @enumFromInt(@intFromEnum(e.mouse_button)), .position = Vec2.init(e.mouse_x, e.mouse_y) } }); @@ -288,11 +287,9 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; }, .MOUSE_UP => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - Input.processEvent(&self.frame, .{ .mouse_released = .{ - .button = mouse_button, + .button = @enumFromInt(@intFromEnum(e.mouse_button)), .position = Vec2.init(e.mouse_x, e.mouse_y) } }); diff --git a/src/game.zig b/src/game.zig index d55d6b4..12e0628 100644 --- a/src/game.zig +++ b/src/game.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const clamp = std.math.clamp; +const log = std.log.scoped(.game); const Assets = @import("./assets.zig"); const Tilemap = Assets.Tilemap; @@ -17,6 +18,9 @@ const rgba = Engine.Math.rgba; const Range = Engine.Math.Range; const TextureId = Engine.Graphics.TextureId; const AudioId = Engine.Audio.Data.Id; +const Sprite = Engine.Graphics.Sprite; + +const RaycastTileIterator = @import("./raycast_tile_iterator.zig"); const Game = @This(); @@ -94,6 +98,69 @@ const AudioBundle = struct { } }; +const Bullet = struct { + position: Vec2, + velocity: Vec2 +}; + +const Tile = struct { + sprites_buffer: [4]Sprite, + sprites_len: usize, + + solid: bool, + + const empty = Tile{ + .sprites_buffer = undefined, + .sprites_len = 0, + .solid = false + }; + + fn sprites(self: *Tile) []Sprite { + return self.sprites_buffer[0..self.sprites_len]; + } + + fn appendSprite(self: *Tile, sprite: Sprite) void { + var list: std.ArrayList(Sprite) = .{ + .items = self.sprites(), + .capacity = self.sprites_buffer.len + }; + + if (list.items.len == list.capacity) { + _ = list.orderedRemove(0); + log.warn("Too many sprites on a single tile", .{}); + } + list.appendAssumeCapacity(sprite); + + self.sprites_len = list.items.len; + } +}; + +const TileGrid = struct { + origin: Vec2, + width: usize, + height: usize, + tile_size: Vec2, + tiles: []Tile, + + pub fn get(self: TileGrid, x: usize, y: usize) *Tile { + assert(0 <= x and x < self.width); + assert(0 <= y and y < self.height); + return &self.tiles[y * self.width + x]; + } + + pub fn isInBounds(self: TileGrid, x: i32, y: i32) bool { + return (0 <= x and x < @as(i32, @intCast(self.width))) and (0 <= y and y < @as(i32, @intCast(self.height))); + } + + pub fn toTileSpace(self: TileGrid, pos: Vec2) Vec2 { + return pos.sub(self.origin).divide(self.tile_size); + } + + pub fn fromTileSpace(self: TileGrid, pos: Vec2) Vec2 { + return pos.multiply(self.tile_size).add(self.origin); + } +}; + arena: std.heap.ArenaAllocator, gpa: Allocator, rng: RNGState, @@ -105,6 +172,9 @@ last_faced_left: bool = false, player_walk_sound: AudioBundle = .empty, hand_offset: Vec2 = .zero, +bullets: std.ArrayList(Bullet) = .empty, +tilegrid: TileGrid, + player_anim: Animation, pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { @@ -125,18 +195,76 @@ pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { }), }; + const tilemap = assets.terrain_tilemap; + const map_bounds = assets.map.getTileBounds(); + + const map_width = map_bounds.getWidth(); + const map_height = map_bounds.getHeight(); + const tilegrid = TileGrid{ + .origin = Vec2.initFromInt(i32, map_bounds.left, map_bounds.top).multiply(tilemap.tile_size), + .width = map_width, + .height = map_height, + .tile_size = tilemap.tile_size, + .tiles = try arena.allocator().alloc(Tile, map_width * map_height), + }; + + @memset(tilegrid.tiles, .empty); + + { + const texture_info = Engine.Graphics.getTextureInfo(tilemap.texture); + const tilemap_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height); + + for (assets.map.layers) |layer| { + if (layer.variant != .tile) { + continue; + } + + const is_layer_solid = layer.properties.getBool("solid") orelse false; + + const tile_layer = layer.variant.tile; + const layer_bounds = tile_layer.getBounds(); + for (0..layer_bounds.getHeight()) |oy| { + const y = layer_bounds.top + @as(i32, @intCast(oy)); + + for (0..layer_bounds.getWidth()) |ox| { + const x = layer_bounds.left + @as(i32, @intCast(ox)); + const tile_gid = tile_layer.get(x, y) orelse continue; + if (tile_gid == 0) continue; + const tile = assets.map.getTile(assets.tilesets, tile_gid) orelse continue; + + const tile_id_f32: f32 = @floatFromInt(tile.id); + const width_in_tiles = tilemap_size.x / tilemap.tile_size.x; + const tile_x = @rem(tile_id_f32, width_in_tiles); + const tile_y =@divFloor(tile_id_f32, width_in_tiles); + + const tilegrid_tile = tilegrid.get( + @intCast(x - map_bounds.left), + @intCast(y - map_bounds.top) + ); + tilegrid_tile.solid |= is_layer_solid; + tilegrid_tile.appendSprite(.{ + .texture = tilemap.texture, + .uv = tilemap.getTileUV(tile_x, tile_y) + }); + } + } + } + } + return Game{ .arena = arena, .gpa = gpa, .assets = assets, .player = findSpawnpoint(assets) orelse .init(0, 0), .player_anim = player_anim, - .rng = RNGState.init(seed) + .rng = RNGState.init(seed), + .tilegrid = tilegrid }; } pub fn deinit(self: *Game) void { self.arena.deinit(); + self.bullets.deinit(self.gpa); } fn findSpawnpoint(assets: *Assets) ?Vec2 { @@ -187,42 +315,181 @@ fn drawTile(frame: *Engine.Frame, opts: DrawTileOptions) void { } fn drawTilemap(self: *Game, frame: *Engine.Frame) void { - const tilemap = self.assets.terrain_tilemap; - const texture_info = Engine.Graphics.getTextureInfo(tilemap.texture); - const tilemap_size = Vec2.initFromInt(u32,texture_info.width, texture_info.height); + for (0..self.tilegrid.height) |y| { + for (0..self.tilegrid.width) |x| { + const tile = self.tilegrid.get(x, y); + if (tile.sprites_len == 0) continue; - const map = self.assets.map; + const tile_pos = Vec2.initFromInt(usize, x, y).multiply(self.tilegrid.tile_size); + const tile_rect = Rect{ + .pos = self.tilegrid.origin.add(tile_pos), + .size = self.tilegrid.tile_size + }; - for (map.layers) |layer| { - if (layer.variant != .tile) { - continue; - } - - const tile_layer = layer.variant.tile; - const layer_bounds = tile_layer.getBounds(); - for (0..layer_bounds.height()) |oy| { - const y = layer_bounds.top + @as(i32, @intCast(oy)); - - for (0..layer_bounds.width()) |ox| { - const x = layer_bounds.left + @as(i32, @intCast(ox)); - const tile_gid = tile_layer.get(x, y) orelse continue; - const tile = map.getTile(self.assets.tilesets, tile_gid) orelse continue; - - const tile_id_f32: f32 = @floatFromInt(tile.id); - const width_in_tiles = tilemap_size.x / tilemap.tile_size.x; - const tile_x = @rem(tile_id_f32, width_in_tiles); - const tile_y = @divFloor(tile_id_f32, width_in_tiles); - - drawTile(frame, .{ - .pos = Vec2.initFromInt(i32, x, y).multiply(tilemap.tile_size), - .tilemap = tilemap, - .tile = .init(tile_x, tile_y) + const color = rgb(255, 255, 255); + for (tile.sprites()) |sprite| { + frame.drawRectangle(.{ + .rect = tile_rect, + .color = color, + .sprite = sprite }); } } } } +const CollisionResult = struct { + time: f32, + normal: Vec2, +}; + +fn sweptAABB(b1: Rect, v1: Vec2, b2: Rect) ?CollisionResult { + var entry_x: f32 = undefined; + var exit_x: f32 = undefined; + if (v1.x > 0) { + entry_x = b2.left() - b1.right(); + exit_x = b2.right() - b1.left(); + } else { + entry_x = b2.right() - b1.left(); + exit_x = b2.left() - b1.right(); + } + + var entry_y: f32 = undefined; + var exit_y: f32 = undefined; + if (v1.y > 0) { + entry_y = b2.top() - b1.bottom(); + exit_y = b2.bottom() - b1.top(); + } else { + entry_y = b2.bottom() - b1.top(); + exit_y = b2.top() - b1.bottom(); + } + + const inf = std.math.inf(f32); + + var entry_x_time: f32 = undefined; + var exit_x_time: f32 = undefined; + if (v1.x == 0) { + entry_x_time = -inf; + exit_x_time = inf; + } else { + entry_x_time = entry_x / v1.x; + exit_x_time = exit_x / v1.x; + } + + var entry_y_time: f32 = undefined; + var exit_y_time: f32 = undefined; + if (v1.y == 0) { + entry_y_time = -inf; + exit_y_time = inf; + } else { + entry_y_time = entry_y / v1.y; + exit_y_time = exit_y / v1.y; + } + + const entry_time = @max(entry_x_time, entry_y_time); + const exit_time = @min(exit_x_time, exit_y_time); + + if (entry_time > exit_time or + (entry_x_time < 0 and entry_y_time < 0) or + entry_x_time > 1.0 or + entry_y_time > 1.0 + ) { + return null; + } + + var result = CollisionResult{ + .time = entry_time, + .normal = .zero + }; + + if (entry_x_time > entry_y_time) { + result.normal.x = if ((exit_x - entry_x) < 0.0) 1 else -1; + } else { + result.normal.y = if ((exit_y - entry_y) < 0.0) 1 else -1; + } + + return result; +} + +fn getStepBounds(rect: Rect, step: Vec2) Rect { + var result = rect; + + if (step.x >= 0) { + result.size.x += step.x; + } else { + result.pos.x += step.x; + result.size.x -= step.x; + } + + if (step.y >= 0) { + result.size.y += step.y; + } else { + result.pos.y += step.y; + result.size.y -= step.y; + } + + return result; +} + +fn collisionResponseSlide(step: *Vec2, collision: CollisionResult) void { + var new_step = step.multiplyScalar(collision.time); + + const remaining_time = 1 - collision.time; + const dotprod = (step.x * collision.normal.y + step.y * collision.normal.x) * remaining_time; + new_step.x += dotprod * collision.normal.y; + new_step.y += dotprod * collision.normal.x; + + step.* = new_step; +} + +fn listNearestSolids(self: *Game, gpa: Allocator, bounds: Rect) ![]Rect { + var result: std.ArrayList(Rect) = .empty; + errdefer result.deinit(gpa); + + var distances: std.ArrayList(f32) = .empty; + defer distances.deinit(gpa); + + const top_left = self.tilegrid.toTileSpace(Vec2.init(bounds.left(), bounds.top())); + const bottom_right = self.tilegrid.toTileSpace(Vec2.init(bounds.right(), bounds.bottom())); + + var y = @floor(top_left.y); + while (y < bottom_right.y) : (y += 1) { + var x = @floor(top_left.x); + while (x < bottom_right.x) : (x += 1) { + const x_i32: i32 = @intFromFloat(x); + const y_i32: i32 = @intFromFloat(y); + if (!self.tilegrid.isInBounds(x_i32, y_i32)) continue; + + const tile = self.tilegrid.get(@intCast(x_i32), @intCast(y_i32)); + if (tile.solid) { + const tile_collider = Rect{ + .pos = self.tilegrid.fromTileSpace(Vec2.init(x, y)), + .size = self.tilegrid.tile_size + }; + try result.append(gpa, tile_collider); + try distances.append(gpa, Vec2.distanceSqr(tile_collider.center(), bounds.center())); + } + } + } + + const Context = struct { + bounds_center: Vec2, + + fn lessThanFn(ctx: @This(), lhs: Rect, rhs: Rect) bool { + const lhs_distance = Vec2.distanceSqr(lhs.center(), ctx.bounds_center); + const rhs_distance = Vec2.distanceSqr(rhs.center(), ctx.bounds_center); + return lhs_distance < rhs_distance; + } + }; + + const ctx = Context{ + .bounds_center = bounds.center() + }; + std.mem.sort(Rect, result.items, ctx, Context.lessThanFn); + + return try result.toOwnedSlice(gpa); +} + pub fn tick(self: *Game, frame: *Engine.Frame) !void { const dt = frame.deltaTime(); @@ -242,6 +509,8 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { frame.pushTransform(camera_offset, .init(1, 1)); defer frame.popTransform(); + self.drawTilemap(frame); + var dir = Vec2.init(0, 0); if (frame.isKeyDown(.W)) { dir.y -= 1; @@ -269,11 +538,59 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { self.player_anim_state.frame_index = 0; } - self.player = self.player.add(dir.multiplyScalar(50 * dt)); + const velocity = dir.multiplyScalar(50); + var step = velocity.multiplyScalar(dt); - const regular_font = self.assets.font_id.get(.regular); + const player_collider_size = Vec2.init(20, 20); + const player_collider = Rect{ + .pos = self.player.sub(player_collider_size.divideScalar(2)), + .size = player_collider_size, + }; - self.drawTilemap(frame); + { + // const solids = try self.listNearestSolids(frame.arena.allocator(), getStepBounds(player_collider, step)); + // for (solids) |solid| { + // if (sweptAABB(player_collider, step, solid)) |collision| { + // collisionResponseSlide(&step, collision); + // } + // } + + inline for (.{ + // Vec2.init(0, 0), + // Vec2.init(1, 0), + Vec2.init(0, 1), + // Vec2.init(1, 1), + }) |corner_offset| { + const corner = player_collider.pos.add(player_collider.size.multiply(corner_offset)); + var iter = RaycastTileIterator.init( + self.tilegrid.toTileSpace(corner), + self.tilegrid.toTileSpace(corner.add(step)), + ); + while (iter.next()) |collision| { + if (!self.tilegrid.isInBounds(collision.tile_x, collision.tile_y)) break; + const tile = self.tilegrid.get(@intCast(collision.tile_x), @intCast(collision.tile_y)); + if (tile.solid) { + const time = collision.distance / step.divide(self.tilegrid.tile_size).length(); + collisionResponseSlide(&step, .{ + .normal = .initFromInt(i32, collision.normal_x, collision.normal_y), + .time = time + }); + + const pos = Vec2.initFromInt(i32, collision.tile_x, collision.tile_y).multiply(self.tilegrid.tile_size).add(self.tilegrid.origin); + frame.drawRectangle(.{ + .rect = Rect{ + .pos = pos, + .size = self.tilegrid.tile_size, + }, + .color = rgba(200, 20, 20, 0.5) + }); + break; + } + } + } + } + + self.player = self.player.add(step); if (dir.x < 0) { self.last_faced_left = true; @@ -297,8 +614,14 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { } }); + frame.drawRectangle(.{ + .rect = player_collider, + .color = rgba(20, 255, 255, 0.5) + }); + + const max_hand_length = 32; - if (frame.input.mouse.position) |mouse_screen| { + if (frame.input.mouse_position) |mouse_screen| { const mouse = mouse_screen.sub(camera_offset); const player_to_mouse = mouse.sub(self.player); self.hand_offset = mouse.sub(self.player).limitLength(max_hand_length); @@ -311,6 +634,38 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { .tile = .init(4, 2) }); frame.hide_cursor = true; + + if (false) { + var iter = RaycastTileIterator.init( + self.tilegrid.toTileSpace(self.player), + self.tilegrid.toTileSpace(mouse), + ); + while (iter.next()) |item| { + if (!self.tilegrid.isInBounds(item.tile_x, item.tile_y)) break; + + const tile = self.tilegrid.get(@intCast(item.tile_x), @intCast(item.tile_y)); + const pos = Vec2.initFromInt(i32, item.tile_x, item.tile_y).multiply(self.tilegrid.tile_size).add(self.tilegrid.origin); + + frame.drawRectangle(.{ + .rect = Rect{ + .pos = pos, + .size = self.tilegrid.tile_size, + }, + .color = rgba(200, 20, 20, 0.5) + }); + frame.drawRectangle(.{ + .rect = Rect{ + .pos = pos.add(self.tilegrid.tile_size.multiplyScalar(0.25)).add(self.tilegrid.tile_size.multiplyScalar(0.25).multiply(.initFromInt(i32, item.normal_x, item.normal_y))), + .size = self.tilegrid.tile_size.divideScalar(2), + }, + .color = rgba(20, 200, 20, 0.5) + }); + + if (tile.solid) { + break; + } + } + } } const hand = self.player.add(self.hand_offset); @@ -329,10 +684,25 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { .rotation = self.hand_offset.getAngle() }); - frame.drawText(self.player, "Player", .{ - .font = regular_font, - .size = 16 - }); + if (frame.isMousePressed(.left)) { + try self.bullets.append(self.gpa, .{ + .position = hand, + .velocity = self.hand_offset.normalized().multiplyScalar(100) + }); + } + + for (self.bullets.items) |*bullet| { + bullet.position = bullet.position.add(bullet.velocity.multiplyScalar(dt)); + + const bullet_size = Vec2.init(16, 16); + frame.drawRectangle(.{ + .rect = .{ + .pos = bullet.position.sub(bullet_size.multiplyScalar(0.5)), + .size = bullet_size + }, + .color = rgb(200, 10, 10) + }); + } } pub fn debug(self: *Game) !void { diff --git a/src/raycast_tile_iterator.zig b/src/raycast_tile_iterator.zig new file mode 100644 index 0000000..f3257f9 --- /dev/null +++ b/src/raycast_tile_iterator.zig @@ -0,0 +1,97 @@ +const Engine = @import("./engine/root.zig"); +const Vec2 = Engine.Vec2; + +// DDA Algorithm: https://lodev.org/cgtutor/raycasting.html +const RaycastTileIterator = @This(); + +max_distance: f32, +ray_length_1d: Vec2, +check_x: i32, +check_y: i32, +step_x: i32, +step_y: i32, +unit_step_size: Vec2, + +const Item = struct { + tile_x: i32, + tile_y: i32, + normal_x: i32, + normal_y: i32, + distance: f32, +}; + +pub fn init(origin: Vec2, target: Vec2) RaycastTileIterator { + const target_origin = target.sub(origin); + const max_distance = target_origin.length(); + const dir = target_origin.normalized(); + + var unit_step_size_x: f32 = 0; + if (dir.x != 0) { + unit_step_size_x = dir.y / dir.x; + } + + var unit_step_size_y: f32= 0; + if (dir.y != 0) { + unit_step_size_y = dir.x / dir.y; + } + + const unit_step_size = Vec2{ + .x = Vec2.init(1, unit_step_size_x).length(), + .y = Vec2.init(unit_step_size_y, 1).length() + }; + + const step_x: i32 = if (dir.x < 0) -1 else 1; + const step_y: i32 = if (dir.y < 0) -1 else 1; + + const step_x_f32: f32 = @floatFromInt(step_x); + const step_y_f32: f32 = @floatFromInt(step_y); + + const ray_length_1d = (Vec2{ + .x = ((step_x_f32 + 1)/2 - step_x_f32 * @mod(origin.x, 1)), + .y = ((step_y_f32 + 1)/2 - step_y_f32 * @mod(origin.y, 1)) + }).multiply(unit_step_size); + + const check_x: i32 = @intFromFloat(origin.x); + const check_y: i32 = @intFromFloat(origin.y); + + return RaycastTileIterator{ + .max_distance = max_distance, + .ray_length_1d = ray_length_1d, + .check_x = check_x, + .check_y = check_y, + .step_x = step_x, + .step_y = step_y, + .unit_step_size = unit_step_size + }; +} + +pub fn next(self: *RaycastTileIterator) ?Item { + if (self.ray_length_1d.x > self.max_distance and self.ray_length_1d.y > self.max_distance) { + return null; + } + + var normal_x: i32 = 0; + var normal_y: i32 = 0; + var distance: f32 = undefined; + if (self.ray_length_1d.x < self.ray_length_1d.y) { + distance = self.ray_length_1d.x; + + normal_x = -self.step_x; + self.check_x += self.step_x; + self.ray_length_1d.x += self.unit_step_size.x; + } else { + distance = self.ray_length_1d.y; + + normal_y = -self.step_y; + self.check_y += self.step_y; + self.ray_length_1d.y += self.unit_step_size.y; + } + + return .{ + .distance = distance, + .tile_x = self.check_x, + .tile_y = self.check_y, + .normal_x = normal_x, + .normal_y = normal_y + }; +}