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"),
|
||||
.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"),
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
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 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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user