add hot reload

This commit is contained in:
Rokas Puzonas 2026-02-08 21:55:59 +02:00
parent daeb07379f
commit 407da9e009
10 changed files with 898 additions and 618 deletions

View File

@ -21,6 +21,7 @@ pub fn build(b: *std.Build) !void {
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
.win32_png_icon = b.path("src/assets/icon.png"),
.hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"),
});
{

View File

@ -14,6 +14,7 @@ const InitOptions = struct {
dep_engine: *std.Build.Dependency,
statically_linked: ?bool = null,
hot_reload: ?bool = null,
has_imgui: ?bool = null,
has_tracy: ?bool = null,
@ -26,57 +27,61 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
const isWasm = opts.target.result.cpu.arch.isWasm();
var has_tracy = false;
var hot_reload = false;
if (!isWasm) {
hot_reload = opts.hot_reload orelse (opts.optimize == .Debug);
has_tracy = opts.has_tracy orelse (opts.optimize == .Debug);
}
const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug);
const statically_linked = opts.statically_linked orelse !hot_reload;
var build_options = b.addOptions();
build_options.addOption(bool, "has_imgui", has_imgui);
build_options.addOption(bool, "has_tracy", has_tracy);
build_options.addOption(bool, "hot_reload", hot_reload);
build_options.addOption(bool, "statically_linked", statically_linked);
const engine_lib = b.createModule(.{
.root_source_file = b.path("src/lib/root.zig")
});
engine_lib.addOptions("build_options", build_options);
opts.root_module.addImport("engine", engine_lib);
const engine_module = createEngineModule(b, .{
const runtime_module = createRuntimeModule(b, .{
.target = opts.target,
.optimize = opts.optimize,
.has_tracy = opts.has_tracy,
.has_imgui = opts.has_imgui,
.hot_reload = hot_reload
.has_tracy = has_tracy,
.has_imgui = has_imgui
});
engine_module.root_module.addImport("lib", engine_lib);
if (!hot_reload) {
engine_module.root_module.addImport("game", opts.root_module);
runtime_module.root_module.addImport("lib", engine_lib);
if (statically_linked) {
runtime_module.root_module.addImport("game", opts.root_module);
}
const mod_main = b.createModule(.{
.root_source_file = b.path("src/runtime/main.zig"),
.target = opts.target,
.optimize = opts.optimize,
.link_libc = true
});
mod_main.addImport("engine", engine_module.root_module);
var run_cmd_step: *std.Build.Step = undefined;
if (isWasm) {
const build_step = buildWasm(b, .{
.name = "index",
.root_module = mod_main,
.dep_sokol = engine_module.dep_sokol,
.root_module = runtime_module.root_module,
.dep_sokol = runtime_module.dep_sokol,
.outer_builder = outer_builder
});
outer_builder.getInstallStep().dependOn(build_step);
const dep_sokol = engine_module.dep_sokol;
const dep_sokol = runtime_module.dep_sokol;
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
emrun_step.step.dependOn(build_step);
run_cmd_step = &emrun_step.step;
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
} else {
runtime_module.root_module.link_libc = true;
const exe = buildNative(b, .{
.name = opts.name,
.root_module = mod_main,
.root_module = runtime_module.root_module,
.win32_has_console = opts.win32_has_console,
.win32_png_icon = opts.win32_png_icon
});
@ -93,10 +98,15 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
const game_lib = b.addLibrary(.{
.name = "game",
.root_module = opts.root_module,
.linkage = .dynamic
.linkage = .dynamic,
});
const install_game_lib = b.addInstallArtifact(game_lib, .{});
run_cmd.addFileArg(game_lib.getEmittedBin());
run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename));
run_cmd.step.dependOn(&install_game_lib.step);
var game_lib_step = outer_builder.step("game-lib", "Build game dynamic library");
game_lib_step.dependOn(&install_game_lib.step);
}
run_cmd_step = &run_cmd.step;
}
@ -106,7 +116,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
run_step.dependOn(run_cmd_step);
}
const EngineModule = struct {
const RuntimeModule = struct {
root_module: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
@ -114,23 +124,14 @@ const EngineModule = struct {
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
has_imgui: ?bool = null,
has_tracy: ?bool = null,
hot_reload: bool
has_imgui: bool,
has_tracy: bool,
};
};
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug);
var has_tracy = opts.has_tracy orelse (opts.optimize == .Debug);
const isWasm = opts.target.result.cpu.arch.isWasm();
if (isWasm) {
has_tracy = false;
}
fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule {
const mod = b.createModule(.{
.root_source_file = b.path("src/runtime/root.zig"),
.root_source_file = b.path("src/runtime/main.zig"),
.target = opts.target,
.optimize = opts.optimize,
.link_libc = true
@ -139,12 +140,12 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
const dep_sokol = b.dependency("sokol", .{
.target = opts.target,
.optimize = opts.optimize,
.with_sokol_imgui = has_imgui,
.with_sokol_imgui = opts.has_imgui,
});
mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
mod.addImport("sokol", dep_sokol.module("sokol"));
if (has_imgui) {
if (opts.has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = opts.target,
.optimize = opts.optimize,
@ -159,10 +160,10 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
const dep_tracy = b.dependency("tracy", .{
.target = opts.target,
.optimize = opts.optimize,
.tracy_enable = has_tracy,
.tracy_enable = opts.has_tracy,
.tracy_only_localhost = true
});
if (has_tracy) {
if (opts.has_tracy) {
mod.linkLibrary(dep_tracy.artifact("tracy"));
}
mod.addImport("tracy", dep_tracy.module("tracy"));
@ -206,13 +207,7 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
// // TODO: Define buid config for wasm
// }
var options = b.addOptions();
options.addOption(bool, "has_imgui", has_imgui);
options.addOption(bool, "has_tracy", has_tracy);
options.addOption(bool, "hot_reload", opts.hot_reload);
mod.addOptions("build_options", options);
return EngineModule{
return RuntimeModule{
.root_module = mod,
.dep_sokol = dep_sokol
};
@ -306,7 +301,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = b.path("src/shell.html"),
.shell_file_path = b.path("src/runtime/shell.html"),
}) catch unreachable;
return &link_step.step;

View File

@ -1,6 +1,7 @@
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");
@ -285,10 +286,8 @@ pub const Frame = struct {
screen_size: Vec2,
clear_color: Vec4,
show_debug: bool,
pub fn init(self: *Frame, gpa: std.mem.Allocator) void {
self.* = Frame{
pub fn init(gpa: std.mem.Allocator) Frame {
return Frame{
.arena = std.heap.ArenaAllocator.init(gpa),
.time_ns = 0,
.dt_ns = 0,
@ -299,7 +298,6 @@ pub const Frame = struct {
.graphics_commands = .empty,
.canvas_size = null,
.clear_color = rgb(0, 0, 0),
.show_debug = false,
.audio_commands = .empty,
.hide_cursor = false,
.screen_size = .init(0, 0),
@ -471,3 +469,35 @@ 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 });
}
}

View File

@ -13,7 +13,7 @@ pub const Mixer = @import("./mixer.zig");
pub const Command = Mixer.Command;
const Nanoseconds = @import("../root.zig").Nanoseconds;
const Nanoseconds = @import("lib").Nanoseconds;
const sokol = @import("sokol");
const saudio = sokol.audio;

View File

@ -0,0 +1,788 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const assert = std.debug.assert;
const sokol = @import("sokol");
const sapp = sokol.app;
pub const Input = @import("./input.zig");
const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig");
pub const Graphics = @import("./graphics.zig");
pub const Audio = @import("./audio/root.zig");
const tracy = @import("tracy");
const builtin = @import("builtin");
const STBImage = @import("stb_image");
const Gfx = Graphics;
const Lib = @import("lib");
const build_options = Lib.build_options;
const debug_keybinds = (builtin.mode == .Debug);
const GameCallbacks = Lib.Callbacks;
pub const Math = Lib.Math;
pub const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const GameLinking = struct {
kind: union(enum) {
static,
dynamic: struct {
fan_fd: std.os.linux.fd_t,
path: []const u8,
dir_path: [:0]const u8,
lib: std.DynLib,
last_event_at: ?std.time.Instant,
ignore_next_event: bool,
reload_index: u32
},
},
callbacks: GameCallbacks,
// TODO: Make this configurable
const event_debounce_ns = std.time.ns_per_ms * 150;
pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking {
if (!build_options.hot_reload) {
return error.NotSupported;
}
const dir_path_z = try gpa.dupeZ(u8, std.fs.path.dirname(path) orelse ".");
errdefer gpa.free(dir_path_z);
const path_dupe = try gpa.dupe(u8, path);
errdefer gpa.free(path_dupe);
const fan_fd: std.c.fd_t = @bitCast(@as(u32, @truncate(
std.os.linux.fanotify_init(.{
.REPORT_FID = true,
.REPORT_DIR_FID = true,
.REPORT_NAME = true,
.NONBLOCK = true
}, @intFromEnum(std.posix.ACCMODE.RDONLY))
)));
if (fan_fd == -1) {
return error.fanotify_init;
}
const mark_err = std.os.linux.fanotify_mark(
@intCast(fan_fd),
.{ .ADD = true, .ONLYDIR = true },
.{
.CLOSE_WRITE = true,
.CLOSE_NOWRITE = true,
.CREATE = true,
.MOVED_TO = true,
.EVENT_ON_CHILD = true,
.ONDIR = true,
},
@intCast(std.c.AT.FDCWD),
dir_path_z
);
assert(mark_err == 0);
var lib = try std.DynLib.open(path);
const callbacks = try getCallbacksFromLibrary(&lib);
return GameLinking{
.kind = .{
.dynamic = .{
.fan_fd = fan_fd,
.dir_path = dir_path_z,
.path = path_dupe,
.lib = lib,
.last_event_at = null,
.reload_index = 0,
.ignore_next_event = false
}
},
.callbacks = callbacks
};
}
pub fn initStatic() !GameLinking {
if (!build_options.statically_linked) {
return error.NotStaticallyLinked;
}
const game = @import("game");
return GameLinking{
.kind = .static,
.callbacks = GameCallbacks{
.init = game.init,
.deinit = game.deinit,
.tick = game.tick,
.debug = if (build_options.has_imgui) game.debug else {},
}
};
}
pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void {
if (build_options.hot_reload and self.kind == .dynamic) {
self.kind.dynamic.lib.close();
gpa.free(self.kind.dynamic.dir_path);
gpa.free(self.kind.dynamic.path);
std.posix.close(self.kind.dynamic.fan_fd);
}
}
fn getCallbacksFromLibrary(lib: *std.DynLib) !GameCallbacks {
return GameCallbacks{
.init = lib.lookup(GameCallbacks.InitFn, "init") orelse return error.MissingFunction,
.tick = lib.lookup(GameCallbacks.TickFn, "tick") orelse return error.MissingFunction,
.deinit = lib.lookup(GameCallbacks.DeinitFn, "deinit") orelse return error.MissingFunction,
.debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.DebugFn, "debug") orelse return error.MissingFunction else {},
};
}
pub fn check(self: *GameLinking) !void {
if (self.kind == .static) {
return;
}
if (!build_options.hot_reload) {
return;
}
const fanotify = std.os.linux.fanotify;
const M = fanotify.event_metadata;
const fan_fd = self.kind.dynamic.fan_fd;
var need_to_reload = false;
var events_buf: [256 + 4096]u8 = undefined;
while (true) {
var len = std.posix.read(fan_fd, &events_buf) catch |err| switch (err) {
error.WouldBlock => break,
else => |e| return e,
};
var meta: [*]align(1) M = @ptrCast(&events_buf);
while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
len -= meta[0].event_len;
meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
}) {
assert(meta[0].vers == M.VERSION);
if (meta[0].mask.Q_OVERFLOW) {
need_to_reload = true;
// TODO:
// std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
break;
}
var is_lib_event = false;
const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
switch (fid.hdr.info_type) {
.DFID_NAME => {
const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
const file_name = std.mem.span(file_name_z);
const lib_name = std.fs.path.basename(self.kind.dynamic.path);
is_lib_event = std.mem.eql(u8,file_name, lib_name);
},
else => |t| log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
}
if (is_lib_event) {
need_to_reload = true;
}
}
}
if (need_to_reload) {
if (self.kind.dynamic.ignore_next_event) {
self.kind.dynamic.ignore_next_event = false;
} else {
self.kind.dynamic.last_event_at = try std.time.Instant.now();
}
}
if (self.kind.dynamic.last_event_at) |last_event_at| {
const now = try std.time.Instant.now();
const time_passed = now.since(last_event_at);
if (time_passed > event_debounce_ns) {
self.kind.dynamic.last_event_at = null;
var tmp_dir = try std.fs.openDirAbsolute("/tmp", .{});
defer tmp_dir.close();
var new_filename_buffer: [std.fs.max_name_bytes]u8 = undefined;
const tmp_filename = try std.fmt.bufPrint(
&new_filename_buffer,
"{s}_{}.so",
.{std.fs.path.stem(self.kind.dynamic.path), self.kind.dynamic.reload_index}
);
try std.fs.Dir.copyFile(
std.fs.cwd(), self.kind.dynamic.path,
tmp_dir, tmp_filename,
.{}
);
var tmp_path_buffer: [std.fs.max_path_bytes]u8 = undefined;
const tmp_path = try std.fmt.bufPrint(
&tmp_path_buffer,
"/tmp/{s}",
.{tmp_filename}
);
var new_lib = try std.DynLib.open(tmp_path);
const new_callbacks = try getCallbacksFromLibrary(&new_lib);
try tmp_dir.deleteFile(tmp_filename);
log.debug("Reload game code", .{});
self.kind.dynamic.lib.close();
self.kind.dynamic.lib = new_lib;
self.callbacks = new_callbacks;
self.kind.dynamic.reload_index += 1;
self.kind.dynamic.ignore_next_event = true;
}
}
}
};
const GameState = struct {
state: GameCallbacks.State,
input: Input,
frame: Lib.Frame,
started_at: std.time.Instant,
last_frame_at: Lib.Nanoseconds,
previous_tick_canvas_size: ?Vec2,
seed: u64,
const Options = struct {
gpa: std.mem.Allocator,
seed: u64
};
pub fn init(opts: Options) GameState {
return GameState{
.input = .initial,
.frame = .init(opts.gpa),
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.last_frame_at = 0,
.previous_tick_canvas_size = null,
.seed = opts.seed,
.state = undefined
};
}
pub fn deinit(self: *GameState) void {
self.frame.deinit();
}
};
const Engine = @This();
allocator: std.mem.Allocator,
graphics: Graphics,
game_linking: GameLinking,
game_state: GameState,
seed: u64,
show_debug: bool,
restart_game: bool,
const RunOptions = struct {
allocator: std.mem.Allocator,
game_library_path: ?[]const u8 = null
};
pub fn run(self: *Engine, opts: RunOptions) !void {
self.* = Engine{
.allocator = opts.allocator,
.graphics = undefined,
.game_state = undefined,
.game_linking = undefined,
.show_debug = false,
.restart_game = false,
.seed = @bitCast(std.time.milliTimestamp()),
};
if (opts.game_library_path) |game_library_path| {
self.game_linking = try .initDynamic(self.allocator, game_library_path);
} else {
self.game_linking = try .initStatic();
}
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);
}
log.debug("Build options:", .{});
inline for (@typeInfo(build_options).@"struct".decls) |decl| {
log.debug("- {s}: {}", .{decl.name, @field(build_options, decl.name)});
}
// TODO: Don't hard code icon path, allow changing through options
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
// defer icon_data.deinit();
var icon: sapp.IconDesc = .{};
icon.sokol_default = true;
// TODO:
// icon.images[0] = .{
// .width = @intCast(icon_data.width),
// .height = @intCast(icon_data.height),
// .pixels = .{
// .ptr = icon_data.rgba8_pixels,
// .size = icon_data.width * icon_data.height * 4
// }
// };
sapp.run(.{
.init_userdata_cb = sokolInitCallback,
.frame_userdata_cb = sokolFrameCallback,
.cleanup_userdata_cb = sokolCleanupCallback,
.event_userdata_cb = sokolEventCallback,
.user_data = self,
.width = 640,
.height = 480,
.icon = icon,
.window_title = "Game",
.logger = .{ .func = sokolLogCallback },
.win32 = .{
.console_utf8 = true
}
});
}
fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
try self.graphics.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
// TODO:
// .imgui_font = .{
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
// }
});
try Audio.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
});
self.game_state = .init(.{
.gpa = self.allocator,
.seed = self.seed
});
const opts = Lib.Init{
.gpa = self.allocator,
.seed = self.game_state.seed,
};
self.game_state.state = self.game_linking.callbacks.init(&opts);
}
fn sokolCleanup(self: *Engine) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
self.game_linking.callbacks.deinit(self.game_state.state);
self.game_state.deinit();
Audio.deinit();
self.graphics.deinit(self.allocator);
self.game_linking.deinit(self.allocator);
}
fn sokolFrame(self: *Engine) !void {
tracy.frameMark();
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const frame = &self.game_state.frame;
try self.game_linking.check();
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
var maybe_screen_scaler: ?ScreenScalar = null;
if (self.game_state.previous_tick_canvas_size) |canvas_size| {
maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size);
}
{
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
const time_passed = now.since(self.game_state.started_at);
defer self.game_state.last_frame_at = time_passed;
_ = frame.arena.reset(.retain_capacity);
const arena = frame.arena.allocator();
const audio_commands_capacity = frame.audio_commands.capacity;
frame.audio_commands = .empty;
try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity);
const graphics_commands_capacity = frame.graphics_commands.capacity;
frame.graphics_commands = .empty;
try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity);
frame.screen_size = screen_size;
frame.time_ns = time_passed;
frame.dt_ns = time_passed - self.game_state.last_frame_at;
frame.mouse_position = self.game_state.input.mouse_position;
if (maybe_screen_scaler) |screen_scaler| {
screen_scaler.push(frame);
if (frame.mouse_position) |mouse_position| {
frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale);
}
}
if (debug_keybinds) {
if (frame.isKeyPressed(.F3)) {
self.show_debug = !self.show_debug;
}
if (frame.isKeyPressed(.F5)) {
self.restart_game = true;
}
}
self.game_linking.callbacks.tick(self.game_state.state, frame);
if (maybe_screen_scaler) |screen_scaler| {
screen_scaler.pop(frame, frame.clear_color);
}
frame.keyboard.pressed = .initEmpty();
frame.keyboard.released = .initEmpty();
frame.mouse_button.pressed = .initEmpty();
frame.mouse_button.released = .initEmpty();
}
// Canvas size modification must always be applied a frame later.
// So that mouse coordinate transformations are consistent.
self.game_state.previous_tick_canvas_size = frame.canvas_size;
sapp.showMouse(!frame.hide_cursor);
{
self.graphics.beginFrame();
defer self.graphics.endFrame(frame.clear_color);
self.graphics.drawCommands(frame.graphics_commands.items);
if (self.show_debug and build_options.has_imgui) {
try self.showDebugWindow(frame);
}
}
for (frame.audio_commands.items) |command| {
try Audio.mixer.commands.push(command);
}
if (self.restart_game) {
self.restart_game = false;
self.game_linking.callbacks.deinit(self.game_state.state);
self.game_state.deinit();
self.game_state = .init(.{
.gpa = self.allocator,
.seed = self.seed
});
const opts = Lib.Init{
.gpa = self.allocator,
.seed = self.game_state.seed,
};
self.game_state.state = self.game_linking.callbacks.init(&opts);
}
}
fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
if (!imgui.beginWindow(.{
.name = "Debug",
.pos = Vec2.init(20, 20),
.size = Vec2.init(200, 200),
})) {
return;
}
defer imgui.endWindow();
_ = imgui.beginTabBar("debug");
defer imgui.endTabBar();
if (imgui.beginTabItem("Game")) {
defer imgui.endTabItem();
var imgui_ctx = Lib.ImGui.init(self.allocator);
defer imgui_ctx.deinit();
self.game_linking.callbacks.debug(self.game_state.state, &imgui_ctx);
for (imgui_ctx.commands.items) |cmd| {
switch (cmd) {
.text => |str| imgui.text(str)
}
}
}
if (imgui.beginTabItem("Engine")) {
defer imgui.endTabItem();
if (imgui.button("Restart")) {
self.restart_game = true;
}
const linking_label = if (self.game_linking.kind == .static) "static" else "dynamic";
imgui.textFmt("Linking: {s}", .{ linking_label });
imgui.textFmt("Seed: 0x{x:08}", .{ self.seed });
const time_ms: f64 = @floatFromInt(@divFloor(self.game_state.last_frame_at, std.time.ns_per_ms));
imgui.textFmt("Time: {:.2}", .{ time_ms / 1000 });
imgui.textFmt("Draw commands: {}\n", .{
frame.graphics_commands.items.len,
});
imgui.textFmt("Audio instances: {}/{}\n", .{
Audio.mixer.instances.items.len,
Audio.mixer.instances.capacity
});
}
}
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const e = e_ptr.*;
const input = &self.game_state.input;
const frame = &self.game_state.frame;
if (imgui.handleEvent(e)) {
if (input.mouse_position != null) {
input.processEvent(frame, .{
.mouse_leave = {}
});
}
input.mouse_position = null;
return true;
}
blk: switch (e.type) {
.MOUSE_DOWN => {
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
input.processEvent(frame, .{
.mouse_pressed = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_UP => {
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
input.processEvent(frame, .{
.mouse_released = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_MOVE => {
if (input.mouse_position == null) {
input.processEvent(frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
} else {
input.processEvent(frame, .{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
});
}
return true;
},
.MOUSE_ENTER => {
if (input.mouse_position == null) {
input.processEvent(frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
return true;
},
.RESIZED => {
if (input.mouse_position != null) {
input.processEvent(frame, .{
.mouse_leave = {}
});
}
input.processEvent(frame, .{
.window_resize = {}
});
return true;
},
.MOUSE_LEAVE => {
if (input.mouse_position != null) {
input.processEvent(frame, .{
.mouse_leave = {}
});
}
return true;
},
.MOUSE_SCROLL => {
input.processEvent(frame, .{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
});
return true;
},
.KEY_DOWN => {
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
input.processEvent(frame, .{
.key_pressed = .{
.code = key_code,
.repeat = e.key_repeat
}
});
return true;
},
.KEY_UP => {
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
input.processEvent(frame, .{
.key_released = key_code
});
return true;
},
.CHAR => {
input.processEvent(frame, .{
.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();
}

View File

@ -1,7 +1,5 @@
const std = @import("std");
const Lib = @import("lib");
const Math = Lib.Math;
const build_options = @import("build_options");
pub const ig = @import("cimgui");
const Vec2 = Math.Vec2;
const Vec3 = Math.Vec3;
@ -11,6 +9,8 @@ const sokol = @import("sokol");
const sapp = sokol.app;
const simgui = sokol.imgui;
const Lib = @import("lib");
const build_options = Lib.build_options;
const enabled = build_options.has_imgui;
var global_allocator: ?std.mem.Allocator = null;

View File

@ -9,8 +9,6 @@ const Vec2 = lib.Math.Vec2;
const Input = @This();
const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode);
pub const Event = union(enum) {
mouse_pressed: struct {
button: lib.MouseButton,
@ -33,7 +31,11 @@ pub const Event = union(enum) {
char: u21,
};
mouse_position: ?Vec2 = null,
mouse_position: ?Vec2,
pub const initial = Input{
.mouse_position = null
};
pub fn processEvent(self: *Input, frame: *Frame, event: Event) void {
switch (event) {

View File

@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const Engine = @import("engine");
const Engine = @import("./engine.zig");
var engine: Engine = undefined;

View File

@ -1,539 +0,0 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const assert = std.debug.assert;
const sokol = @import("sokol");
const sapp = sokol.app;
pub const Input = @import("./input.zig");
const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig");
pub const Graphics = @import("./graphics.zig");
pub const Audio = @import("./audio/root.zig");
const tracy = @import("tracy");
const builtin = @import("builtin");
const STBImage = @import("stb_image");
const Gfx = Graphics;
const build_options = @import("build_options");
const Lib = @import("lib");
pub const Math = Lib.Math;
pub const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const GameCallbacks = struct {
const Init = *const fn(opts: *const Lib.Init) callconv(.c) void;
const Tick = *const fn(frame: *Lib.Frame) callconv(.c) void;
const Deinit = *const fn() callconv(.c) void;
const Debug = *const fn(imgui: *Lib.ImGui) callconv(.c) void;
init: Init,
tick: Tick,
deinit: Deinit,
debug: if (build_options.has_imgui) Debug else void
};
const GameLinking = union(enum) {
static,
dynamic: struct {
lib: std.DynLib
},
pub fn getCallbacks(self: *GameLinking) !GameCallbacks {
switch (self.*) {
.static => {
if (build_options.hot_reload) {
return error.StaticLinkingDisabled;
}
const game = @import("game");
return GameCallbacks{
.init = game.init,
.deinit = game.deinit,
.tick = game.tick,
.debug = if (build_options.has_imgui) game.debug else {},
};
},
.dynamic => |*opts| {
const lib = &opts.lib;
return GameCallbacks{
.init = lib.lookup(GameCallbacks.Init, "init") orelse return error.MissingFunction,
.tick = lib.lookup(GameCallbacks.Tick, "tick") orelse return error.MissingFunction,
.deinit = lib.lookup(GameCallbacks.Deinit, "deinit") orelse return error.MissingFunction,
.debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.Debug, "debug") orelse return error.MissingFunction else {},
};
}
}
}
};
const Engine = @This();
allocator: std.mem.Allocator,
started_at: std.time.Instant,
last_frame_at: Lib.Nanoseconds,
graphics: Graphics,
input: Input,
game_linking: GameLinking,
game_callbacks: GameCallbacks,
frame: Lib.Frame,
previous_tick_canvas_size: ?Vec2 = null,
const RunOptions = struct {
allocator: std.mem.Allocator,
game_library_path: ?[]const u8 = null
};
pub fn run(self: *Engine, opts: RunOptions) !void {
self.* = Engine{
.allocator = opts.allocator,
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.last_frame_at = 0,
.frame = undefined,
.graphics = undefined,
.game_linking = undefined,
.game_callbacks = undefined,
.input = .{}
};
if (opts.game_library_path) |game_library_path| {
self.game_linking = .{
.dynamic = .{
.lib = try std.DynLib.open(game_library_path)
}
};
} else {
self.game_linking = .static;
}
self.game_callbacks = try self.game_linking.getCallbacks();
self.frame.init(self.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);
}
// TODO: Don't hard code icon path, allow changing through options
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
// defer icon_data.deinit();
var icon: sapp.IconDesc = .{};
icon.sokol_default = true;
// TODO:
// icon.images[0] = .{
// .width = @intCast(icon_data.width),
// .height = @intCast(icon_data.height),
// .pixels = .{
// .ptr = icon_data.rgba8_pixels,
// .size = icon_data.width * icon_data.height * 4
// }
// };
sapp.run(.{
.init_userdata_cb = sokolInitCallback,
.frame_userdata_cb = sokolFrameCallback,
.cleanup_userdata_cb = sokolCleanupCallback,
.event_userdata_cb = sokolEventCallback,
.user_data = self,
.width = 640,
.height = 480,
.icon = icon,
.window_title = "Game",
.logger = .{ .func = sokolLogCallback },
.win32 = .{
.console_utf8 = true
}
});
}
fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
log.debug("Init", .{});
try self.graphics.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
// TODO:
// .imgui_font = .{
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
// }
});
try Audio.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
});
const seed: u64 = @bitCast(std.time.milliTimestamp());
const opts = Lib.Init{
.gpa = self.allocator,
.seed = seed,
};
self.game_callbacks.init(&opts);
}
fn sokolCleanup(self: *Engine) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
self.frame.deinit();
Audio.deinit();
self.game_callbacks.deinit();
self.graphics.deinit(self.allocator);
}
fn sokolFrame(self: *Engine) !void {
tracy.frameMark();
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
const time_passed = now.since(self.started_at);
defer self.last_frame_at = time_passed;
const frame = &self.frame;
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
var maybe_screen_scaler: ?ScreenScalar = null;
if (self.previous_tick_canvas_size) |canvas_size| {
maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size);
}
{
_ = frame.arena.reset(.retain_capacity);
const arena = frame.arena.allocator();
const audio_commands_capacity = frame.audio_commands.capacity;
frame.audio_commands = .empty;
try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity);
const graphics_commands_capacity = frame.graphics_commands.capacity;
frame.graphics_commands = .empty;
try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity);
frame.screen_size = screen_size;
frame.time_ns = time_passed;
frame.dt_ns = time_passed - self.last_frame_at;
frame.mouse_position = self.input.mouse_position;
if (maybe_screen_scaler) |screen_scaler| {
screen_scaler.push(frame);
if (frame.mouse_position) |mouse_position| {
frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale);
}
}
self.game_callbacks.tick(frame);
if (maybe_screen_scaler) |screen_scaler| {
screen_scaler.pop(frame, frame.clear_color);
}
frame.keyboard.pressed = .initEmpty();
frame.keyboard.released = .initEmpty();
frame.mouse_button.pressed = .initEmpty();
frame.mouse_button.released = .initEmpty();
}
// Canvas size modification must always be applied a frame later.
// So that mouse coordinate transformations are consistent.
self.previous_tick_canvas_size = self.frame.canvas_size;
sapp.showMouse(!self.frame.hide_cursor);
{
self.graphics.beginFrame();
defer self.graphics.endFrame(frame.clear_color);
self.graphics.drawCommands(frame.graphics_commands.items);
if (frame.show_debug and build_options.has_imgui) {
try self.showDebugWindow(frame);
}
}
for (frame.audio_commands.items) |command| {
try Audio.mixer.commands.push(command);
}
}
fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void {
if (!imgui.beginWindow(.{
.name = "Debug",
.pos = Vec2.init(20, 20),
.size = Vec2.init(200, 200),
})) {
return;
}
defer imgui.endWindow();
_ = imgui.beginTabBar("debug");
defer imgui.endTabBar();
if (imgui.beginTabItem("Game")) {
defer imgui.endTabItem();
var imgui_ctx = Lib.ImGui.init(self.allocator);
defer imgui_ctx.deinit();
self.game_callbacks.debug(&imgui_ctx);
for (imgui_ctx.commands.items) |cmd| {
switch (cmd) {
.text => |str| imgui.text(str)
}
}
}
if (imgui.beginTabItem("Engine")) {
defer imgui.endTabItem();
imgui.textFmt("Draw commands: {}\n", .{
frame.graphics_commands.items.len,
});
imgui.textFmt("Audio instances: {}/{}\n", .{
Audio.mixer.instances.items.len,
Audio.mixer.instances.capacity
});
}
}
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const e = e_ptr.*;
if (imgui.handleEvent(e)) {
if (self.input.mouse_position != null) {
self.input.processEvent(&self.frame, .{
.mouse_leave = {}
});
}
self.input.mouse_position = null;
return true;
}
blk: switch (e.type) {
.MOUSE_DOWN => {
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
self.input.processEvent(&self.frame, .{
.mouse_pressed = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_UP => {
const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk;
self.input.processEvent(&self.frame, .{
.mouse_released = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_MOVE => {
if (self.input.mouse_position == null) {
self.input.processEvent(&self.frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
} else {
self.input.processEvent(&self.frame, .{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
});
}
return true;
},
.MOUSE_ENTER => {
if (self.input.mouse_position == null) {
self.input.processEvent(&self.frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
return true;
},
.RESIZED => {
if (self.input.mouse_position != null) {
self.input.processEvent(&self.frame, .{
.mouse_leave = {}
});
}
self.input.processEvent(&self.frame, .{
.window_resize = {}
});
return true;
},
.MOUSE_LEAVE => {
if (self.input.mouse_position != null) {
self.input.processEvent(&self.frame, .{
.mouse_leave = {}
});
}
return true;
},
.MOUSE_SCROLL => {
self.input.processEvent(&self.frame, .{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
});
return true;
},
.KEY_DOWN => {
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
self.input.processEvent(&self.frame, .{
.key_pressed = .{
.code = key_code,
.repeat = e.key_repeat
}
});
return true;
},
.KEY_UP => {
const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk;
self.input.processEvent(&self.frame, .{
.key_released = key_code
});
return true;
},
.CHAR => {
self.input.processEvent(&self.frame, .{
.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();
}

View File

@ -25,9 +25,7 @@ const State = struct {
// wood01: Audio.Data.Id,
};
var g_state: *State = undefined;
pub export fn init(opts: *const Engine.Init) void {
pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
const gpa = opts.gpa;
// const font_id_array: FontName.EnumArray = .init(.{
@ -41,32 +39,31 @@ pub export fn init(opts: *const Engine.Init) void {
// .data = @embedFile("assets/wood01.ogg"),
// });
g_state = gpa.create(State) catch @panic("OOM");
g_state.* = State{
const state = gpa.create(State) catch @panic("OOM");
state.* = State{
.gpa = gpa,
.player = .init(50, 50),
// .font_id = font_id_array,
// .wood01 = wood01
};
return state;
}
pub export fn deinit() void {
const gpa = g_state.gpa;
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
const state: *State = @alignCast(@ptrCast(state_ptr));
const gpa = state.gpa;
gpa.destroy(g_state);
g_state = undefined;
gpa.destroy(state);
}
pub export fn tick(frame: *Engine.Frame) void {
pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
const state: *State = @alignCast(@ptrCast(state_ptr));
const dt = frame.deltaTime();
const canvas_size = Vec2.init(100, 100);
frame.canvas_size = canvas_size;
if (frame.isKeyPressed(.F3)) {
frame.show_debug = !frame.show_debug;
}
var dir = Vec2.init(0, 0);
if (frame.isKeyDown(.W)) {
dir.y -= 1;
@ -89,7 +86,7 @@ pub export fn tick(frame: *Engine.Frame) void {
// });
}
g_state.player = g_state.player.add(dir.multiplyScalar(50 * dt));
state.player = state.player.add(dir.multiplyScalar(50 * dt));
frame.drawRectangle(.{
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
@ -98,13 +95,13 @@ pub export fn tick(frame: *Engine.Frame) void {
const size = Vec2.init(20, 20);
frame.drawRectangle(.{
.rect = .{
.pos = g_state.player.sub(size.divideScalar(2)),
.pos = state.player.sub(size.divideScalar(2)),
.size = size
},
.color = rgb(200, 20, 20)
.color = rgb(200, 2, 200)
});
if (dir.x != 0 or dir.y != 0) {
frame.drawRectanglOutline(g_state.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3);
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);
}
// const regular_font = g_state.font_id.get(.regular);
@ -114,6 +111,12 @@ pub export fn tick(frame: *Engine.Frame) void {
// });
}
pub export fn debug(imgui: *Engine.ImGui) void {
pub fn debug(state_ptr: *anyopaque, imgui: *Engine.ImGui) callconv(.c) void {
const state: *State = @alignCast(@ptrCast(state_ptr));
_ = state; // autofix
imgui.text("Hello World!\n", .{});
}
comptime {
Engine.exportCallbacksIfNeeded(init, tick, deinit, debug);
}