From 4df4f42022330593e02c5fb1c044e5ade378fddf Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 30 Dec 2025 14:22:56 +0200 Subject: [PATCH] move engine code to separate folder --- src/assets.zig | 11 + src/{ => engine}/graphics.zig | 14 +- src/{ => engine}/imgui.zig | 0 src/engine/input.zig | 239 +++++++++++++++ src/{ => engine}/math.zig | 0 src/engine/root.zig | 445 +++++++++++++++++++++++++++ src/engine/screen_scaler.zig | 63 ++++ src/entity.zig | 7 +- src/game.zig | 52 ++-- src/main.zig | 117 +------- src/window.zig | 551 ---------------------------------- 11 files changed, 797 insertions(+), 702 deletions(-) create mode 100644 src/assets.zig rename src/{ => engine}/graphics.zig (98%) rename src/{ => engine}/imgui.zig (100%) create mode 100644 src/engine/input.zig rename src/{ => engine}/math.zig (100%) create mode 100644 src/engine/root.zig create mode 100644 src/engine/screen_scaler.zig delete mode 100644 src/window.zig diff --git a/src/assets.zig b/src/assets.zig new file mode 100644 index 0000000..1c1a033 --- /dev/null +++ b/src/assets.zig @@ -0,0 +1,11 @@ + +const Assets = @This(); + +pub fn init() !Assets { + return Assets{ + }; +} + +pub fn deinit(self: *Assets) void { + _ = self; // autofix +} diff --git a/src/graphics.zig b/src/engine/graphics.zig similarity index 98% rename from src/graphics.zig rename to src/engine/graphics.zig index e54a824..7e5a961 100644 --- a/src/graphics.zig +++ b/src/engine/graphics.zig @@ -825,7 +825,7 @@ pub fn init(options: Options) !void { .disable_set_mouse_cursor = true }); - imgui.addFont(@embedFile("./assets/roboto-font/Roboto-Regular.ttf"), 16); + imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16); // make sure the fontstash atlas width/height is pow-2 const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(512 * dpi_scale)); @@ -836,12 +836,12 @@ pub fn init(options: Options) !void { assert(g_fons_context != null); font_id_map = .init(.{ - .regular = loadEmbededFont(g_fons_context, "regular", "./assets/roboto-font/Roboto-Regular.ttf"), - .italic = loadEmbededFont(g_fons_context, "italic", "./assets/roboto-font/Roboto-Italic.ttf"), - .bold = loadEmbededFont(g_fons_context, "bold", "./assets/roboto-font/Roboto-Bold.ttf"), + .regular = loadEmbededFont(g_fons_context, "regular", "../assets/roboto-font/Roboto-Regular.ttf"), + .italic = loadEmbededFont(g_fons_context, "italic", "../assets/roboto-font/Roboto-Italic.ttf"), + .bold = loadEmbededFont(g_fons_context, "bold", "../assets/roboto-font/Roboto-Bold.ttf"), }); - const tilemap = try ImageData.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png")); + const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png")); defer tilemap.deinit(); image_map = .init(.{ @@ -1057,10 +1057,6 @@ pub fn popTransform() void { sgl.popMatrix(); } -pub fn event(ev: [*c]const sapp.Event) bool { - return imgui.handleEvent(ev.*); -} - fn createProjectionMatrix() Mat4 { const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); diff --git a/src/imgui.zig b/src/engine/imgui.zig similarity index 100% rename from src/imgui.zig rename to src/engine/imgui.zig diff --git a/src/engine/input.zig b/src/engine/input.zig new file mode 100644 index 0000000..0cf6518 --- /dev/null +++ b/src/engine/input.zig @@ -0,0 +1,239 @@ +const std = @import("std"); +const sokol = @import("sokol"); + +const Engine = @import("./root.zig"); +const Nanoseconds = Engine.Nanoseconds; + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; + +const Input = @This(); + +pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { + SPACE = 32, + APOSTROPHE = 39, + COMMA = 44, + MINUS = 45, + PERIOD = 46, + SLASH = 47, + _0 = 48, + _1 = 49, + _2 = 50, + _3 = 51, + _4 = 52, + _5 = 53, + _6 = 54, + _7 = 55, + _8 = 56, + _9 = 57, + SEMICOLON = 59, + EQUAL = 61, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LEFT_BRACKET = 91, + BACKSLASH = 92, + RIGHT_BRACKET = 93, + GRAVE_ACCENT = 96, + WORLD_1 = 161, + WORLD_2 = 162, + ESCAPE = 256, + ENTER = 257, + TAB = 258, + BACKSPACE = 259, + INSERT = 260, + DELETE = 261, + RIGHT = 262, + LEFT = 263, + DOWN = 264, + UP = 265, + PAGE_UP = 266, + PAGE_DOWN = 267, + HOME = 268, + END = 269, + CAPS_LOCK = 280, + SCROLL_LOCK = 281, + NUM_LOCK = 282, + PRINT_SCREEN = 283, + PAUSE = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + F13 = 302, + F14 = 303, + F15 = 304, + F16 = 305, + F17 = 306, + F18 = 307, + F19 = 308, + F20 = 309, + F21 = 310, + F22 = 311, + F23 = 312, + F24 = 313, + F25 = 314, + KP_0 = 320, + KP_1 = 321, + KP_2 = 322, + KP_3 = 323, + KP_4 = 324, + KP_5 = 325, + KP_6 = 326, + KP_7 = 327, + KP_8 = 328, + KP_9 = 329, + KP_DECIMAL = 330, + KP_DIVIDE = 331, + KP_MULTIPLY = 332, + KP_SUBTRACT = 333, + KP_ADD = 334, + KP_ENTER = 335, + KP_EQUAL = 336, + LEFT_SHIFT = 340, + LEFT_CONTROL = 341, + LEFT_ALT = 342, + LEFT_SUPER = 343, + RIGHT_SHIFT = 344, + RIGHT_CONTROL = 345, + RIGHT_ALT = 346, + RIGHT_SUPER = 347, + MENU = 348, +}; + +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 Mouse = struct { + pub const Button = enum { + left, + right, + middle, + + pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?Button { + return switch(mouse_button) { + .LEFT => Button.left, + .RIGHT => Button.right, + .MIDDLE => Button.middle, + else => null + }; + } + }; + + position: ?Vec2, + buttons: std.EnumSet(Button), + + pub const empty = Mouse{ + .position = null, + .buttons = .initEmpty() + }; +}; + +down_keys: std.EnumSet(KeyCode), +pressed_keys: std.EnumSet(KeyCode), +released_keys: std.EnumSet(KeyCode), +pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds), + +mouse: Mouse, + +pub const empty = Input{ + .down_keys = .initEmpty(), + .pressed_keys = .initEmpty(), + .released_keys = .initEmpty(), + .pressed_keys_at = .init(.{}), + .mouse = .empty +}; + +pub fn isKeyDown(self: *Input, key_code: KeyCode) bool { + return self.down_keys.contains(key_code); +} + +pub fn getKeyDownDuration(self: *Input, frame: Engine.Frame, 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 = frame.time_ns - pressed_at_ns; + + return @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s; +} + +pub fn isKeyPressed(self: *Input, key_code: KeyCode) bool { + return self.pressed_keys.contains(key_code); +} + +pub fn isKeyReleased(self: *Input, key_code: KeyCode) bool { + return self.released_keys.contains(key_code); +} + +pub fn getKeyState(self: *Input, frame: Engine.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(frame, key_code) + }; +} diff --git a/src/math.zig b/src/engine/math.zig similarity index 100% rename from src/math.zig rename to src/engine/math.zig diff --git a/src/engine/root.zig b/src/engine/root.zig new file mode 100644 index 0000000..590bb2e --- /dev/null +++ b/src/engine/root.zig @@ -0,0 +1,445 @@ +const std = @import("std"); +const log = std.log.scoped(.engine); + +const sokol = @import("sokol"); +const sapp = sokol.app; + +pub const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; + +pub const Input = @import("./input.zig"); + +const ScreenScalar = @import("./screen_scaler.zig"); +pub const imgui = @import("./imgui.zig"); +pub const Graphics = @import("./graphics.zig"); +const tracy = @import("tracy"); +const builtin = @import("builtin"); + +const Gfx = Graphics; + +const Game = @import("../game.zig"); +const Assets = @import("../assets.zig"); + +const Engine = @This(); + +pub const Nanoseconds = u64; + +pub const Event = union(enum) { + mouse_pressed: struct { + button: Input.Mouse.Button, + position: Vec2, + }, + mouse_released: struct { + button: Input.Mouse.Button, + position: Vec2, + }, + mouse_move: Vec2, + mouse_enter: Vec2, + mouse_leave, + mouse_scroll: Vec2, + key_pressed: struct { + code: Input.KeyCode, + repeat: bool + }, + key_released: Input.KeyCode, + window_resize, + char: u21, +}; + +pub const Frame = struct { + time_ns: Nanoseconds, + dt_ns: Nanoseconds, + dt: f32, + input: *Input +}; + +allocator: std.mem.Allocator, +started_at: std.time.Instant, +mouse_inside: bool, +last_frame_at: Nanoseconds, +input: Input, + +game: Game, +assets: Assets, + +const RunOptions = struct { + window_title: [*:0]const u8 = "Game", + window_width: u31 = 640, + window_height: u31 = 480, +}; + +pub fn run(self: *Engine, opts: RunOptions) !void { + self.* = Engine{ + .allocator = undefined, + .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), + .input = .empty, + .mouse_inside = false, + .last_frame_at = 0, + .assets = undefined, + .game = undefined + }; + + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + + // TODO: Use tracy TracingAllocator + if (builtin.mode == .Debug) { + self.allocator = debug_allocator.allocator(); + } else { + self.allocator = std.heap.smp_allocator; + } + + tracy.setThreadName("Main"); + + if (builtin.os.tag == .linux) { + var sa: std.posix.Sigaction = .{ + .handler = .{ .handler = posixSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.RESTART, + }; + std.posix.sigaction(std.posix.SIG.INT, &sa, null); + } + + sapp.run(.{ + .init_userdata_cb = sokolInitCallback, + .frame_userdata_cb = sokolFrameCallback, + .cleanup_userdata_cb = sokolCleanupCallback, + .event_userdata_cb = sokolEventCallback, + .user_data = self, + .width = opts.window_width, + .height = opts.window_height, + .icon = .{ .sokol_default = true }, + .window_title = opts.window_title, + .logger = .{ .func = sokolLogCallback }, + }); +} + +fn sokolInit(self: *Engine) !void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + try Gfx.init(.{ + .allocator = self.allocator, + .logger = .{ .func = sokolLogCallback } + }); + + self.assets = try Assets.init(); + self.game = try Game.init(self.allocator, &self.assets); +} + +fn sokolCleanup(self: *Engine) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + self.game.deinit(); + self.assets.deinit(); + Gfx.deinit(); +} + +fn sokolFrame(self: *Engine) !void { + tracy.frameMark(); + + const time_passed = self.timePassed(); + defer self.last_frame_at = time_passed; + const dt_ns = time_passed - self.last_frame_at; + + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + Gfx.beginFrame(); + defer Gfx.endFrame(); + + { + const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); + const ctx = ScreenScalar.push(window_size, self.game.canvas_size); + defer ctx.pop(); + + try self.game.tick(Frame{ + .time_ns = time_passed, + .dt_ns = dt_ns, + .dt = @as(f32, @floatFromInt(dt_ns)) / std.time.ns_per_s, + .input = &self.input + }); + } + + try self.game.debug(); + + self.input.pressed_keys = .initEmpty(); + self.input.released_keys = .initEmpty(); +} + +fn timePassed(self: *Engine) Nanoseconds { + const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); + return now.since(self.started_at); +} + +fn event(self: *Engine, e: Event) !void { + const input = &self.input; + + switch (e) { + .key_pressed => |opts| { + if (!opts.repeat) { + input.pressed_keys_at.put(opts.code, self.timePassed()); + input.pressed_keys.insert(opts.code); + input.down_keys.insert(opts.code); + } + }, + .key_released => |key_code| { + input.down_keys.remove(key_code); + input.released_keys.insert(key_code); + input.pressed_keys_at.remove(key_code); + }, + .mouse_leave => { + var iter = input.down_keys.iterator(); + while (iter.next()) |key_code| { + input.released_keys.insert(key_code); + } + input.down_keys = .initEmpty(); + input.pressed_keys_at = .init(.{}); + + input.mouse = .empty; + }, + .mouse_enter => |pos| { + input.mouse.position = pos; + }, + .mouse_move => |pos| { + input.mouse.position = pos; + }, + .mouse_pressed => |opts| { + input.mouse.position = opts.position; + input.mouse.buttons.insert(opts.button); + }, + .mouse_released => |opts| { + input.mouse.position = opts.position; + input.mouse.buttons.remove(opts.button); + }, + else => {} + } +} + +fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + const e = e_ptr.*; + const MouseButton = Input.Mouse.Button; + + if (imgui.handleEvent(e)) { + if (self.mouse_inside) { + try self.event(Event{ + .mouse_leave = {} + }); + } + self.mouse_inside = false; + return true; + } + + blk: switch (e.type) { + .MOUSE_DOWN => { + const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try self.event(Event{ + .mouse_pressed = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_UP => { + const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try self.event(Event{ + .mouse_released = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_MOVE => { + if (!self.mouse_inside) { + try self.event(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } else { + try self.event(Event{ + .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + self.mouse_inside = true; + return true; + }, + .MOUSE_ENTER => { + if (!self.mouse_inside) { + try self.event(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + self.mouse_inside = true; + return true; + }, + .RESIZED => { + if (self.mouse_inside) { + try self.event(Event{ + .mouse_leave = {} + }); + } + + try self.event(Event{ + .window_resize = {} + }); + + self.mouse_inside = false; + return true; + }, + .MOUSE_LEAVE => { + if (self.mouse_inside) { + try self.event(Event{ + .mouse_leave = {} + }); + } + + self.mouse_inside = false; + return true; + }, + .MOUSE_SCROLL => { + try self.event(Event{ + .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) + }); + + return true; + }, + .KEY_DOWN => { + try self.event(Event{ + .key_pressed = .{ + .code = @enumFromInt(@intFromEnum(e.key_code)), + .repeat = e.key_repeat + } + }); + + return true; + }, + .KEY_UP => { + try self.event(Event{ + .key_released = @enumFromInt(@intFromEnum(e.key_code)) + }); + + return true; + }, + .CHAR => { + try self.event(Event{ + .char = @intCast(e.char_code) + }); + + return true; + }, + .QUIT_REQUESTED => { + // TODO: handle quit request. Maybe show confirmation window in certain cases. + }, + else => {} + } + + return false; +} + +fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: { + log.err("sokolEvent() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + break :blk false; + }; + + if (consume_event) { + sapp.consumeEvent(); + } +} + +fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolCleanup(); +} + +fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolInit() catch |e| { + log.err("sokolInit() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + sapp.requestQuit(); + }; +} + +fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void { + const engine: *Engine = @alignCast(@ptrCast(userdata)); + + engine.sokolFrame() catch |e| { + log.err("sokolFrame() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + sapp.requestQuit(); + }; +} + +fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { + const log_sokol = std.log.scoped(.sokol); + + if (log_level == 0) { + log_sokol.err(format, args); + } else if (log_level == 1) { + log_sokol.err(format, args); + } else if (log_level == 2) { + log_sokol.warn(format, args); + } else { + log_sokol.info(format, args); + } +} + +fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { + return std.mem.span(c_str); +} + +fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { + _ = user_data; + + if (filename != null) { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}:{}: {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + std.fs.path.basename(cStrToZig(filename orelse "-")), + line_nr, + cStrToZig(message orelse "") + } + ); + } else { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + cStrToZig(message orelse "") + } + ); + } +} + +fn posixSignalHandler(sig: i32) callconv(.c) void { + _ = sig; + sapp.requestQuit(); +} diff --git a/src/engine/screen_scaler.zig b/src/engine/screen_scaler.zig new file mode 100644 index 0000000..3d1ad62 --- /dev/null +++ b/src/engine/screen_scaler.zig @@ -0,0 +1,63 @@ +const Gfx = @import("./graphics.zig"); + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; +const rgb = Math.rgb; + +const ScreenScalar = @This(); + +window_size: Vec2, +translation: Vec2, +scale: f32, + +pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar { + // TODO: Render to a lower resolution instead of scaling. + // To avoid pixel bleeding in spritesheet artifacts + const scale = @floor(@min( + window_size.x / canvas_size.x, + window_size.y / canvas_size.y, + )); + + var translation: Vec2 = Vec2.sub(window_size, canvas_size.multiplyScalar(scale)).multiplyScalar(0.5); + translation.x = @round(translation.x); + translation.y = @round(translation.y); + + Gfx.pushTransform(translation, scale); + + return ScreenScalar{ + .window_size = window_size, + .translation = translation, + .scale = scale + }; +} + +pub fn pop(self: ScreenScalar) void { + Gfx.popTransform(); + + const bg_color = rgb(0, 0, 0); + const filler_size = self.translation; + + Gfx.drawRectangle( + .init(0, 0), + .init(self.window_size.x, filler_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(0, self.window_size.y - filler_size.y), + .init(self.window_size.x, filler_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(0, 0), + .init(filler_size.x, self.window_size.y), + bg_color + ); + + Gfx.drawRectangle( + .init(self.window_size.x - filler_size.x, 0), + .init(filler_size.x, self.window_size.y), + bg_color + ); +} diff --git a/src/entity.zig b/src/entity.zig index 6eb2294..c85e682 100644 --- a/src/entity.zig +++ b/src/entity.zig @@ -1,9 +1,8 @@ const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; -const Gfx = @import("./graphics.zig"); - -const Math = @import("./math.zig"); -const Vec2 = Math.Vec2; +const Engine = @import("./engine/root.zig"); +const Vec2 = Engine.Math.Vec2; +const Gfx = Engine.Graphics; const Entity = @This(); diff --git a/src/game.zig b/src/game.zig index b2a2023..c5c18e0 100644 --- a/src/game.zig +++ b/src/game.zig @@ -2,28 +2,30 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Math = @import("./math.zig"); -const Vec2 = Math.Vec2; -const Vec4 = Math.Vec4; -const rgb = Math.rgb; -const rgb_hex = Math.rgb_hex; +const Engine = @import("./engine/root.zig"); +const imgui = Engine.imgui; +const Gfx = Engine.Graphics; +const Vec2 = Engine.Math.Vec2; +const Vec4 = Engine.Math.Vec4; +const rgb = Engine.Math.rgb; +const rgb_hex = Engine.Math.rgb_hex; 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 Assets = @import("./assets.zig"); const tiled = @import("tiled"); const Game = @This(); +const KeyState = Engine.Input.KeyState; + pub const Input = struct { dt: f64, - move_up: Window.KeyState, - move_down: Window.KeyState, - move_left: Window.KeyState, - move_right: Window.KeyState, + move_up: KeyState, + move_down: KeyState, + move_left: KeyState, + move_right: KeyState, restart: bool }; @@ -80,7 +82,8 @@ finale: bool = false, finale_timer: ?Timer.Id = null, finale_counter: u32 = 0, -pub fn init(gpa: Allocator) !Game { +pub fn init(gpa: Allocator, assets: *Assets) !Game { + _ = assets; // autofix var self = Game{ .gpa = gpa, .canvas_size = (Vec2.init(20, 15)), @@ -323,17 +326,16 @@ fn moveEntity(self: *Game, entity_id: Entity.Id, dir: Vec2) bool { return true; } -pub fn getInput(self: *Game, window: *Window) Input { +pub fn getInput(self: *Game, frame: Engine.Frame) Input { _ = self; // autofix - const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s; - + const input = frame.input; return Input{ - .dt = dt, - .move_up = window.getKeyState(.W), - .move_down = window.getKeyState(.S), - .move_left = window.getKeyState(.A), - .move_right = window.getKeyState(.D), - .restart = window.isKeyPressed(.R) + .dt = frame.dt, + .move_up = input.getKeyState(frame, .W), + .move_down = input.getKeyState(frame, .S), + .move_left = input.getKeyState(frame, .A), + .move_right = input.getKeyState(frame, .D), + .restart = input.isKeyPressed(.R) }; } @@ -410,7 +412,7 @@ pub fn tickLevel(self: *Game, input: Input) !void { var move: Vec2 = .init(0, 0); if (can_move) { - const repeat_options = Window.KeyState.RepeatOptions{ + const repeat_options = KeyState.RepeatOptions{ .first_at = 0.3, .period = 0.1 }; @@ -541,7 +543,9 @@ pub fn tickFinale(self: *Game) !void { } } -pub fn tick(self: *Game, input: Input) !void { +pub fn tick(self: *Game, frame: Engine.Frame) !void { + const input = self.getInput(frame); + const bg_color = rgb_hex("#222323").?; Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color); diff --git a/src/main.zig b/src/main.zig index ae2c29b..87c9173 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,118 +1,7 @@ -const std = @import("std"); -const tracy = @import("tracy"); -const Gfx = @import("./graphics.zig"); -const builtin = @import("builtin"); - -const Window = @import("./window.zig"); -const Event = Window.Event; -const MouseButton = Window.MouseButton; - -const Math = @import("./math.zig"); -const Vec2 = Math.Vec2; - -const sokol = @import("sokol"); -const slog = sokol.log; -const sg = sokol.gfx; -const sapp = sokol.app; -const sglue = sokol.glue; - -const log = std.log.scoped(.main); - -var gpa: std.mem.Allocator = undefined; - -var window: Window = undefined; -var event_queue_full_shown = false; - -fn signalHandler(sig: i32) callconv(.c) void { - _ = sig; - sapp.requestQuit(); -} - -export fn init() void { - var zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - Window.init(&window, gpa) catch |e| { - log.err("init() failed: {}", .{e}); - sapp.requestQuit(); - }; -} - -export fn frame() void { - tracy.frameMark(); - - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - window.frame() catch |e| { - log.err("frame() failed: {}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - sapp.requestQuit(); - }; -} - -export fn cleanup() void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - window.deinit(); -} - -export fn event(e_ptr: [*c]const sapp.Event) void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - - const consumed_event = window.event(e_ptr) catch |e| switch (e) { - error.EventQueueFull => blk: { - if (!event_queue_full_shown) { - log.warn("Event queue is full! Frame is taking too long to process", .{}); - event_queue_full_shown = true; - } - break :blk false; - }, - }; - - if (consumed_event) { - event_queue_full_shown = false; - sapp.consumeEvent(); - } -} +const Engine = @import("./engine/root.zig"); pub fn main() !void { - var debug_allocator: std.heap.DebugAllocator(.{}) = .init; - defer _ = debug_allocator.deinit(); - - if (builtin.mode == .Debug) { - gpa = debug_allocator.allocator(); - } else { - gpa = std.heap.smp_allocator; - } - - // TODO: Use tracy TracingAllocator - - tracy.setThreadName("Main"); - - if (builtin.os.tag == .linux) { - var sa: std.posix.Sigaction = .{ - .handler = .{ .handler = signalHandler }, - .mask = std.posix.sigemptyset(), - .flags = std.posix.SA.RESTART, - }; - std.posix.sigaction(std.posix.SIG.INT, &sa, null); - } - - sapp.run(.{ - .init_cb = init, - .frame_cb = frame, - .cleanup_cb = cleanup, - .event_cb = event, - .width = 640, - .height = 480, - .icon = .{ .sokol_default = true }, - .window_title = "Game", - .logger = .{ .func = Window.sokolLogCallback }, - }); + var engine: Engine = undefined; + try engine.run(.{}); } diff --git a/src/window.zig b/src/window.zig deleted file mode 100644 index 7bd8bd3..0000000 --- a/src/window.zig +++ /dev/null @@ -1,551 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const sokol = @import("sokol"); -const sapp = sokol.app; - -const Gfx = @import("./graphics.zig"); -const tiled = @import("tiled"); - -const Math = @import("./math.zig"); -const Vec2 = Math.Vec2; -const rgb = Math.rgb; - -const Game = @import("./game.zig"); - -const Window = @This(); - -const log = std.log.scoped(.window); - -pub const MouseButton = enum { - left, - right, - middle, - - pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton { - return switch(mouse_button) { - .LEFT => MouseButton.left, - .RIGHT => MouseButton.right, - .MIDDLE => MouseButton.middle, - else => null - }; - } -}; - -pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { - SPACE = 32, - APOSTROPHE = 39, - COMMA = 44, - MINUS = 45, - PERIOD = 46, - SLASH = 47, - _0 = 48, - _1 = 49, - _2 = 50, - _3 = 51, - _4 = 52, - _5 = 53, - _6 = 54, - _7 = 55, - _8 = 56, - _9 = 57, - SEMICOLON = 59, - EQUAL = 61, - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90, - LEFT_BRACKET = 91, - BACKSLASH = 92, - RIGHT_BRACKET = 93, - GRAVE_ACCENT = 96, - WORLD_1 = 161, - WORLD_2 = 162, - ESCAPE = 256, - ENTER = 257, - TAB = 258, - BACKSPACE = 259, - INSERT = 260, - DELETE = 261, - RIGHT = 262, - LEFT = 263, - DOWN = 264, - UP = 265, - PAGE_UP = 266, - PAGE_DOWN = 267, - HOME = 268, - END = 269, - CAPS_LOCK = 280, - SCROLL_LOCK = 281, - NUM_LOCK = 282, - PRINT_SCREEN = 283, - PAUSE = 284, - F1 = 290, - F2 = 291, - F3 = 292, - F4 = 293, - F5 = 294, - F6 = 295, - F7 = 296, - F8 = 297, - F9 = 298, - F10 = 299, - F11 = 300, - F12 = 301, - F13 = 302, - F14 = 303, - F15 = 304, - F16 = 305, - F17 = 306, - F18 = 307, - F19 = 308, - F20 = 309, - F21 = 310, - F22 = 311, - F23 = 312, - F24 = 313, - F25 = 314, - KP_0 = 320, - KP_1 = 321, - KP_2 = 322, - KP_3 = 323, - KP_4 = 324, - KP_5 = 325, - KP_6 = 326, - KP_7 = 327, - KP_8 = 328, - KP_9 = 329, - KP_DECIMAL = 330, - KP_DIVIDE = 331, - KP_MULTIPLY = 332, - KP_SUBTRACT = 333, - KP_ADD = 334, - KP_ENTER = 335, - KP_EQUAL = 336, - LEFT_SHIFT = 340, - LEFT_CONTROL = 341, - LEFT_ALT = 342, - LEFT_SUPER = 343, - RIGHT_SHIFT = 344, - RIGHT_CONTROL = 345, - RIGHT_ALT = 346, - RIGHT_SUPER = 347, - MENU = 348, -}; - -pub const Event = union(enum) { - mouse_pressed: struct { - button: MouseButton, - position: Vec2, - }, - mouse_released: struct { - button: MouseButton, - position: Vec2, - }, - mouse_move: Vec2, - mouse_enter: Vec2, - mouse_leave, - mouse_scroll: Vec2, - key_pressed: struct { - code: KeyCode, - repeat: bool - }, - key_released: KeyCode, - window_resize, - 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, -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); - try events.ensureTotalCapacityPrecise(gpa, 50); - - try Gfx.init(.{ - .allocator = gpa, - .logger = .{ .func = sokolLogCallback } - }); - - var game = try Game.init(gpa); - errdefer game.deinit(); - - self.* = Window{ - .gpa = gpa, - .events = events, - .last_frame_at_ns = std.time.nanoTimestamp(), - .frame_dt_ns = 0, - .time_ns = 0, - .game = game - }; -} - -pub fn deinit(self: *Window) void { - const gpa = self.gpa; - - self.game.deinit(); - self.events.deinit(gpa); - - Gfx.deinit(); -} - -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 => {} - } - } - self.events.clearRetainingCapacity(); - - // TODO: Render to a lower resolution instead of scaling. - // To avoid pixel bleeding in spritesheet artifacts - const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); - const scale = @floor(@min( - window_size.x / self.game.canvas_size.x, - window_size.y / self.game.canvas_size.y, - )); - - var filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5); - filler_size.x = @round(filler_size.x); - filler_size.y = @round(filler_size.y); - - const input = self.game.getInput(self); - - { - Gfx.pushTransform(filler_size, scale); - defer Gfx.popTransform(); - - try self.game.tick(input); - } - - const bg_color = rgb(0, 0, 0); - - Gfx.drawRectangle( - .init(0, 0), - .init(window_size.x, filler_size.y), - bg_color - ); - - Gfx.drawRectangle( - .init(0, window_size.y - filler_size.y), - .init(window_size.x, filler_size.y), - bg_color - ); - - Gfx.drawRectangle( - .init(0, 0), - .init(filler_size.x, window_size.y), - bg_color - ); - - Gfx.drawRectangle( - .init(window_size.x - filler_size.x, 0), - .init(filler_size.x, window_size.y), - bg_color - ); - - 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; -} - -pub fn event(self: *Window, e_ptr: [*c]const sapp.Event) !bool { - if (Gfx.event(e_ptr)) { - try self.appendEvent(Event{ - .mouse_leave = {} - }); - self.mouse_inside = false; - return true; - } - - const e = e_ptr.*; - blk: switch (e.type) { - .MOUSE_DOWN => { - const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try self.appendEvent(Event{ - .mouse_pressed = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_UP => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try self.appendEvent(Event{ - .mouse_released = .{ - .button = mouse_button, - .position = Vec2.init(e.mouse_x, e.mouse_y) - } - }); - - return true; - }, - .MOUSE_MOVE => { - if (!self.mouse_inside) { - try self.appendEvent(Event{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - - try self.appendEvent(Event{ - .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) - }); - - self.mouse_inside = true; - return true; - }, - .MOUSE_ENTER => { - if (!self.mouse_inside) { - try self.appendEvent(Event{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } - - self.mouse_inside = true; - return true; - }, - .RESIZED => { - if (self.mouse_inside) { - try self.appendEvent(Event{ - .mouse_leave = {} - }); - } - - try self.appendEvent(Event{ - .window_resize = {} - }); - - self.mouse_inside = false; - return true; - }, - .MOUSE_LEAVE => { - if (self.mouse_inside) { - try self.appendEvent(Event{ - .mouse_leave = {} - }); - } - - self.mouse_inside = false; - return true; - }, - .MOUSE_SCROLL => { - try self.appendEvent(Event{ - .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) - }); - - return true; - }, - .KEY_DOWN => { - try self.appendEvent(Event{ - .key_pressed = .{ - .code = @enumFromInt(@intFromEnum(e.key_code)), - .repeat = e.key_repeat - } - }); - - return true; - }, - .KEY_UP => { - try self.appendEvent(Event{ - .key_released = @enumFromInt(@intFromEnum(e.key_code)) - }); - - return true; - }, - .CHAR => { - try self.appendEvent(Event{ - .char = @intCast(e.char_code) - }); - - return true; - }, - .QUIT_REQUESTED => { - // TODO: handle quit request. Maybe show confirmation window in certain cases. - }, - else => {} - } - - return false; -} - -fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { - return std.mem.span(c_str); -} - -fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { - const log_sokol = std.log.scoped(.sokol); - - if (log_level == 0) { - log_sokol.err(format, args); - } else if (log_level == 1) { - log_sokol.err(format, args); - } else if (log_level == 2) { - log_sokol.warn(format, args); - } else { - log_sokol.info(format, args); - } -} - -pub fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { - _ = user_data; - - if (filename != null) { - const format = "[{s}][id:{}] {s}:{}: {s}"; - const args = .{ - cStrToZig(tag orelse "-"), - log_item, - std.fs.path.basename(cStrToZig(filename orelse "-")), - line_nr, - cStrToZig(message orelse "") - }; - - sokolLogFmt(log_level, format, args); - } else { - const format = "[{s}][id:{}] {s}"; - const args = .{ - cStrToZig(tag orelse "-"), - log_item, - cStrToZig(message orelse "") - }; - - sokolLogFmt(log_level, format, args); - } -}