add key repeat

This commit is contained in:
Rokas Puzonas 2025-12-14 13:47:50 +02:00
parent c2e784bfb2
commit 5be299ad4c
6 changed files with 742 additions and 42 deletions

17
src/entity.zig Normal file
View File

@ -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

View File

@ -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});
}

View File

@ -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);
}

View File

@ -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);
}

63
src/timer.zig Normal file
View File

@ -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;
}
};

View File

@ -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;
}