const std = @import("std"); const log = std.log.scoped(.engine); pub const build_options = @import("build_options"); pub const ImGui = @import("./imgui.zig"); pub const Math = @import("./math.zig"); const rgb = Math.rgb; const Rect = Math.Rect; const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; pub const Nanoseconds = u64; pub const KeyCode = enum { SPACE, APOSTROPHE, COMMA, MINUS, PERIOD, SLASH, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, SEMICOLON, EQUAL, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, LEFT_BRACKET, BACKSLASH, RIGHT_BRACKET, GRAVE_ACCENT, WORLD_1, WORLD_2, ESCAPE, ENTER, TAB, BACKSPACE, INSERT, DELETE, RIGHT, LEFT, DOWN, UP, PAGE_UP, PAGE_DOWN, HOME, END, CAPS_LOCK, SCROLL_LOCK, NUM_LOCK, PRINT_SCREEN, PAUSE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, KP_0, KP_1, KP_2, KP_3, KP_4, KP_5, KP_6, KP_7, KP_8, KP_9, KP_DECIMAL, KP_DIVIDE, KP_MULTIPLY, KP_SUBTRACT, KP_ADD, KP_ENTER, KP_EQUAL, LEFT_SHIFT, LEFT_CONTROL, LEFT_ALT, LEFT_SUPER, RIGHT_SHIFT, RIGHT_CONTROL, RIGHT_ALT, RIGHT_SUPER, MENU, }; pub const MouseButton = enum { left, right, middle, }; pub const TextureId = enum(u16) { _ }; pub const FontId = enum(u16) { _ }; pub const AudioId = enum(u16) { _ }; pub const Sprite = struct { texture: TextureId, uv: Rect }; fn ButtonStateSet(E: type) type { return struct { const Self = @This(); down: std.EnumSet(E), pressed: std.EnumSet(E), released: std.EnumSet(E), pressed_at: std.EnumMap(E, Nanoseconds), pub const empty = Self{ .down = .initEmpty(), .pressed = .initEmpty(), .released = .initEmpty(), .pressed_at = .init(.{}), }; pub fn press(self: *Self, button: E, now: Nanoseconds) void { self.pressed_at.put(button, now); self.pressed.insert(button); self.down.insert(button); } pub fn release(self: *Self, button: E) void { self.down.remove(button); self.released.insert(button); self.pressed_at.remove(button); } pub fn releaseAll(self: *Self) void { var iter = self.down.iterator(); while (iter.next()) |key_code| { self.released.insert(key_code); } self.down = .initEmpty(); self.pressed_at = .init(.{}); } }; } pub const GraphicsCommand = union(enum) { pub const DrawRectangle = struct { rect: Rect, color: Vec4, sprite: ?Sprite = null, rotation: f32 = 0, origin: Vec2 = .init(0, 0), }; pub const DrawLine = struct { pos1: Vec2, pos2: Vec2, color: Vec4, width: f32 }; pub const DrawText = struct { pos: Vec2, text: []const u8, font: FontId, size: f32, color: Vec4, }; push_scissor: Rect, pop_scissor: void, draw_rectangle: DrawRectangle, draw_line: DrawLine, draw_text: DrawText, push_transformation: struct { translation: Vec2, scale: Vec2 }, pop_transformation: void }; pub const AudioCommand = union(enum) { pub const Play = struct { id: AudioId, volume: f32 = 1 }; play: Play, }; 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 Frame = struct { arena: std.heap.ArenaAllocator, time_ns: Nanoseconds, dt_ns: Nanoseconds, char_mapping: std.EnumMap(KeyCode, u21), keyboard: ButtonStateSet(KeyCode), mouse_button: ButtonStateSet(MouseButton), mouse_position: ?Vec2, hide_cursor: bool, audio_commands: std.ArrayList(AudioCommand), graphics_commands: std.ArrayList(GraphicsCommand), canvas_size: ?Vec2, screen_size: Vec2, clear_color: Vec4, pub fn init(gpa: std.mem.Allocator) Frame { return Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, .dt_ns = 0, .char_mapping = .{}, .keyboard = .empty, .mouse_button = .empty, .mouse_position = null, .graphics_commands = .empty, .canvas_size = null, .clear_color = rgb(0, 0, 0), .audio_commands = .empty, .hide_cursor = false, .screen_size = .init(0, 0), }; } 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; } // --------------------- Audio ----------------------- // 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 playAudio(self: *Frame, options: AudioCommand.Play) void { self.pushAudioCommand(.{ .play = options, }); } // --------------------- Graphics ----------------------- // 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 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 fn pushScissor(self: *Frame, rect: Rect) void { self.pushGraphicsCommand(.{ .push_scissor = rect }); } pub fn popScissor(self: *Frame) void { self.pushGraphicsCommand(.{ .pop_scissor = {} }); } 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 = {} }); } pub const DrawTextOptions = struct { font: FontId, size: f32 = 16, color: Vec4 = rgb(255, 255, 255), }; pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) 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, } }); } // --------------------- Mouse ----------------------- // pub fn isMousePressed(self: Frame, button: MouseButton) bool { return self.mouse_button.pressed.contains(button); } pub fn isMouseDown(self: Frame, button: MouseButton) bool { return self.mouse_button.down.contains(button); } pub fn isMouseReleased(self: Frame, button: MouseButton) bool { return self.mouse_button.released.contains(button); } // --------------------- Keyboard ----------------------- // pub fn isKeyDown(self: Frame, key_code: KeyCode) bool { return self.keyboard.down.contains(key_code); } pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool { return self.keyboard.pressed.contains(key_code); } pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool { return self.keyboard.released.contains(key_code); } pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 { if (!self.isKeyDown(key_code)) { return null; } const pressed_at_ns = self.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 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 const Init = struct { gpa: std.mem.Allocator, seed: u64 }; pub const Callbacks = struct { pub const State = *anyopaque; pub const InitFn = *const fn(opts: *const Init) callconv(.c) State; pub const TickFn = *const fn(state: State, frame: *Frame) callconv(.c) void; pub const DeinitFn = *const fn(state: State) callconv(.c) void; pub const DebugFn = *const fn(state: State, imgui: *ImGui) callconv(.c) void; init: InitFn, tick: TickFn, deinit: DeinitFn, debug: if (build_options.has_imgui) DebugFn else void }; pub fn exportCallbacksIfNeeded( comptime init: Callbacks.InitFn, comptime tick: Callbacks.TickFn, comptime deinit: Callbacks.DeinitFn, comptime debug: Callbacks.DebugFn ) void { if (build_options.statically_linked) { return; } @export(init, .{ .name = "init", .linkage = .strong }); @export(tick, .{ .name = "tick", .linkage = .strong }); @export(deinit, .{ .name = "deinit", .linkage = .strong }); if (build_options.has_imgui) { @export(debug, .{ .name = "debug", .linkage = .strong }); } }