540 lines
15 KiB
Zig
540 lines
15 KiB
Zig
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();
|
|
}
|