From a153a3f04a628e713e08e6db638ecab4c4069f4e Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 18 Aug 2026 00:04:25 +0300 Subject: [PATCH] add key repeat --- src/app.zig | 24 ++++---------- src/platform/input.zig | 74 +++++++++++++++++++++++++++++++++++------- src/platform/root.zig | 43 ++++++++++++------------ 3 files changed, 91 insertions(+), 50 deletions(-) diff --git a/src/app.zig b/src/app.zig index 02b9a87..cf2e816 100644 --- a/src/app.zig +++ b/src/app.zig @@ -280,16 +280,16 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; var dir: Vec2 = .init(0, 0); - if (input.isKeyPressed(.S)) { + if (input.isKeyPressedOrRepeat(.S)) { dir.y += 1; } - if (input.isKeyPressed(.W)) { + if (input.isKeyPressedOrRepeat(.W)) { dir.y -= 1; } - if (input.isKeyPressed(.D)) { + if (input.isKeyPressedOrRepeat(.D)) { dir.x += 1; } - if (input.isKeyPressed(.A)) { + if (input.isKeyPressedOrRepeat(.A)) { dir.x -= 1; } if (dir.x != 0) { @@ -297,13 +297,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } self.player_pos = self.player_pos.add(dir); - if (input.isKeyPressed(.F)) { - Audio.stop(self.footstep_sound); - } - - if (input.isKeyPressed(.E)) { - Audio.stop(self.footstep_sound); - self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .loop = true, .bus = self.sfx_bus }); + if (dir.x != 0 or dir.y != 0) { + self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus }); } var new_direction = self.player_direction; @@ -325,7 +320,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { const frames = self.player_sprites.getDirection(self.player_direction); if (dir.x != 0 or dir.y != 0) { - self.animation_timer += plt.dt; + self.animation_timer += plt.input.delta_time; const animatin_time = std.time.ns_per_s / 5; while (self.animation_timer >= animatin_time) { self.player_frame_index = (self.player_frame_index + 1) % @as(u32, @intCast(frames.len)); @@ -354,11 +349,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } Gfx.drawSprite(frames[self.player_frame_index], self.player_pos.multiply(tile_size), tile_size, .white); - Gfx.drawText(self.roboto_font, .{ - .pos = .init(300, 100), - .text = try std.fmt.allocPrint(plt.frame, "Hello, World! {:.0}", .{ plt.time() }), - .height = 64, - }); } pub fn deinit(self: *App, plt: Platform.Deinit) void { diff --git a/src/platform/input.zig b/src/platform/input.zig index 4b746da..7d3745e 100644 --- a/src/platform/input.zig +++ b/src/platform/input.zig @@ -6,9 +6,16 @@ const sokol = @import("sokol"); const Math = @import("math"); const Vec2 = Math.Vec2; +const Nanoseconds = @import("./root.zig").Nanoseconds; + const Input = @This(); -key_code_mapping: std.EnumMap(KeyCode, u21), +// TODO: Make this configurable from the app +const key_repeat_first: Nanoseconds = 0.4 * std.time.ns_per_s; +const key_repeat_delay: Nanoseconds = 0.2 * std.time.ns_per_s; + +now: Nanoseconds, +delta_time: Nanoseconds, window_size: Vec2, focused: bool, @@ -18,8 +25,13 @@ mouse_position: ?Vec2, mouse_delta: Vec2, mouse_scroll: Vec2, +key_code_mapping: std.EnumMap(KeyCode, u21), +last_key_repeat_at: std.EnumMap(KeyCode, Nanoseconds), + pub fn init(window_size: Vec2) Input { return Input{ + .now = 0, + .delta_time = 0, .key_code_mapping = .{}, .focused = false, .keyboard = .empty, @@ -27,7 +39,8 @@ pub fn init(window_size: Vec2) Input { .mouse_position = null, .mouse_delta = .init(0, 0), .mouse_scroll = .init(0, 0), - .window_size = window_size + .window_size = window_size, + .last_key_repeat_at = .{} }; } @@ -40,7 +53,33 @@ pub fn isKeyReleased(self: Input, key: KeyCode) bool { } pub fn isKeyDown(self: Input, key: KeyCode) bool { - return self.keyboard.down.contains(key); + return self.keyboard.pressed_at.contains(key); +} + +pub fn getKeyDown(self: Input, key: KeyCode) ?Nanoseconds { + return self.keyboard.getDown(key, self.now); +} + +pub fn isKeyRepeat(self: *Input, key: KeyCode) bool { + const pressed_at = self.keyboard.pressed_at.get(key) orelse return false; + + var next_repeat_at: Nanoseconds = undefined; + if (self.last_key_repeat_at.get(key)) |last_repeat_at| { + next_repeat_at = last_repeat_at + key_repeat_delay; + } else { + next_repeat_at = pressed_at + key_repeat_first; + } + + if (self.now >= next_repeat_at) { + self.last_key_repeat_at.put(key, next_repeat_at); + return true; + } else { + return false; + } +} + +pub fn isKeyPressedOrRepeat(self: *Input, key: KeyCode) bool { + return self.isKeyPressed(key) or self.isKeyRepeat(key); } pub fn isMousePressed(self: Input, button: MouseButton) bool { @@ -52,7 +91,11 @@ pub fn isMouseReleased(self: Input, button: MouseButton) bool { } pub fn isMouseDown(self: Input, button: MouseButton) bool { - return self.mouse_buttons.down.contains(button); + return self.mouse_buttons.pressed_at.contains(button); +} + +pub fn getMouseDown(self: Input, button: MouseButton) ?Nanoseconds { + return self.mouse_buttons.getDown(button, self.now); } pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { @@ -212,28 +255,35 @@ fn KeyStateType(T: type) type { return struct { pressed: std.EnumSet(T), released: std.EnumSet(T), - down: std.EnumSet(T), + pressed_at: std.EnumMap(T, Nanoseconds), pub const empty = @This(){ .pressed = .empty, .released = .empty, - .down = .empty, + .pressed_at = .{}, }; - pub fn press(self: *@This(), key: T) void { + pub fn press(self: *@This(), key: T, now: Nanoseconds) void { self.pressed.insert(key); - self.down.insert(key); + self.pressed_at.put(key, now); } pub fn release(self: *@This(), key: T) void { self.released.insert(key); - self.down.remove(key); + self.pressed_at.remove(key); + } + + pub fn getDown(self: @This(), key: T, now: Nanoseconds) ?Nanoseconds { + if (self.pressed_at.get(key)) |pressed_at| { + return now - pressed_at; + } + return null; } pub fn releaseAll(self: *@This()) void { - var iter = self.down.iterator(); - while (iter.next()) |key| { - self.release(key); + var iter = self.pressed_at.iterator(); + while (iter.next()) |e| { + self.release(e.key); } } }; diff --git a/src/platform/root.zig b/src/platform/root.zig index 88fa967..94ee4f3 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -105,10 +105,11 @@ fn PlatformType(App: type) type { _ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable const now = Io.Timestamp.now(self.io, .awake); - const t = self.started_at.durationTo(now); - const dt = self.last_frame_at.durationTo(now); self.last_frame_at = now; + self.input.now = @intCast(self.started_at.durationTo(now).nanoseconds); + self.input.delta_time = @intCast(self.last_frame_at.durationTo(now).nanoseconds); + if (self.input.keyboard.pressed.contains(.F4)) { self.show_imgui = !self.show_imgui; } @@ -126,8 +127,6 @@ fn PlatformType(App: type) type { if (@hasDecl(App, "frame")) { const plt = Frame{ - .t = t.nanoseconds, - .dt = dt.nanoseconds, .gpa = self.gpa, .arena = self.arena.allocator(), .io = self.io, @@ -195,7 +194,9 @@ fn PlatformType(App: type) type { return false; } - input.keyboard.press(key); + const now = self.started_at.durationTo(Io.Timestamp.now(self.io, .awake)); + + input.keyboard.press(key, @intCast(now.nanoseconds)); self.last_key_pressed = key; }, .key_released => |key| { @@ -204,13 +205,16 @@ fn PlatformType(App: type) type { } input.keyboard.release(key); + input.last_key_repeat_at.remove(key); }, .mouse_pressed => |button| { if (input.mouse_position == null or input.isMouseDown(button)) { return false; } - input.mouse_buttons.press(button); + const now = self.started_at.durationTo(Io.Timestamp.now(self.io, .awake)); + + input.mouse_buttons.press(button, @intCast(now.nanoseconds)); }, .mouse_released => |button| { if (input.mouse_position == null or !input.isMouseDown(button)) { @@ -274,8 +278,8 @@ fn PlatformType(App: type) type { } assert(input.mouse_position == null); - assert(input.keyboard.down.eql(.empty)); - assert(input.mouse_buttons.down.eql(.empty)); + assert(input.keyboard.pressed_at.count() == 0); + assert(input.mouse_buttons.pressed_at.count() == 0); input.focused = false; } } @@ -306,13 +310,13 @@ fn PlatformType(App: type) type { } if (e == .unfocused) { - var key_iter = input.keyboard.down.iterator(); - while (key_iter.next()) |key| { - self.appendEvent(.{ .key_released = key }); + var key_iter = input.keyboard.pressed_at.iterator(); + while (key_iter.next()) |key_pressed_at| { + self.appendEvent(.{ .key_released = key_pressed_at.key }); } - var mouse_buttons_iter = input.mouse_buttons.down.iterator(); - while (mouse_buttons_iter.next()) |button| { - self.appendEvent(.{ .mouse_released = button }); + var mouse_buttons_iter = input.mouse_buttons.pressed_at.iterator(); + while (mouse_buttons_iter.next()) |key_pressed_at| { + self.appendEvent(.{ .mouse_released = key_pressed_at.key }); } self.appendEvent(.{ .mouse_leave = {} }); } @@ -566,12 +570,9 @@ pub const Deinit = struct { io: Io, }; -pub const Nanoseconds = i96; +pub const Nanoseconds = i64; pub const Frame = struct { - t: Nanoseconds, - dt: Nanoseconds, - gpa: Allocator, arena: Allocator, frame: Allocator, @@ -580,16 +581,16 @@ pub const Frame = struct { input: *Input, input_events: []Input.Event, - fn nanosecondsToSeconds(nanoseconds: i96) f32 { + fn nanosecondsToSeconds(nanoseconds: Nanoseconds) f32 { return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s); } pub fn deltaTime(self: Frame) f32 { - return nanosecondsToSeconds(self.dt); + return nanosecondsToSeconds(self.input.delta_time); } pub fn time(self: Frame) f32 { - return nanosecondsToSeconds(self.t); + return nanosecondsToSeconds(self.input.now); } };