add key repeat
This commit is contained in:
parent
c2e784bfb2
commit
5be299ad4c
17
src/entity.zig
Normal file
17
src/entity.zig
Normal 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
|
||||
|
||||
139
src/game.zig
139
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});
|
||||
}
|
||||
|
||||
460
src/generational_array_list.zig
Normal file
460
src/generational_array_list.zig
Normal 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);
|
||||
}
|
||||
@ -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
63
src/timer.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user