From 5be299ad4cc2c755585d84f655c83d98f2d67ca9 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 14 Dec 2025 13:47:50 +0200 Subject: [PATCH] add key repeat --- src/entity.zig | 17 ++ src/game.zig | 139 +++++++--- src/generational_array_list.zig | 460 ++++++++++++++++++++++++++++++++ src/imgui.zig | 6 +- src/timer.zig | 63 +++++ src/window.zig | 99 ++++++- 6 files changed, 742 insertions(+), 42 deletions(-) create mode 100644 src/entity.zig create mode 100644 src/generational_array_list.zig create mode 100644 src/timer.zig diff --git a/src/entity.zig b/src/entity.zig new file mode 100644 index 0000000..b6f307e --- /dev/null +++ b/src/entity.zig @@ -0,0 +1,17 @@ +const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; + +const Entity = @This(); + +pub const List = GenerationalArrayList(Entity); +pub const Id = List.Id; + +pub const Type = enum { + player +}; + +type: Type, +position: Vec2 + diff --git a/src/game.zig b/src/game.zig index 6fcb56c..46eec30 100644 --- a/src/game.zig +++ b/src/game.zig @@ -1,72 +1,140 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const Math = @import("./math.zig"); const Vec2 = Math.Vec2; +const Vec4 = Math.Vec4; const rgb = Math.rgb; +const Timer = @import("./timer.zig"); const Window = @import("./window.zig"); - const imgui = @import("./imgui.zig"); - const Gfx = @import("./graphics.zig"); +const Entity = @import("./entity.zig"); const Game = @This(); pub const Input = struct { - dt: f32, - move: Vec2 + allocator: Allocator, + dt: f64, + move_up: Window.KeyState, + move_down: Window.KeyState, + move_left: Window.KeyState, + move_right: Window.KeyState, }; -canvas_size: Vec2, -position: Vec2 = .init(0, 0), +const tile_size = Vec2.init(10, 10); -pub fn init() !Game { - return Game{ - .canvas_size = .init(320, 180) +canvas_size: Vec2, +entities: Entity.List, +timers: Timer.List, + +last_move: Vec2, +last_up_repeat_at: ?f64 = null, +last_down_repeat_at: ?f64 = null, +last_left_repeat_at: ?f64 = null, +last_right_repeat_at: ?f64 = null, + +pub fn init(gpa: Allocator) !Game { + var game = Game{ + .canvas_size = .init(320, 180), + .entities = .empty, + .timers = .empty, + .last_move = .init(0, 0) }; + errdefer game.deinit(gpa); + + _ = try game.entities.insert(gpa, .{ + .type = .player, + .position = .init(0, 0) + }); + + return game; } -pub fn deinit(self: *Game) void { - _ = self; // autofix +pub fn deinit(self: *Game, gpa: Allocator) void { + self.entities.deinit(gpa); + self.timers.deinit(gpa); } pub fn getInput(self: *Game, window: *Window) Input { _ = self; // autofix const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s; - var move = Vec2.init(0, 0); - if (window.down_keys.contains(.W)) { - move.y -= 1; - } - if (window.down_keys.contains(.S)) { - move.y += 1; - } - if (move.x == 0) { - if (window.down_keys.contains(.A)) { - move.x -= 1; - } - if (window.down_keys.contains(.D)) { - move.x += 1; - } - } - return Input{ + .allocator = window.gpa, .dt = dt, - .move = move + .move_up = window.getKeyState(.W), + .move_down = window.getKeyState(.S), + .move_left = window.getKeyState(.A), + .move_right = window.getKeyState(.D), }; } +fn drawGrid(self: *Game, size: Vec2, color: Vec4) void { + var x: f32 = 0; + while (x < self.canvas_size.x) { + x += size.x; + Gfx.drawLine( + .init(x, 0), + .init(x, self.canvas_size.y), + color, + 1 + ); + } + + var y: f32 = 0; + while (y < self.canvas_size.y) { + y += size.y; + Gfx.drawLine( + .init(0, y), + .init(self.canvas_size.x, y), + color, + 1 + ); + } +} + pub fn tick(self: *Game, input: Input) !void { - - const velocity = input.move.multiplyScalar(100); - self.position = self.position.add(velocity.multiplyScalar(input.dt)); - Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), rgb(255, 255, 255)); - Gfx.drawRectangle(self.position, .init(10, 10), rgb(255, 0, 0)); + self.drawGrid(tile_size, rgb(200, 200, 200)); + + self.timers.now += input.dt; + + var move: Vec2 = .init(0, 0); + defer self.last_move = move; + + const repeat_options = Window.KeyState.RepeatOptions{ + .first_at = 0.4, + .period = 0.2 + }; + + if (input.move_up.pressed or input.move_up.repeat(&self.last_up_repeat_at, repeat_options)) { + move.y -= 1; + } + if (input.move_down.pressed or input.move_down.repeat(&self.last_down_repeat_at, repeat_options)) { + move.y += 1; + } + if (input.move_left.pressed or input.move_left.repeat(&self.last_left_repeat_at, repeat_options)) { + move.x -= 1; + } + if (input.move_right.pressed or input.move_right.repeat(&self.last_right_repeat_at, repeat_options)) { + move.x += 1; + } + + var iter = self.entities.iterator(); + while (iter.nextItem()) |entity| { + if (entity.type == .player) { + // const velocity = input.move.multiplyScalar(100); + // entity.position = entity.position.add(velocity.multiplyScalar(input.dt)); + entity.position = entity.position.add(move.multiply(tile_size)); + + Gfx.drawRectangle(entity.position, .init(10, 10), rgb(255, 0, 0)); + } + } } pub fn debug(self: *Game) !void { - _ = self; // autofix if (!imgui.beginWindow(.{ .name = "Debug", .pos = Vec2.init(20, 20), @@ -75,4 +143,7 @@ pub fn debug(self: *Game) !void { return; } defer imgui.endWindow(); + + imgui.textFmt("Entities: {}", .{self.entities.len}); + imgui.textFmt("Timers: {}", .{self.timers.array_list.len}); } diff --git a/src/generational_array_list.zig b/src/generational_array_list.zig new file mode 100644 index 0000000..3d49e69 --- /dev/null +++ b/src/generational_array_list.zig @@ -0,0 +1,460 @@ +const std = @import("std"); +const tracy = @import("tracy"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const Index = u24; +const Generation = u8; + +pub fn GenerationalArrayList(Item: type) type { + assert(@bitSizeOf(Generation) % 8 == 0); + assert(@bitSizeOf(Index) % 8 == 0); + + return struct { + const Self = @This(); + + items: [*]Item, + generations: [*]Generation, + unused: [*]u8, + + len: u32, + capacity: u32, + + count: u32, + + pub const empty = Self{ + .items = &[_]Item{}, + .generations = &[_]Generation{}, + .unused = &[_]u8{}, + .capacity = 0, + .len = 0, + .count = 0 + }; + + pub const Id = packed struct { + generation: Generation, + index: Index, + + // TODO: Maybe `Id.Optional` type should be created to ensure .wrap() and .toOptional() + pub const none = Id{ + .generation = std.math.maxInt(Generation), + .index = std.math.maxInt(Index), + }; + + pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void { + if (self == Id.none) { + try writer.print("Id({s}){{ .none }}", .{ @typeName(Item) }); + } else { + try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Item), self.index, self.generation }); + } + } + + pub fn asInt(self: Id) u32 { + return @bitCast(self); + } + }; + + pub const ItemWithId = struct { + id: Id, + item: *Item, + }; + + pub const Iterator = struct { + array_list: *Self, + index: Index, + + pub fn nextId(self: *Iterator) ?Id { + while (self.index < self.array_list.len) { + const index = self.index; + self.index += 1; + + // TODO: Inline the `byte_index` calculate for better speed. + // Probably not needed. Idk + if (self.array_list.isUnused(index)) { + continue; + } + + return Id{ + .index = @intCast(index), + .generation = self.array_list.generations[index] + }; + } + + return null; + } + + pub fn nextItem(self: *Iterator) ?*Item { + if (self.nextId()) |id| { + return &self.array_list.items[id.index]; + } + + return null; + } + + pub fn next(self: *Iterator) ?ItemWithId { + if (self.nextId()) |id| { + return ItemWithId{ + .id = id, + .item = &self.array_list.items[id.index] + }; + } + + return null; + } + }; + + pub const Metadata = extern struct { + len: u32, + count: u32 + }; + + fn divCeilGeneration(num: u32) u32 { + return std.math.divCeil(u32, num, @bitSizeOf(Generation)) catch unreachable; + } + + fn divFloorGeneration(num: u32) u32 { + return @divFloor(num, @bitSizeOf(Generation)); + } + + pub fn ensureTotalCapacityPrecise(self: *Self, allocator: Allocator, new_capacity: u32) !void { + if (new_capacity > std.math.maxInt(Index)) { + return error.OutOfIndexSpace; + } + + // TODO: Shrinking is not supported + assert(new_capacity >= self.capacity); + + const unused_bit_array_len = divCeilGeneration(self.capacity); + const new_unused_bit_array_len = divCeilGeneration(new_capacity); + + // TODO: Handle allocation failure case + const new_unused = try allocator.realloc(self.unused[0..unused_bit_array_len], new_unused_bit_array_len); + const new_items = try allocator.realloc(self.items[0..self.capacity], new_capacity); + const new_generations = try allocator.realloc(self.generations[0..self.capacity], new_capacity); + + self.unused = new_unused.ptr; + self.items = new_items.ptr; + self.generations = new_generations.ptr; + self.capacity = new_capacity; + } + + fn growCapacity(current: u32, minimum: u32) u32 { + const init_capacity = @as(comptime_int, @max(1, std.atomic.cache_line / @sizeOf(Item))); + + var new = current; + while (true) { + new +|= new / 2 + init_capacity; + if (new >= minimum) { + return new; + } + } + } + + pub fn ensureTotalCapacity(self: *Self, allocator: Allocator, new_capacity: u32) !void { + if (self.capacity >= new_capacity) return; + + const better_capacity = Self.growCapacity(self.capacity, new_capacity); + try self.ensureTotalCapacityPrecise(allocator, better_capacity); + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.count = 0; + self.len = 0; + } + + pub fn ensureUnusedCapacity(self: *Self, allocator: Allocator, unused_capacity: u32) !void { + try self.ensureTotalCapacity(allocator, self.len + unused_capacity); + } + + fn findFirstUnused(self: *Self) ?Index { + for (0..divCeilGeneration(self.len)) |byte_index| { + if (self.unused[byte_index] != 0) { + const found = @ctz(self.unused[byte_index]) + byte_index * @bitSizeOf(Generation); + if (found < self.len) { + return @intCast(found); + } else { + return null; + } + } + } + + return null; + } + + fn markUnused(self: *Self, index: Index, unused: bool) void { + assert(index < self.len); + + const byte_index = divFloorGeneration(index); + const bit_index = @mod(index, @bitSizeOf(Generation)); + const bit_flag = @as(u8, 1) << @intCast(bit_index); + if (unused) { + self.unused[byte_index] |= bit_flag; + } else { + self.unused[byte_index] &= ~bit_flag; + } + } + + fn isUnused(self: *Self, index: Index) bool { + assert(index < self.len); + + const byte_index = divFloorGeneration(index); + const bit_index = @mod(index, @bitSizeOf(Generation)); + const bit_flag = @as(u8, 1) << @intCast(bit_index); + return (self.unused[byte_index] & bit_flag) != 0; + } + + pub fn insertUndefined(self: *Self, allocator: Allocator) !Id { + var unused_index: Index = undefined; + + if (self.findFirstUnused()) |index| { + unused_index = index; + } else { + try self.ensureUnusedCapacity(allocator, 1); + + unused_index = @intCast(self.len); + self.len += 1; + self.generations[unused_index] = 0; + } + + self.markUnused(unused_index, false); + self.count += 1; + + const id = Id{ + .index = @intCast(unused_index), + .generation = self.generations[unused_index] + }; + + assert(id != Id.none); + + return id; + } + + pub fn insert(self: *Self, allocator: Allocator, item: Item) !Id { + const id = try self.insertUndefined(allocator); + + const new_item_ptr = self.getAssumeExists(id); + new_item_ptr.* = item; + + return id; + } + + pub fn exists(self: *Self, id: Id) bool { + if (id.index >= self.len) { + return false; + } + + if (self.isUnused(id.index)) { + return false; + } + + if (self.generations[id.index] != id.generation) { + return false; + } + + return true; + } + + pub fn removeAssumeExists(self: *Self, id: Id) void { + assert(self.exists(id)); + + self.markUnused(id.index, true); + // TODO: Maybe a log should be shown when a wrap-around occurs? + self.generations[id.index] +%= 1; + self.count -= 1; + } + + pub fn remove(self: *Self, id: Id) bool { + if (!self.exists(id)) { + return false; + } + + self.removeAssumeExists(id); + return true; + } + + pub fn getAssumeExists(self: *Self, id: Id) *Item { + assert(self.exists(id)); + + return &self.items[id.index]; + } + + pub fn get(self: *Self, id: Id) ?*Item { + if (self.exists(id)) { + return self.getAssumeExists(id); + } else { + return null; + } + } + + pub fn iterator(self: *Self) Iterator { + return Iterator{ + .array_list = self, + .index = 0 + }; + } + + pub fn deinit(self: *Self, allocator: Allocator) void { + allocator.free(self.unused[0..divCeilGeneration(self.capacity)]); + allocator.free(self.generations[0..self.capacity]); + allocator.free(self.items[0..self.capacity]); + } + + pub fn getMetadata(self: *Self) Metadata { + return Metadata{ + .len = self.len, + .count = self.count + }; + } + + pub fn write(self: *Self, writer: *std.Io.Writer, endian: std.builtin.Endian) !void { + const zone = tracy.beginZone(@src(), .{ .name = "gen array list write" }); + defer zone.end(); + + try writer.writeSliceEndian(Item, self.items[0..self.len], endian); + try writer.writeSliceEndian(Generation, self.generations[0..self.len], endian); + try writer.writeAll(self.unused[0..divCeilGeneration(self.len)]); + } + + pub fn read( + self: *Self, + allocator: Allocator, + reader: *std.Io.Reader, + endian: std.builtin.Endian, + metadata: Metadata + ) !void { + const zone = tracy.beginZone(@src(), .{ .name = "gen array list read" }); + defer zone.end(); + + try self.ensureTotalCapacity(allocator, metadata.len); + + try reader.readSliceEndian(Item, self.items[0..metadata.len], endian); + try reader.readSliceEndian(Generation, self.generations[0..metadata.len], endian); + try reader.readSliceAll(self.unused[0..divCeilGeneration(metadata.len)]); + + self.len = metadata.len; + self.count = metadata.count; + } + }; +} + +const TestArray = GenerationalArrayList(u32); + +test "insert & remove" { + const expect = std.testing.expect; + const gpa = std.testing.allocator; + + var array_list: TestArray = .empty; + defer array_list.deinit(gpa); + + const id1 = try array_list.insert(gpa, 10); + try expect(array_list.exists(id1)); + try expect(array_list.remove(id1)); + try expect(!array_list.exists(id1)); + try expect(!array_list.remove(id1)); + + const id2 = try array_list.insert(gpa, 10); + try expect(array_list.exists(id2)); + try expect(!array_list.exists(id1)); + try expect(id1.index == id2.index); +} + +test "generation wrap around" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var array_list: TestArray = .empty; + defer array_list.deinit(gpa); + + // Grow array list so that at least 1 slot exists + const id1 = try array_list.insert(gpa, 10); + array_list.removeAssumeExists(id1); + + // Artificially increase generation count + array_list.generations[id1.index] = std.math.maxInt(Generation); + + // Check if generation wraps around + const id2 = try array_list.insert(gpa, 10); + array_list.removeAssumeExists(id2); + try expectEqual(id1.index, id2.index); + try expectEqual(0, array_list.generations[id1.index]); +} + +test "iterator" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var array_list: TestArray = .empty; + defer array_list.deinit(gpa); + + // Create array which has a hole + const id1 = try array_list.insert(gpa, 1); + const id2 = try array_list.insert(gpa, 2); + const id3 = try array_list.insert(gpa, 3); + + array_list.removeAssumeExists(id2); + + var iter = array_list.iterator(); + try expectEqual( + TestArray.ItemWithId{ + .id = id1, + .item = array_list.getAssumeExists(id1) + }, + iter.next().? + ); + try expectEqual( + TestArray.ItemWithId{ + .id = id3, + .item = array_list.getAssumeExists(id3) + }, + iter.next().? + ); + try expectEqual(null, iter.next()); +} + +test "read & write" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var array_list1: TestArray = .empty; + defer array_list1.deinit(gpa); + + var array_list2: TestArray = .empty; + defer array_list2.deinit(gpa); + + const id1 = try array_list1.insert(gpa, 1); + const id2 = try array_list1.insert(gpa, 2); + const id3 = try array_list1.insert(gpa, 3); + + var buffer: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&buffer); + const native_endian = builtin.cpu.arch.endian(); + + try array_list1.write(&writer, native_endian); + + var reader = std.Io.Reader.fixed(writer.buffered()); + try array_list2.read(gpa, &reader, native_endian, array_list1.getMetadata()); + + try expectEqual(array_list1.getAssumeExists(id1).*, array_list2.getAssumeExists(id1).*); + try expectEqual(array_list1.getAssumeExists(id2).*, array_list2.getAssumeExists(id2).*); + try expectEqual(array_list1.getAssumeExists(id3).*, array_list2.getAssumeExists(id3).*); + try expectEqual(array_list1.count, array_list2.count); +} + +test "clear retaining capacity" { + const expect = std.testing.expect; + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var array_list: TestArray = .empty; + defer array_list.deinit(gpa); + + const id1 = try array_list.insert(gpa, 10); + try expect(array_list.exists(id1)); + array_list.clearRetainingCapacity(); + + const id2 = try array_list.insert(gpa, 10); + try expect(array_list.exists(id2)); + + try expectEqual(id1, id2); +} diff --git a/src/imgui.zig b/src/imgui.zig index 02fe58f..3a95aea 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -149,8 +149,10 @@ pub fn textFmt(comptime fmt: []const u8, args: anytype) void { return; } - const formatted = std.fmt.allocPrintSentinel(global_allocator, fmt, args, 0) catch return; - defer global_allocator.free(formatted); + const gpa = global_allocator orelse return; + + const formatted = std.fmt.allocPrintSentinel(gpa, fmt, args, 0) catch return; + defer gpa.free(formatted); text(formatted); } diff --git a/src/timer.zig b/src/timer.zig new file mode 100644 index 0000000..1b67a7f --- /dev/null +++ b/src/timer.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const Entity = @import("./entity.zig"); +const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; + +const Timer = @This(); + +const ArrayList = GenerationalArrayList(Timer); +pub const Id = ArrayList.Id; + +pub const Options = struct { + duration: f64, + entity: ?Entity.Id = null +}; + +started_at: f64, +finishes_at: f64, +entity: ?Entity.Id, + +pub const List = struct { + array_list: GenerationalArrayList(Timer), + now: f64, + + pub const empty = List{ + .array_list = .empty, + .now = 0 + }; + + pub fn deinit(self: *List, gpa: Allocator) void { + self.array_list.deinit(gpa); + } + + pub fn start(self: *List, gpa: Allocator, opts: Options) !Id { + assert(opts.duration > 0); + return try self.array_list.insert(gpa, .{ + .started_at = self.now, + .finishes_at = self.now + opts.duration, + .entity = opts.entity + }); + } + + pub fn stop(self: *List, id: Id) void { + _ = self.array_list.remove(id); + } + + pub fn running(self: *List, id: Id) bool { + const timer = self.array_list.get(id) orelse return false; + return timer.finishes_at > self.now; + } + + pub fn finished(self: *List, id: Id) bool { + const timer = self.array_list.get(id) orelse return false; + if (timer.finishes_at > self.now) { + return false; + } + + self.array_list.removeAssumeExists(id); + + return true; + } +}; diff --git a/src/window.zig b/src/window.zig index a079609..2079678 100644 --- a/src/window.zig +++ b/src/window.zig @@ -176,14 +176,56 @@ pub const Event = union(enum) { char: u21, }; +pub const KeyState = struct { + down: bool, + pressed: bool, + released: bool, + down_duration: ?f64, + + pub const RepeatOptions = struct { + first_at: f64 = 0, + period: f64 + }; + + pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool { + if (!self.down) { + last_repeat_at.* = null; + return false; + } + + const down_duration = self.down_duration.?; + if (last_repeat_at.* != null) { + if (down_duration >= last_repeat_at.*.? + opts.period) { + last_repeat_at.* = last_repeat_at.*.? + opts.period; + return true; + } + } else { + if (down_duration >= opts.first_at) { + last_repeat_at.* = opts.first_at; + return true; + } + } + + return false; + } +}; + +const Nanoseconds = i128; + gpa: Allocator, events: std.ArrayList(Event), mouse_inside: bool = false, -last_frame_at_ns: i128, -frame_dt_ns: i128, -down_keys: std.EnumSet(KeyCode) = .initEmpty(), game: Game, +last_frame_at_ns: Nanoseconds, +frame_dt_ns: Nanoseconds, +time_ns: Nanoseconds, + +down_keys: std.EnumSet(KeyCode) = .initEmpty(), +pressed_keys: std.EnumSet(KeyCode) = .initEmpty(), +released_keys: std.EnumSet(KeyCode) = .initEmpty(), +pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}), + pub fn init(self: *Window, gpa: Allocator) !void { var events: std.ArrayList(Event) = .empty; errdefer events.deinit(gpa); @@ -194,14 +236,15 @@ pub fn init(self: *Window, gpa: Allocator) !void { .logger = .{ .func = sokolLogCallback } }); - var game = try Game.init(); - errdefer game.deinit(); + var game = try Game.init(gpa); + errdefer game.deinit(gpa); self.* = Window{ .gpa = gpa, .events = events, .last_frame_at_ns = std.time.nanoTimestamp(), .frame_dt_ns = 0, + .time_ns = 0, .game = game }; } @@ -209,7 +252,7 @@ pub fn init(self: *Window, gpa: Allocator) !void { pub fn deinit(self: *Window) void { const gpa = self.gpa; - self.game.deinit(); + self.game.deinit(gpa); self.events.deinit(gpa); Gfx.deinit(); @@ -219,22 +262,34 @@ pub fn frame(self: *Window) !void { const now = std.time.nanoTimestamp(); self.frame_dt_ns = now - self.last_frame_at_ns; self.last_frame_at_ns = now; + self.time_ns += self.frame_dt_ns; Gfx.beginFrame(); defer Gfx.endFrame(); + self.pressed_keys = .initEmpty(); + self.released_keys = .initEmpty(); for (self.events.items) |e| { switch (e) { .key_pressed => |opts| { if (!opts.repeat) { + self.pressed_keys_at.put(opts.code, self.time_ns); + self.pressed_keys.insert(opts.code); self.down_keys.insert(opts.code); } }, .key_released => |key_code| { self.down_keys.remove(key_code); + self.released_keys.insert(key_code); + self.pressed_keys_at.remove(key_code); }, .mouse_leave => { + var iter = self.down_keys.iterator(); + while (iter.next()) |key_code| { + self.released_keys.insert(key_code); + } self.down_keys = .initEmpty(); + self.pressed_keys_at = .init(.{}); }, else => {} } @@ -287,6 +342,38 @@ pub fn frame(self: *Window) !void { try self.game.debug(); } +pub fn isKeyDown(self: *Window, key_code: KeyCode) bool { + return self.down_keys.contains(key_code); +} + +pub fn getKeyDownDuration(self: *Window, key_code: KeyCode) ?f64 { + if (!self.isKeyDown(key_code)) { + return null; + } + + const pressed_at_ns = self.pressed_keys_at.get(key_code).?; + const duration_ns = self.time_ns - pressed_at_ns; + + return @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s; +} + +pub fn isKeyPressed(self: *Window, key_code: KeyCode) bool { + return self.pressed_keys.contains(key_code); +} + +pub fn isKeyReleased(self: *Window, key_code: KeyCode) bool { + return self.released_keys.contains(key_code); +} + +pub fn getKeyState(self: *Window, key_code: KeyCode) KeyState { + return KeyState{ + .down = self.isKeyDown(key_code), + .released = self.isKeyReleased(key_code), + .pressed = self.isKeyPressed(key_code), + .down_duration = self.getKeyDownDuration(key_code) + }; +} + fn appendEvent(self: *Window, e: Event) !void { self.events.appendBounded(e) catch return error.EventQueueFull; }