move engine code to separate folder

This commit is contained in:
Rokas Puzonas 2025-12-30 14:22:56 +02:00
parent 8b7c50375c
commit 4df4f42022
11 changed files with 797 additions and 702 deletions

11
src/assets.zig Normal file
View File

@ -0,0 +1,11 @@
const Assets = @This();
pub fn init() !Assets {
return Assets{
};
}
pub fn deinit(self: *Assets) void {
_ = self; // autofix
}

View File

@ -825,7 +825,7 @@ pub fn init(options: Options) !void {
.disable_set_mouse_cursor = true .disable_set_mouse_cursor = true
}); });
imgui.addFont(@embedFile("./assets/roboto-font/Roboto-Regular.ttf"), 16); imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16);
// make sure the fontstash atlas width/height is pow-2 // make sure the fontstash atlas width/height is pow-2
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(512 * dpi_scale)); const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(512 * dpi_scale));
@ -836,12 +836,12 @@ pub fn init(options: Options) !void {
assert(g_fons_context != null); assert(g_fons_context != null);
font_id_map = .init(.{ font_id_map = .init(.{
.regular = loadEmbededFont(g_fons_context, "regular", "./assets/roboto-font/Roboto-Regular.ttf"), .regular = loadEmbededFont(g_fons_context, "regular", "../assets/roboto-font/Roboto-Regular.ttf"),
.italic = loadEmbededFont(g_fons_context, "italic", "./assets/roboto-font/Roboto-Italic.ttf"), .italic = loadEmbededFont(g_fons_context, "italic", "../assets/roboto-font/Roboto-Italic.ttf"),
.bold = loadEmbededFont(g_fons_context, "bold", "./assets/roboto-font/Roboto-Bold.ttf"), .bold = loadEmbededFont(g_fons_context, "bold", "../assets/roboto-font/Roboto-Bold.ttf"),
}); });
const tilemap = try ImageData.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png")); const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
defer tilemap.deinit(); defer tilemap.deinit();
image_map = .init(.{ image_map = .init(.{
@ -1057,10 +1057,6 @@ pub fn popTransform() void {
sgl.popMatrix(); sgl.popMatrix();
} }
pub fn event(ev: [*c]const sapp.Event) bool {
return imgui.handleEvent(ev.*);
}
fn createProjectionMatrix() Mat4 { fn createProjectionMatrix() Mat4 {
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());

239
src/engine/input.zig Normal file
View File

@ -0,0 +1,239 @@
const std = @import("std");
const sokol = @import("sokol");
const Engine = @import("./root.zig");
const Nanoseconds = Engine.Nanoseconds;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Input = @This();
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
SPACE = 32,
APOSTROPHE = 39,
COMMA = 44,
MINUS = 45,
PERIOD = 46,
SLASH = 47,
_0 = 48,
_1 = 49,
_2 = 50,
_3 = 51,
_4 = 52,
_5 = 53,
_6 = 54,
_7 = 55,
_8 = 56,
_9 = 57,
SEMICOLON = 59,
EQUAL = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LEFT_BRACKET = 91,
BACKSLASH = 92,
RIGHT_BRACKET = 93,
GRAVE_ACCENT = 96,
WORLD_1 = 161,
WORLD_2 = 162,
ESCAPE = 256,
ENTER = 257,
TAB = 258,
BACKSPACE = 259,
INSERT = 260,
DELETE = 261,
RIGHT = 262,
LEFT = 263,
DOWN = 264,
UP = 265,
PAGE_UP = 266,
PAGE_DOWN = 267,
HOME = 268,
END = 269,
CAPS_LOCK = 280,
SCROLL_LOCK = 281,
NUM_LOCK = 282,
PRINT_SCREEN = 283,
PAUSE = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
KP_0 = 320,
KP_1 = 321,
KP_2 = 322,
KP_3 = 323,
KP_4 = 324,
KP_5 = 325,
KP_6 = 326,
KP_7 = 327,
KP_8 = 328,
KP_9 = 329,
KP_DECIMAL = 330,
KP_DIVIDE = 331,
KP_MULTIPLY = 332,
KP_SUBTRACT = 333,
KP_ADD = 334,
KP_ENTER = 335,
KP_EQUAL = 336,
LEFT_SHIFT = 340,
LEFT_CONTROL = 341,
LEFT_ALT = 342,
LEFT_SUPER = 343,
RIGHT_SHIFT = 344,
RIGHT_CONTROL = 345,
RIGHT_ALT = 346,
RIGHT_SUPER = 347,
MENU = 348,
};
pub const KeyState = struct {
down: bool,
pressed: bool,
released: bool,
down_duration: ?f64,
pub const RepeatOptions = struct {
first_at: f64 = 0,
period: f64
};
pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool {
if (!self.down) {
last_repeat_at.* = null;
return false;
}
const down_duration = self.down_duration.?;
if (last_repeat_at.* != null) {
if (down_duration >= last_repeat_at.*.? + opts.period) {
last_repeat_at.* = last_repeat_at.*.? + opts.period;
return true;
}
} else {
if (down_duration >= opts.first_at) {
last_repeat_at.* = opts.first_at;
return true;
}
}
return false;
}
};
pub const Mouse = struct {
pub const Button = enum {
left,
right,
middle,
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?Button {
return switch(mouse_button) {
.LEFT => Button.left,
.RIGHT => Button.right,
.MIDDLE => Button.middle,
else => null
};
}
};
position: ?Vec2,
buttons: std.EnumSet(Button),
pub const empty = Mouse{
.position = null,
.buttons = .initEmpty()
};
};
down_keys: std.EnumSet(KeyCode),
pressed_keys: std.EnumSet(KeyCode),
released_keys: std.EnumSet(KeyCode),
pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds),
mouse: Mouse,
pub const empty = Input{
.down_keys = .initEmpty(),
.pressed_keys = .initEmpty(),
.released_keys = .initEmpty(),
.pressed_keys_at = .init(.{}),
.mouse = .empty
};
pub fn isKeyDown(self: *Input, key_code: KeyCode) bool {
return self.down_keys.contains(key_code);
}
pub fn getKeyDownDuration(self: *Input, frame: Engine.Frame, key_code: KeyCode) ?f64 {
if (!self.isKeyDown(key_code)) {
return null;
}
const pressed_at_ns = self.pressed_keys_at.get(key_code).?;
const duration_ns = frame.time_ns - pressed_at_ns;
return @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s;
}
pub fn isKeyPressed(self: *Input, key_code: KeyCode) bool {
return self.pressed_keys.contains(key_code);
}
pub fn isKeyReleased(self: *Input, key_code: KeyCode) bool {
return self.released_keys.contains(key_code);
}
pub fn getKeyState(self: *Input, frame: Engine.Frame, key_code: KeyCode) KeyState {
return KeyState{
.down = self.isKeyDown(key_code),
.released = self.isKeyReleased(key_code),
.pressed = self.isKeyPressed(key_code),
.down_duration = self.getKeyDownDuration(frame, key_code)
};
}

445
src/engine/root.zig Normal file
View File

@ -0,0 +1,445 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const sokol = @import("sokol");
const sapp = sokol.app;
pub const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
pub const Input = @import("./input.zig");
const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig");
pub const Graphics = @import("./graphics.zig");
const tracy = @import("tracy");
const builtin = @import("builtin");
const Gfx = Graphics;
const Game = @import("../game.zig");
const Assets = @import("../assets.zig");
const Engine = @This();
pub const Nanoseconds = u64;
pub const Event = union(enum) {
mouse_pressed: struct {
button: Input.Mouse.Button,
position: Vec2,
},
mouse_released: struct {
button: Input.Mouse.Button,
position: Vec2,
},
mouse_move: Vec2,
mouse_enter: Vec2,
mouse_leave,
mouse_scroll: Vec2,
key_pressed: struct {
code: Input.KeyCode,
repeat: bool
},
key_released: Input.KeyCode,
window_resize,
char: u21,
};
pub const Frame = struct {
time_ns: Nanoseconds,
dt_ns: Nanoseconds,
dt: f32,
input: *Input
};
allocator: std.mem.Allocator,
started_at: std.time.Instant,
mouse_inside: bool,
last_frame_at: Nanoseconds,
input: Input,
game: Game,
assets: Assets,
const RunOptions = struct {
window_title: [*:0]const u8 = "Game",
window_width: u31 = 640,
window_height: u31 = 480,
};
pub fn run(self: *Engine, opts: RunOptions) !void {
self.* = Engine{
.allocator = undefined,
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.input = .empty,
.mouse_inside = false,
.last_frame_at = 0,
.assets = undefined,
.game = undefined
};
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
// TODO: Use tracy TracingAllocator
if (builtin.mode == .Debug) {
self.allocator = debug_allocator.allocator();
} else {
self.allocator = std.heap.smp_allocator;
}
tracy.setThreadName("Main");
if (builtin.os.tag == .linux) {
var sa: std.posix.Sigaction = .{
.handler = .{ .handler = posixSignalHandler },
.mask = std.posix.sigemptyset(),
.flags = std.posix.SA.RESTART,
};
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
}
sapp.run(.{
.init_userdata_cb = sokolInitCallback,
.frame_userdata_cb = sokolFrameCallback,
.cleanup_userdata_cb = sokolCleanupCallback,
.event_userdata_cb = sokolEventCallback,
.user_data = self,
.width = opts.window_width,
.height = opts.window_height,
.icon = .{ .sokol_default = true },
.window_title = opts.window_title,
.logger = .{ .func = sokolLogCallback },
});
}
fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
try Gfx.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback }
});
self.assets = try Assets.init();
self.game = try Game.init(self.allocator, &self.assets);
}
fn sokolCleanup(self: *Engine) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
self.game.deinit();
self.assets.deinit();
Gfx.deinit();
}
fn sokolFrame(self: *Engine) !void {
tracy.frameMark();
const time_passed = self.timePassed();
defer self.last_frame_at = time_passed;
const dt_ns = time_passed - self.last_frame_at;
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
Gfx.beginFrame();
defer Gfx.endFrame();
{
const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf());
const ctx = ScreenScalar.push(window_size, self.game.canvas_size);
defer ctx.pop();
try self.game.tick(Frame{
.time_ns = time_passed,
.dt_ns = dt_ns,
.dt = @as(f32, @floatFromInt(dt_ns)) / std.time.ns_per_s,
.input = &self.input
});
}
try self.game.debug();
self.input.pressed_keys = .initEmpty();
self.input.released_keys = .initEmpty();
}
fn timePassed(self: *Engine) Nanoseconds {
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
return now.since(self.started_at);
}
fn event(self: *Engine, e: Event) !void {
const input = &self.input;
switch (e) {
.key_pressed => |opts| {
if (!opts.repeat) {
input.pressed_keys_at.put(opts.code, self.timePassed());
input.pressed_keys.insert(opts.code);
input.down_keys.insert(opts.code);
}
},
.key_released => |key_code| {
input.down_keys.remove(key_code);
input.released_keys.insert(key_code);
input.pressed_keys_at.remove(key_code);
},
.mouse_leave => {
var iter = input.down_keys.iterator();
while (iter.next()) |key_code| {
input.released_keys.insert(key_code);
}
input.down_keys = .initEmpty();
input.pressed_keys_at = .init(.{});
input.mouse = .empty;
},
.mouse_enter => |pos| {
input.mouse.position = pos;
},
.mouse_move => |pos| {
input.mouse.position = pos;
},
.mouse_pressed => |opts| {
input.mouse.position = opts.position;
input.mouse.buttons.insert(opts.button);
},
.mouse_released => |opts| {
input.mouse.position = opts.position;
input.mouse.buttons.remove(opts.button);
},
else => {}
}
}
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const e = e_ptr.*;
const MouseButton = Input.Mouse.Button;
if (imgui.handleEvent(e)) {
if (self.mouse_inside) {
try self.event(Event{
.mouse_leave = {}
});
}
self.mouse_inside = false;
return true;
}
blk: switch (e.type) {
.MOUSE_DOWN => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.event(Event{
.mouse_pressed = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_UP => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.event(Event{
.mouse_released = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_MOVE => {
if (!self.mouse_inside) {
try self.event(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
} else {
try self.event(Event{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
});
}
self.mouse_inside = true;
return true;
},
.MOUSE_ENTER => {
if (!self.mouse_inside) {
try self.event(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
self.mouse_inside = true;
return true;
},
.RESIZED => {
if (self.mouse_inside) {
try self.event(Event{
.mouse_leave = {}
});
}
try self.event(Event{
.window_resize = {}
});
self.mouse_inside = false;
return true;
},
.MOUSE_LEAVE => {
if (self.mouse_inside) {
try self.event(Event{
.mouse_leave = {}
});
}
self.mouse_inside = false;
return true;
},
.MOUSE_SCROLL => {
try self.event(Event{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
});
return true;
},
.KEY_DOWN => {
try self.event(Event{
.key_pressed = .{
.code = @enumFromInt(@intFromEnum(e.key_code)),
.repeat = e.key_repeat
}
});
return true;
},
.KEY_UP => {
try self.event(Event{
.key_released = @enumFromInt(@intFromEnum(e.key_code))
});
return true;
},
.CHAR => {
try self.event(Event{
.char = @intCast(e.char_code)
});
return true;
},
.QUIT_REQUESTED => {
// TODO: handle quit request. Maybe show confirmation window in certain cases.
},
else => {}
}
return false;
}
fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void {
const engine: *Engine = @alignCast(@ptrCast(userdata));
const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: {
log.err("sokolEvent() failed: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
break :blk false;
};
if (consume_event) {
sapp.consumeEvent();
}
}
fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void {
const engine: *Engine = @alignCast(@ptrCast(userdata));
engine.sokolCleanup();
}
fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void {
const engine: *Engine = @alignCast(@ptrCast(userdata));
engine.sokolInit() catch |e| {
log.err("sokolInit() failed: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
sapp.requestQuit();
};
}
fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void {
const engine: *Engine = @alignCast(@ptrCast(userdata));
engine.sokolFrame() catch |e| {
log.err("sokolFrame() failed: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
sapp.requestQuit();
};
}
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
const log_sokol = std.log.scoped(.sokol);
if (log_level == 0) {
log_sokol.err(format, args);
} else if (log_level == 1) {
log_sokol.err(format, args);
} else if (log_level == 2) {
log_sokol.warn(format, args);
} else {
log_sokol.info(format, args);
}
}
fn cStrToZig(c_str: [*c]const u8) [:0]const u8 {
return std.mem.span(c_str);
}
fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void {
_ = user_data;
if (filename != null) {
sokolLogFmt(
log_level,
"[{s}][id:{}] {s}:{}: {s}",
.{
cStrToZig(tag orelse "-"),
log_item,
std.fs.path.basename(cStrToZig(filename orelse "-")),
line_nr,
cStrToZig(message orelse "")
}
);
} else {
sokolLogFmt(
log_level,
"[{s}][id:{}] {s}",
.{
cStrToZig(tag orelse "-"),
log_item,
cStrToZig(message orelse "")
}
);
}
}
fn posixSignalHandler(sig: i32) callconv(.c) void {
_ = sig;
sapp.requestQuit();
}

View File

@ -0,0 +1,63 @@
const Gfx = @import("./graphics.zig");
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const ScreenScalar = @This();
window_size: Vec2,
translation: Vec2,
scale: f32,
pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar {
// TODO: Render to a lower resolution instead of scaling.
// To avoid pixel bleeding in spritesheet artifacts
const scale = @floor(@min(
window_size.x / canvas_size.x,
window_size.y / canvas_size.y,
));
var translation: Vec2 = Vec2.sub(window_size, canvas_size.multiplyScalar(scale)).multiplyScalar(0.5);
translation.x = @round(translation.x);
translation.y = @round(translation.y);
Gfx.pushTransform(translation, scale);
return ScreenScalar{
.window_size = window_size,
.translation = translation,
.scale = scale
};
}
pub fn pop(self: ScreenScalar) void {
Gfx.popTransform();
const bg_color = rgb(0, 0, 0);
const filler_size = self.translation;
Gfx.drawRectangle(
.init(0, 0),
.init(self.window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, self.window_size.y - filler_size.y),
.init(self.window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, 0),
.init(filler_size.x, self.window_size.y),
bg_color
);
Gfx.drawRectangle(
.init(self.window_size.x - filler_size.x, 0),
.init(filler_size.x, self.window_size.y),
bg_color
);
}

View File

@ -1,9 +1,8 @@
const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList; const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
const Gfx = @import("./graphics.zig"); const Engine = @import("./engine/root.zig");
const Vec2 = Engine.Math.Vec2;
const Math = @import("./math.zig"); const Gfx = Engine.Graphics;
const Vec2 = Math.Vec2;
const Entity = @This(); const Entity = @This();

View File

@ -2,28 +2,30 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const Math = @import("./math.zig"); const Engine = @import("./engine/root.zig");
const Vec2 = Math.Vec2; const imgui = Engine.imgui;
const Vec4 = Math.Vec4; const Gfx = Engine.Graphics;
const rgb = Math.rgb; const Vec2 = Engine.Math.Vec2;
const rgb_hex = Math.rgb_hex; const Vec4 = Engine.Math.Vec4;
const rgb = Engine.Math.rgb;
const rgb_hex = Engine.Math.rgb_hex;
const Timer = @import("./timer.zig"); const Timer = @import("./timer.zig");
const Window = @import("./window.zig");
const imgui = @import("./imgui.zig");
const Gfx = @import("./graphics.zig");
const Entity = @import("./entity.zig"); const Entity = @import("./entity.zig");
const Assets = @import("./assets.zig");
const tiled = @import("tiled"); const tiled = @import("tiled");
const Game = @This(); const Game = @This();
const KeyState = Engine.Input.KeyState;
pub const Input = struct { pub const Input = struct {
dt: f64, dt: f64,
move_up: Window.KeyState, move_up: KeyState,
move_down: Window.KeyState, move_down: KeyState,
move_left: Window.KeyState, move_left: KeyState,
move_right: Window.KeyState, move_right: KeyState,
restart: bool restart: bool
}; };
@ -80,7 +82,8 @@ finale: bool = false,
finale_timer: ?Timer.Id = null, finale_timer: ?Timer.Id = null,
finale_counter: u32 = 0, finale_counter: u32 = 0,
pub fn init(gpa: Allocator) !Game { pub fn init(gpa: Allocator, assets: *Assets) !Game {
_ = assets; // autofix
var self = Game{ var self = Game{
.gpa = gpa, .gpa = gpa,
.canvas_size = (Vec2.init(20, 15)), .canvas_size = (Vec2.init(20, 15)),
@ -323,17 +326,16 @@ fn moveEntity(self: *Game, entity_id: Entity.Id, dir: Vec2) bool {
return true; return true;
} }
pub fn getInput(self: *Game, window: *Window) Input { pub fn getInput(self: *Game, frame: Engine.Frame) Input {
_ = self; // autofix _ = self; // autofix
const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s; const input = frame.input;
return Input{ return Input{
.dt = dt, .dt = frame.dt,
.move_up = window.getKeyState(.W), .move_up = input.getKeyState(frame, .W),
.move_down = window.getKeyState(.S), .move_down = input.getKeyState(frame, .S),
.move_left = window.getKeyState(.A), .move_left = input.getKeyState(frame, .A),
.move_right = window.getKeyState(.D), .move_right = input.getKeyState(frame, .D),
.restart = window.isKeyPressed(.R) .restart = input.isKeyPressed(.R)
}; };
} }
@ -410,7 +412,7 @@ pub fn tickLevel(self: *Game, input: Input) !void {
var move: Vec2 = .init(0, 0); var move: Vec2 = .init(0, 0);
if (can_move) { if (can_move) {
const repeat_options = Window.KeyState.RepeatOptions{ const repeat_options = KeyState.RepeatOptions{
.first_at = 0.3, .first_at = 0.3,
.period = 0.1 .period = 0.1
}; };
@ -541,7 +543,9 @@ pub fn tickFinale(self: *Game) !void {
} }
} }
pub fn tick(self: *Game, input: Input) !void { pub fn tick(self: *Game, frame: Engine.Frame) !void {
const input = self.getInput(frame);
const bg_color = rgb_hex("#222323").?; const bg_color = rgb_hex("#222323").?;
Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color); Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color);

View File

@ -1,118 +1,7 @@
const std = @import("std"); const Engine = @import("./engine/root.zig");
const tracy = @import("tracy");
const Gfx = @import("./graphics.zig");
const builtin = @import("builtin");
const Window = @import("./window.zig");
const Event = Window.Event;
const MouseButton = Window.MouseButton;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const sokol = @import("sokol");
const slog = sokol.log;
const sg = sokol.gfx;
const sapp = sokol.app;
const sglue = sokol.glue;
const log = std.log.scoped(.main);
var gpa: std.mem.Allocator = undefined;
var window: Window = undefined;
var event_queue_full_shown = false;
fn signalHandler(sig: i32) callconv(.c) void {
_ = sig;
sapp.requestQuit();
}
export fn init() void {
var zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
Window.init(&window, gpa) catch |e| {
log.err("init() failed: {}", .{e});
sapp.requestQuit();
};
}
export fn frame() void {
tracy.frameMark();
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
window.frame() catch |e| {
log.err("frame() failed: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
sapp.requestQuit();
};
}
export fn cleanup() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
window.deinit();
}
export fn event(e_ptr: [*c]const sapp.Event) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
const consumed_event = window.event(e_ptr) catch |e| switch (e) {
error.EventQueueFull => blk: {
if (!event_queue_full_shown) {
log.warn("Event queue is full! Frame is taking too long to process", .{});
event_queue_full_shown = true;
}
break :blk false;
},
};
if (consumed_event) {
event_queue_full_shown = false;
sapp.consumeEvent();
}
}
pub fn main() !void { pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init; var engine: Engine = undefined;
defer _ = debug_allocator.deinit(); try engine.run(.{});
if (builtin.mode == .Debug) {
gpa = debug_allocator.allocator();
} else {
gpa = std.heap.smp_allocator;
}
// TODO: Use tracy TracingAllocator
tracy.setThreadName("Main");
if (builtin.os.tag == .linux) {
var sa: std.posix.Sigaction = .{
.handler = .{ .handler = signalHandler },
.mask = std.posix.sigemptyset(),
.flags = std.posix.SA.RESTART,
};
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
}
sapp.run(.{
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = event,
.width = 640,
.height = 480,
.icon = .{ .sokol_default = true },
.window_title = "Game",
.logger = .{ .func = Window.sokolLogCallback },
});
} }

View File

@ -1,551 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const sokol = @import("sokol");
const sapp = sokol.app;
const Gfx = @import("./graphics.zig");
const tiled = @import("tiled");
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const rgb = Math.rgb;
const Game = @import("./game.zig");
const Window = @This();
const log = std.log.scoped(.window);
pub const MouseButton = enum {
left,
right,
middle,
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton {
return switch(mouse_button) {
.LEFT => MouseButton.left,
.RIGHT => MouseButton.right,
.MIDDLE => MouseButton.middle,
else => null
};
}
};
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
SPACE = 32,
APOSTROPHE = 39,
COMMA = 44,
MINUS = 45,
PERIOD = 46,
SLASH = 47,
_0 = 48,
_1 = 49,
_2 = 50,
_3 = 51,
_4 = 52,
_5 = 53,
_6 = 54,
_7 = 55,
_8 = 56,
_9 = 57,
SEMICOLON = 59,
EQUAL = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LEFT_BRACKET = 91,
BACKSLASH = 92,
RIGHT_BRACKET = 93,
GRAVE_ACCENT = 96,
WORLD_1 = 161,
WORLD_2 = 162,
ESCAPE = 256,
ENTER = 257,
TAB = 258,
BACKSPACE = 259,
INSERT = 260,
DELETE = 261,
RIGHT = 262,
LEFT = 263,
DOWN = 264,
UP = 265,
PAGE_UP = 266,
PAGE_DOWN = 267,
HOME = 268,
END = 269,
CAPS_LOCK = 280,
SCROLL_LOCK = 281,
NUM_LOCK = 282,
PRINT_SCREEN = 283,
PAUSE = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
KP_0 = 320,
KP_1 = 321,
KP_2 = 322,
KP_3 = 323,
KP_4 = 324,
KP_5 = 325,
KP_6 = 326,
KP_7 = 327,
KP_8 = 328,
KP_9 = 329,
KP_DECIMAL = 330,
KP_DIVIDE = 331,
KP_MULTIPLY = 332,
KP_SUBTRACT = 333,
KP_ADD = 334,
KP_ENTER = 335,
KP_EQUAL = 336,
LEFT_SHIFT = 340,
LEFT_CONTROL = 341,
LEFT_ALT = 342,
LEFT_SUPER = 343,
RIGHT_SHIFT = 344,
RIGHT_CONTROL = 345,
RIGHT_ALT = 346,
RIGHT_SUPER = 347,
MENU = 348,
};
pub const Event = union(enum) {
mouse_pressed: struct {
button: MouseButton,
position: Vec2,
},
mouse_released: struct {
button: MouseButton,
position: Vec2,
},
mouse_move: Vec2,
mouse_enter: Vec2,
mouse_leave,
mouse_scroll: Vec2,
key_pressed: struct {
code: KeyCode,
repeat: bool
},
key_released: KeyCode,
window_resize,
char: u21,
};
pub const KeyState = struct {
down: bool,
pressed: bool,
released: bool,
down_duration: ?f64,
pub const RepeatOptions = struct {
first_at: f64 = 0,
period: f64
};
pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool {
if (!self.down) {
last_repeat_at.* = null;
return false;
}
const down_duration = self.down_duration.?;
if (last_repeat_at.* != null) {
if (down_duration >= last_repeat_at.*.? + opts.period) {
last_repeat_at.* = last_repeat_at.*.? + opts.period;
return true;
}
} else {
if (down_duration >= opts.first_at) {
last_repeat_at.* = opts.first_at;
return true;
}
}
return false;
}
};
const Nanoseconds = i128;
gpa: Allocator,
events: std.ArrayList(Event),
mouse_inside: bool = false,
game: Game,
last_frame_at_ns: Nanoseconds,
frame_dt_ns: Nanoseconds,
time_ns: Nanoseconds,
down_keys: std.EnumSet(KeyCode) = .initEmpty(),
pressed_keys: std.EnumSet(KeyCode) = .initEmpty(),
released_keys: std.EnumSet(KeyCode) = .initEmpty(),
pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}),
pub fn init(self: *Window, gpa: Allocator) !void {
var events: std.ArrayList(Event) = .empty;
errdefer events.deinit(gpa);
try events.ensureTotalCapacityPrecise(gpa, 50);
try Gfx.init(.{
.allocator = gpa,
.logger = .{ .func = sokolLogCallback }
});
var game = try Game.init(gpa);
errdefer game.deinit();
self.* = Window{
.gpa = gpa,
.events = events,
.last_frame_at_ns = std.time.nanoTimestamp(),
.frame_dt_ns = 0,
.time_ns = 0,
.game = game
};
}
pub fn deinit(self: *Window) void {
const gpa = self.gpa;
self.game.deinit();
self.events.deinit(gpa);
Gfx.deinit();
}
pub fn frame(self: *Window) !void {
const now = std.time.nanoTimestamp();
self.frame_dt_ns = now - self.last_frame_at_ns;
self.last_frame_at_ns = now;
self.time_ns += self.frame_dt_ns;
Gfx.beginFrame();
defer Gfx.endFrame();
self.pressed_keys = .initEmpty();
self.released_keys = .initEmpty();
for (self.events.items) |e| {
switch (e) {
.key_pressed => |opts| {
if (!opts.repeat) {
self.pressed_keys_at.put(opts.code, self.time_ns);
self.pressed_keys.insert(opts.code);
self.down_keys.insert(opts.code);
}
},
.key_released => |key_code| {
self.down_keys.remove(key_code);
self.released_keys.insert(key_code);
self.pressed_keys_at.remove(key_code);
},
.mouse_leave => {
var iter = self.down_keys.iterator();
while (iter.next()) |key_code| {
self.released_keys.insert(key_code);
}
self.down_keys = .initEmpty();
self.pressed_keys_at = .init(.{});
},
else => {}
}
}
self.events.clearRetainingCapacity();
// TODO: Render to a lower resolution instead of scaling.
// To avoid pixel bleeding in spritesheet artifacts
const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf());
const scale = @floor(@min(
window_size.x / self.game.canvas_size.x,
window_size.y / self.game.canvas_size.y,
));
var filler_size: Vec2 = Vec2.sub(window_size, self.game.canvas_size.multiplyScalar(scale)).multiplyScalar(0.5);
filler_size.x = @round(filler_size.x);
filler_size.y = @round(filler_size.y);
const input = self.game.getInput(self);
{
Gfx.pushTransform(filler_size, scale);
defer Gfx.popTransform();
try self.game.tick(input);
}
const bg_color = rgb(0, 0, 0);
Gfx.drawRectangle(
.init(0, 0),
.init(window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, window_size.y - filler_size.y),
.init(window_size.x, filler_size.y),
bg_color
);
Gfx.drawRectangle(
.init(0, 0),
.init(filler_size.x, window_size.y),
bg_color
);
Gfx.drawRectangle(
.init(window_size.x - filler_size.x, 0),
.init(filler_size.x, window_size.y),
bg_color
);
try self.game.debug();
}
pub fn isKeyDown(self: *Window, key_code: KeyCode) bool {
return self.down_keys.contains(key_code);
}
pub fn getKeyDownDuration(self: *Window, key_code: KeyCode) ?f64 {
if (!self.isKeyDown(key_code)) {
return null;
}
const pressed_at_ns = self.pressed_keys_at.get(key_code).?;
const duration_ns = self.time_ns - pressed_at_ns;
return @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s;
}
pub fn isKeyPressed(self: *Window, key_code: KeyCode) bool {
return self.pressed_keys.contains(key_code);
}
pub fn isKeyReleased(self: *Window, key_code: KeyCode) bool {
return self.released_keys.contains(key_code);
}
pub fn getKeyState(self: *Window, key_code: KeyCode) KeyState {
return KeyState{
.down = self.isKeyDown(key_code),
.released = self.isKeyReleased(key_code),
.pressed = self.isKeyPressed(key_code),
.down_duration = self.getKeyDownDuration(key_code)
};
}
fn appendEvent(self: *Window, e: Event) !void {
self.events.appendBounded(e) catch return error.EventQueueFull;
}
pub fn event(self: *Window, e_ptr: [*c]const sapp.Event) !bool {
if (Gfx.event(e_ptr)) {
try self.appendEvent(Event{
.mouse_leave = {}
});
self.mouse_inside = false;
return true;
}
const e = e_ptr.*;
blk: switch (e.type) {
.MOUSE_DOWN => {
const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.appendEvent(Event{
.mouse_pressed = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_UP => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk;
try self.appendEvent(Event{
.mouse_released = .{
.button = mouse_button,
.position = Vec2.init(e.mouse_x, e.mouse_y)
}
});
return true;
},
.MOUSE_MOVE => {
if (!self.mouse_inside) {
try self.appendEvent(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
try self.appendEvent(Event{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
});
self.mouse_inside = true;
return true;
},
.MOUSE_ENTER => {
if (!self.mouse_inside) {
try self.appendEvent(Event{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
});
}
self.mouse_inside = true;
return true;
},
.RESIZED => {
if (self.mouse_inside) {
try self.appendEvent(Event{
.mouse_leave = {}
});
}
try self.appendEvent(Event{
.window_resize = {}
});
self.mouse_inside = false;
return true;
},
.MOUSE_LEAVE => {
if (self.mouse_inside) {
try self.appendEvent(Event{
.mouse_leave = {}
});
}
self.mouse_inside = false;
return true;
},
.MOUSE_SCROLL => {
try self.appendEvent(Event{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
});
return true;
},
.KEY_DOWN => {
try self.appendEvent(Event{
.key_pressed = .{
.code = @enumFromInt(@intFromEnum(e.key_code)),
.repeat = e.key_repeat
}
});
return true;
},
.KEY_UP => {
try self.appendEvent(Event{
.key_released = @enumFromInt(@intFromEnum(e.key_code))
});
return true;
},
.CHAR => {
try self.appendEvent(Event{
.char = @intCast(e.char_code)
});
return true;
},
.QUIT_REQUESTED => {
// TODO: handle quit request. Maybe show confirmation window in certain cases.
},
else => {}
}
return false;
}
fn cStrToZig(c_str: [*c]const u8) [:0]const u8 {
return std.mem.span(c_str);
}
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
const log_sokol = std.log.scoped(.sokol);
if (log_level == 0) {
log_sokol.err(format, args);
} else if (log_level == 1) {
log_sokol.err(format, args);
} else if (log_level == 2) {
log_sokol.warn(format, args);
} else {
log_sokol.info(format, args);
}
}
pub fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void {
_ = user_data;
if (filename != null) {
const format = "[{s}][id:{}] {s}:{}: {s}";
const args = .{
cStrToZig(tag orelse "-"),
log_item,
std.fs.path.basename(cStrToZig(filename orelse "-")),
line_nr,
cStrToZig(message orelse "")
};
sokolLogFmt(log_level, format, args);
} else {
const format = "[{s}][id:{}] {s}";
const args = .{
cStrToZig(tag orelse "-"),
log_item,
cStrToZig(message orelse "")
};
sokolLogFmt(log_level, format, args);
}
}