game-2026-01-30/src/engine/frame.zig
2026-01-31 19:23:48 +02:00

328 lines
8.4 KiB
Zig

const std = @import("std");
const build_options = @import("build_options");
const log = std.log.scoped(.engine);
const InputSystem = @import("./input.zig");
const KeyCode = InputSystem.KeyCode;
const MouseButton = InputSystem.MouseButton;
const AudioSystem = @import("./audio/root.zig");
const AudioData = AudioSystem.Data;
const AudioCommand = AudioSystem.Command;
const GraphicsSystem = @import("./graphics.zig");
const TextureId = GraphicsSystem.TextureId;
const GraphicsCommand = GraphicsSystem.Command;
const Font = GraphicsSystem.Font;
const Sprite = GraphicsSystem.Sprite;
const Math = @import("./math.zig");
const Rect = Math.Rect;
const Vec4 = Math.Vec4;
const Vec2 = Math.Vec2;
const rgb = Math.rgb;
pub const Nanoseconds = u64;
const Frame = @This();
pub const Input = struct {
keyboard: InputSystem.ButtonStateSet(KeyCode),
mouse_button: InputSystem.ButtonStateSet(MouseButton),
mouse_position: ?Vec2,
mouse_delta: Vec2,
pub const empty = Input{
.keyboard = .empty,
.mouse_button = .empty,
.mouse_position = null,
.mouse_delta = .zero,
};
};
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;
}
};
pub const Audio = struct {
commands: std.ArrayList(AudioCommand),
pub const empty = Audio{
.commands = .empty
};
};
pub const Graphics = struct {
clear_color: Vec4,
screen_size: Vec2,
canvas_size: ?Vec2,
scissor_stack: std.ArrayList(Rect),
commands: std.ArrayList(GraphicsCommand),
pub const empty = Graphics{
.clear_color = rgb(0, 0, 0),
.screen_size = .init(0, 0),
.canvas_size = null,
.scissor_stack = .empty,
.commands = .empty
};
};
arena: std.heap.ArenaAllocator,
time_ns: Nanoseconds,
dt_ns: Nanoseconds,
input: Input,
audio: Audio,
graphics: Graphics,
show_debug: bool,
hide_cursor: bool,
pub fn init(self: *Frame, gpa: std.mem.Allocator) void {
self.* = Frame{
.arena = std.heap.ArenaAllocator.init(gpa),
.time_ns = 0,
.dt_ns = 0,
.input = .empty,
.audio = .empty,
.graphics = .empty,
.show_debug = false,
.hide_cursor = false
};
}
pub fn deinit(self: *Frame) void {
self.arena.deinit();
}
pub fn deltaTime(self: Frame) f32 {
return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s;
}
pub fn time(self: Frame) f64 {
return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s;
}
pub fn isKeyDown(self: Frame, key_code: KeyCode) bool {
const keyboard = &self.input.keyboard;
return keyboard.down.contains(key_code);
}
pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 {
if (!self.isKeyDown(key_code)) {
return null;
}
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 {
const keyboard = &self.input.keyboard;
return keyboard.pressed.contains(key_code);
}
pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool {
const keyboard = &self.input.keyboard;
return keyboard.released.contains(key_code);
}
pub fn getKeyState(self: Frame, 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)
};
}
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();
self.audio.commands.append(arena, command) catch |e| {
log.warn("Failed to play audio: {}", .{e});
};
}
pub fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
const arena = self.arena.allocator();
self.graphics.commands.append(arena, command) catch |e|{
log.warn("Failed to push graphics command: {}", .{e});
};
}
pub fn prependGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
const arena = self.arena.allocator();
self.graphics.commands.insert(arena, 0, command) catch |e|{
log.warn("Failed to push graphics command: {}", .{e});
};
}
pub fn playAudio(self: *Frame, options: AudioCommand.Play) void {
self.pushAudioCommand(.{
.play = options,
});
}
pub fn pushScissor(self: *Frame, rect: Rect) void {
const arena = self.arena.allocator();
self.graphics.scissor_stack.append(arena, rect) catch |e| {
log.warn("Failed to push scissor region: {}", .{e});
return;
};
self.pushGraphicsCommand(.{
.set_scissor = rect
});
}
pub fn popScissor(self: *Frame) void {
_ = self.graphics.scissor_stack.pop().?;
const rect = self.graphics.scissor_stack.getLast();
self.pushGraphicsCommand(.{
.set_scissor = rect
});
}
pub fn drawCircle(self: *Frame, center: Vec2, radius: f32, color: Vec4) void {
self.pushGraphicsCommand(.{
.draw_circle = .{
.center = center,
.radius = radius,
.color = color,
}
});
}
pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void {
self.pushGraphicsCommand(.{ .draw_rectangle = opts });
}
pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void {
self.pushGraphicsCommand(.{
.draw_line = .{
.pos1 = pos1,
.pos2 = pos2,
.width = width,
.color = color
}
});
}
pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
// TODO: Don't use line segments
self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
}
pub const DrawTextOptions = struct {
font: Font.Id,
size: f32 = 16,
color: Vec4 = rgb(255, 255, 255),
};
pub fn drawText(self: *Frame, position: Vec2, opts: DrawTextOptions, text: []const u8) void {
const arena = self.arena.allocator();
const text_dupe = arena.dupe(u8, text) catch |e| {
log.warn("Failed to draw text: {}", .{e});
return;
};
self.pushGraphicsCommand(.{
.draw_text = .{
.pos = position,
.text = text_dupe,
.size = opts.size,
.font = opts.font,
.color = opts.color,
}
});
}
pub fn drawTextFormat(
self: *Frame,
position: Vec2,
opts: DrawTextOptions,
comptime fmt: []const u8,
args: anytype
) void {
const arena = self.arena.allocator();
const text = std.fmt.allocPrint(arena, fmt, args) catch |e| {
log.warn("Failed to draw text: {}", .{e});
return;
};
self.pushGraphicsCommand(.{
.draw_text = .{
.pos = position,
.text = text,
.size = opts.size,
.font = opts.font,
.color = opts.color,
}
});
}
pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void {
self.pushGraphicsCommand(.{
.push_transformation = .{
.translation = translation,
.scale = scale
}
});
}
pub fn popTransform(self: *Frame) void {
self.pushGraphicsCommand(.{
.pop_transformation = {}
});
}