add hot reload
This commit is contained in:
parent
daeb07379f
commit
407da9e009
@ -21,6 +21,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
|
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
|
||||||
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
|
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
|
||||||
.win32_png_icon = b.path("src/assets/icon.png"),
|
.win32_png_icon = b.path("src/assets/icon.png"),
|
||||||
|
.hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"),
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const InitOptions = struct {
|
|||||||
|
|
||||||
dep_engine: *std.Build.Dependency,
|
dep_engine: *std.Build.Dependency,
|
||||||
|
|
||||||
|
statically_linked: ?bool = null,
|
||||||
hot_reload: ?bool = null,
|
hot_reload: ?bool = null,
|
||||||
has_imgui: ?bool = null,
|
has_imgui: ?bool = null,
|
||||||
has_tracy: ?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();
|
const isWasm = opts.target.result.cpu.arch.isWasm();
|
||||||
|
|
||||||
|
var has_tracy = false;
|
||||||
var hot_reload = false;
|
var hot_reload = false;
|
||||||
if (!isWasm) {
|
if (!isWasm) {
|
||||||
hot_reload = opts.hot_reload orelse (opts.optimize == .Debug);
|
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(.{
|
const engine_lib = b.createModule(.{
|
||||||
.root_source_file = b.path("src/lib/root.zig")
|
.root_source_file = b.path("src/lib/root.zig")
|
||||||
});
|
});
|
||||||
|
engine_lib.addOptions("build_options", build_options);
|
||||||
opts.root_module.addImport("engine", engine_lib);
|
opts.root_module.addImport("engine", engine_lib);
|
||||||
|
|
||||||
const engine_module = createEngineModule(b, .{
|
const runtime_module = createRuntimeModule(b, .{
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.has_tracy = opts.has_tracy,
|
.has_tracy = has_tracy,
|
||||||
.has_imgui = opts.has_imgui,
|
.has_imgui = has_imgui
|
||||||
.hot_reload = hot_reload
|
|
||||||
});
|
});
|
||||||
engine_module.root_module.addImport("lib", engine_lib);
|
runtime_module.root_module.addImport("lib", engine_lib);
|
||||||
if (!hot_reload) {
|
if (statically_linked) {
|
||||||
engine_module.root_module.addImport("game", opts.root_module);
|
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;
|
var run_cmd_step: *std.Build.Step = undefined;
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
const build_step = buildWasm(b, .{
|
const build_step = buildWasm(b, .{
|
||||||
.name = "index",
|
.name = "index",
|
||||||
.root_module = mod_main,
|
.root_module = runtime_module.root_module,
|
||||||
.dep_sokol = engine_module.dep_sokol,
|
.dep_sokol = runtime_module.dep_sokol,
|
||||||
.outer_builder = outer_builder
|
.outer_builder = outer_builder
|
||||||
});
|
});
|
||||||
outer_builder.getInstallStep().dependOn(build_step);
|
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 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);
|
emrun_step.step.dependOn(build_step);
|
||||||
run_cmd_step = &emrun_step.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
|
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
||||||
} else {
|
} else {
|
||||||
|
runtime_module.root_module.link_libc = true;
|
||||||
const exe = buildNative(b, .{
|
const exe = buildNative(b, .{
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
.root_module = mod_main,
|
.root_module = runtime_module.root_module,
|
||||||
.win32_has_console = opts.win32_has_console,
|
.win32_has_console = opts.win32_has_console,
|
||||||
.win32_png_icon = opts.win32_png_icon
|
.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(.{
|
const game_lib = b.addLibrary(.{
|
||||||
.name = "game",
|
.name = "game",
|
||||||
.root_module = opts.root_module,
|
.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;
|
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);
|
run_step.dependOn(run_cmd_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EngineModule = struct {
|
const RuntimeModule = struct {
|
||||||
root_module: *std.Build.Module,
|
root_module: *std.Build.Module,
|
||||||
dep_sokol: *std.Build.Dependency,
|
dep_sokol: *std.Build.Dependency,
|
||||||
|
|
||||||
@ -114,23 +124,14 @@ const EngineModule = struct {
|
|||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
optimize: std.builtin.OptimizeMode,
|
optimize: std.builtin.OptimizeMode,
|
||||||
|
|
||||||
has_imgui: ?bool = null,
|
has_imgui: bool,
|
||||||
has_tracy: ?bool = null,
|
has_tracy: bool,
|
||||||
hot_reload: bool
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mod = b.createModule(.{
|
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,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.link_libc = true
|
.link_libc = true
|
||||||
@ -139,12 +140,12 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
|||||||
const dep_sokol = b.dependency("sokol", .{
|
const dep_sokol = b.dependency("sokol", .{
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.with_sokol_imgui = has_imgui,
|
.with_sokol_imgui = opts.has_imgui,
|
||||||
});
|
});
|
||||||
mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
|
mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
|
||||||
mod.addImport("sokol", dep_sokol.module("sokol"));
|
mod.addImport("sokol", dep_sokol.module("sokol"));
|
||||||
|
|
||||||
if (has_imgui) {
|
if (opts.has_imgui) {
|
||||||
if (b.lazyDependency("cimgui", .{
|
if (b.lazyDependency("cimgui", .{
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
@ -159,10 +160,10 @@ fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
|||||||
const dep_tracy = b.dependency("tracy", .{
|
const dep_tracy = b.dependency("tracy", .{
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.tracy_enable = has_tracy,
|
.tracy_enable = opts.has_tracy,
|
||||||
.tracy_only_localhost = true
|
.tracy_only_localhost = true
|
||||||
});
|
});
|
||||||
if (has_tracy) {
|
if (opts.has_tracy) {
|
||||||
mod.linkLibrary(dep_tracy.artifact("tracy"));
|
mod.linkLibrary(dep_tracy.artifact("tracy"));
|
||||||
}
|
}
|
||||||
mod.addImport("tracy", dep_tracy.module("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
|
// // TODO: Define buid config for wasm
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var options = b.addOptions();
|
return RuntimeModule{
|
||||||
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{
|
|
||||||
.root_module = mod,
|
.root_module = mod,
|
||||||
.dep_sokol = dep_sokol
|
.dep_sokol = dep_sokol
|
||||||
};
|
};
|
||||||
@ -306,7 +301,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
|
|||||||
.use_webgl2 = true,
|
.use_webgl2 = true,
|
||||||
.use_emmalloc = true,
|
.use_emmalloc = true,
|
||||||
.use_filesystem = false,
|
.use_filesystem = false,
|
||||||
.shell_file_path = b.path("src/shell.html"),
|
.shell_file_path = b.path("src/runtime/shell.html"),
|
||||||
}) catch unreachable;
|
}) catch unreachable;
|
||||||
|
|
||||||
return &link_step.step;
|
return &link_step.step;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log.scoped(.engine);
|
const log = std.log.scoped(.engine);
|
||||||
|
|
||||||
|
pub const build_options = @import("build_options");
|
||||||
pub const ImGui = @import("./imgui.zig");
|
pub const ImGui = @import("./imgui.zig");
|
||||||
pub const Math = @import("./math.zig");
|
pub const Math = @import("./math.zig");
|
||||||
|
|
||||||
@ -285,10 +286,8 @@ pub const Frame = struct {
|
|||||||
screen_size: Vec2,
|
screen_size: Vec2,
|
||||||
clear_color: Vec4,
|
clear_color: Vec4,
|
||||||
|
|
||||||
show_debug: bool,
|
pub fn init(gpa: std.mem.Allocator) Frame {
|
||||||
|
return Frame{
|
||||||
pub fn init(self: *Frame, gpa: std.mem.Allocator) void {
|
|
||||||
self.* = Frame{
|
|
||||||
.arena = std.heap.ArenaAllocator.init(gpa),
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
||||||
.time_ns = 0,
|
.time_ns = 0,
|
||||||
.dt_ns = 0,
|
.dt_ns = 0,
|
||||||
@ -299,7 +298,6 @@ pub const Frame = struct {
|
|||||||
.graphics_commands = .empty,
|
.graphics_commands = .empty,
|
||||||
.canvas_size = null,
|
.canvas_size = null,
|
||||||
.clear_color = rgb(0, 0, 0),
|
.clear_color = rgb(0, 0, 0),
|
||||||
.show_debug = false,
|
|
||||||
.audio_commands = .empty,
|
.audio_commands = .empty,
|
||||||
.hide_cursor = false,
|
.hide_cursor = false,
|
||||||
.screen_size = .init(0, 0),
|
.screen_size = .init(0, 0),
|
||||||
@ -471,3 +469,35 @@ pub const Init = struct {
|
|||||||
gpa: std.mem.Allocator,
|
gpa: std.mem.Allocator,
|
||||||
seed: u64
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ pub const Mixer = @import("./mixer.zig");
|
|||||||
|
|
||||||
pub const Command = Mixer.Command;
|
pub const Command = Mixer.Command;
|
||||||
|
|
||||||
const Nanoseconds = @import("../root.zig").Nanoseconds;
|
const Nanoseconds = @import("lib").Nanoseconds;
|
||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
const saudio = sokol.audio;
|
const saudio = sokol.audio;
|
||||||
|
|
||||||
|
|||||||
788
engine/src/runtime/engine.zig
Normal file
788
engine/src/runtime/engine.zig
Normal 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();
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Lib = @import("lib");
|
|
||||||
const Math = Lib.Math;
|
const Math = Lib.Math;
|
||||||
const build_options = @import("build_options");
|
|
||||||
pub const ig = @import("cimgui");
|
pub const ig = @import("cimgui");
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
const Vec3 = Math.Vec3;
|
const Vec3 = Math.Vec3;
|
||||||
@ -11,6 +9,8 @@ const sokol = @import("sokol");
|
|||||||
const sapp = sokol.app;
|
const sapp = sokol.app;
|
||||||
const simgui = sokol.imgui;
|
const simgui = sokol.imgui;
|
||||||
|
|
||||||
|
const Lib = @import("lib");
|
||||||
|
const build_options = Lib.build_options;
|
||||||
const enabled = build_options.has_imgui;
|
const enabled = build_options.has_imgui;
|
||||||
|
|
||||||
var global_allocator: ?std.mem.Allocator = null;
|
var global_allocator: ?std.mem.Allocator = null;
|
||||||
|
|||||||
@ -9,8 +9,6 @@ const Vec2 = lib.Math.Vec2;
|
|||||||
|
|
||||||
const Input = @This();
|
const Input = @This();
|
||||||
|
|
||||||
const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode);
|
|
||||||
|
|
||||||
pub const Event = union(enum) {
|
pub const Event = union(enum) {
|
||||||
mouse_pressed: struct {
|
mouse_pressed: struct {
|
||||||
button: lib.MouseButton,
|
button: lib.MouseButton,
|
||||||
@ -33,7 +31,11 @@ pub const Event = union(enum) {
|
|||||||
char: u21,
|
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 {
|
pub fn processEvent(self: *Input, frame: *Frame, event: Event) void {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Engine = @import("engine");
|
const Engine = @import("./engine.zig");
|
||||||
|
|
||||||
var engine: Engine = undefined;
|
var engine: Engine = undefined;
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
41
src/game.zig
41
src/game.zig
@ -25,9 +25,7 @@ const State = struct {
|
|||||||
// wood01: Audio.Data.Id,
|
// wood01: Audio.Data.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
var g_state: *State = undefined;
|
pub fn init(opts: *const Engine.Init) callconv(.c) *anyopaque {
|
||||||
|
|
||||||
pub export fn init(opts: *const Engine.Init) void {
|
|
||||||
const gpa = opts.gpa;
|
const gpa = opts.gpa;
|
||||||
|
|
||||||
// const font_id_array: FontName.EnumArray = .init(.{
|
// 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"),
|
// .data = @embedFile("assets/wood01.ogg"),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
g_state = gpa.create(State) catch @panic("OOM");
|
const state = gpa.create(State) catch @panic("OOM");
|
||||||
g_state.* = State{
|
state.* = State{
|
||||||
.gpa = gpa,
|
.gpa = gpa,
|
||||||
.player = .init(50, 50),
|
.player = .init(50, 50),
|
||||||
// .font_id = font_id_array,
|
// .font_id = font_id_array,
|
||||||
// .wood01 = wood01
|
// .wood01 = wood01
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub export fn deinit() void {
|
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
|
||||||
const gpa = g_state.gpa;
|
const state: *State = @alignCast(@ptrCast(state_ptr));
|
||||||
|
const gpa = state.gpa;
|
||||||
|
|
||||||
gpa.destroy(g_state);
|
gpa.destroy(state);
|
||||||
g_state = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 dt = frame.deltaTime();
|
||||||
|
|
||||||
const canvas_size = Vec2.init(100, 100);
|
const canvas_size = Vec2.init(100, 100);
|
||||||
frame.canvas_size = canvas_size;
|
frame.canvas_size = canvas_size;
|
||||||
|
|
||||||
if (frame.isKeyPressed(.F3)) {
|
|
||||||
frame.show_debug = !frame.show_debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dir = Vec2.init(0, 0);
|
var dir = Vec2.init(0, 0);
|
||||||
if (frame.isKeyDown(.W)) {
|
if (frame.isKeyDown(.W)) {
|
||||||
dir.y -= 1;
|
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(.{
|
frame.drawRectangle(.{
|
||||||
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
|
.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);
|
const size = Vec2.init(20, 20);
|
||||||
frame.drawRectangle(.{
|
frame.drawRectangle(.{
|
||||||
.rect = .{
|
.rect = .{
|
||||||
.pos = g_state.player.sub(size.divideScalar(2)),
|
.pos = state.player.sub(size.divideScalar(2)),
|
||||||
.size = size
|
.size = size
|
||||||
},
|
},
|
||||||
.color = rgb(200, 20, 20)
|
.color = rgb(200, 2, 200)
|
||||||
});
|
});
|
||||||
if (dir.x != 0 or dir.y != 0) {
|
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);
|
// 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", .{});
|
imgui.text("Hello World!\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
Engine.exportCallbacksIfNeeded(init, tick, deinit, debug);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user