504 lines
12 KiB
Zig
504 lines
12 KiB
Zig
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");
|
|
|
|
const rgb = Math.rgb;
|
|
const Rect = Math.Rect;
|
|
const Vec2 = Math.Vec2;
|
|
const Vec4 = Math.Vec4;
|
|
|
|
pub const Nanoseconds = u64;
|
|
|
|
pub const KeyCode = enum {
|
|
SPACE,
|
|
APOSTROPHE,
|
|
COMMA,
|
|
MINUS,
|
|
PERIOD,
|
|
SLASH,
|
|
_0,
|
|
_1,
|
|
_2,
|
|
_3,
|
|
_4,
|
|
_5,
|
|
_6,
|
|
_7,
|
|
_8,
|
|
_9,
|
|
SEMICOLON,
|
|
EQUAL,
|
|
A,
|
|
B,
|
|
C,
|
|
D,
|
|
E,
|
|
F,
|
|
G,
|
|
H,
|
|
I,
|
|
J,
|
|
K,
|
|
L,
|
|
M,
|
|
N,
|
|
O,
|
|
P,
|
|
Q,
|
|
R,
|
|
S,
|
|
T,
|
|
U,
|
|
V,
|
|
W,
|
|
X,
|
|
Y,
|
|
Z,
|
|
LEFT_BRACKET,
|
|
BACKSLASH,
|
|
RIGHT_BRACKET,
|
|
GRAVE_ACCENT,
|
|
WORLD_1,
|
|
WORLD_2,
|
|
ESCAPE,
|
|
ENTER,
|
|
TAB,
|
|
BACKSPACE,
|
|
INSERT,
|
|
DELETE,
|
|
RIGHT,
|
|
LEFT,
|
|
DOWN,
|
|
UP,
|
|
PAGE_UP,
|
|
PAGE_DOWN,
|
|
HOME,
|
|
END,
|
|
CAPS_LOCK,
|
|
SCROLL_LOCK,
|
|
NUM_LOCK,
|
|
PRINT_SCREEN,
|
|
PAUSE,
|
|
F1,
|
|
F2,
|
|
F3,
|
|
F4,
|
|
F5,
|
|
F6,
|
|
F7,
|
|
F8,
|
|
F9,
|
|
F10,
|
|
F11,
|
|
F12,
|
|
F13,
|
|
F14,
|
|
F15,
|
|
F16,
|
|
F17,
|
|
F18,
|
|
F19,
|
|
F20,
|
|
F21,
|
|
F22,
|
|
F23,
|
|
F24,
|
|
F25,
|
|
KP_0,
|
|
KP_1,
|
|
KP_2,
|
|
KP_3,
|
|
KP_4,
|
|
KP_5,
|
|
KP_6,
|
|
KP_7,
|
|
KP_8,
|
|
KP_9,
|
|
KP_DECIMAL,
|
|
KP_DIVIDE,
|
|
KP_MULTIPLY,
|
|
KP_SUBTRACT,
|
|
KP_ADD,
|
|
KP_ENTER,
|
|
KP_EQUAL,
|
|
LEFT_SHIFT,
|
|
LEFT_CONTROL,
|
|
LEFT_ALT,
|
|
LEFT_SUPER,
|
|
RIGHT_SHIFT,
|
|
RIGHT_CONTROL,
|
|
RIGHT_ALT,
|
|
RIGHT_SUPER,
|
|
MENU,
|
|
};
|
|
|
|
pub const MouseButton = enum {
|
|
left,
|
|
right,
|
|
middle,
|
|
};
|
|
|
|
pub const TextureId = enum(u16) { _ };
|
|
pub const FontId = enum(u16) { _ };
|
|
pub const AudioId = enum(u16) { _ };
|
|
|
|
pub const Sprite = struct {
|
|
texture: TextureId,
|
|
uv: Rect
|
|
};
|
|
|
|
fn ButtonStateSet(E: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
down: std.EnumSet(E),
|
|
pressed: std.EnumSet(E),
|
|
released: std.EnumSet(E),
|
|
pressed_at: std.EnumMap(E, Nanoseconds),
|
|
|
|
pub const empty = Self{
|
|
.down = .initEmpty(),
|
|
.pressed = .initEmpty(),
|
|
.released = .initEmpty(),
|
|
.pressed_at = .init(.{}),
|
|
};
|
|
|
|
pub fn press(self: *Self, button: E, now: Nanoseconds) void {
|
|
self.pressed_at.put(button, now);
|
|
self.pressed.insert(button);
|
|
self.down.insert(button);
|
|
}
|
|
|
|
pub fn release(self: *Self, button: E) void {
|
|
self.down.remove(button);
|
|
self.released.insert(button);
|
|
self.pressed_at.remove(button);
|
|
}
|
|
|
|
pub fn releaseAll(self: *Self) void {
|
|
var iter = self.down.iterator();
|
|
while (iter.next()) |key_code| {
|
|
self.released.insert(key_code);
|
|
}
|
|
self.down = .initEmpty();
|
|
self.pressed_at = .init(.{});
|
|
}
|
|
};
|
|
}
|
|
|
|
pub const GraphicsCommand = union(enum) {
|
|
pub const DrawRectangle = struct {
|
|
rect: Rect,
|
|
color: Vec4,
|
|
sprite: ?Sprite = null,
|
|
rotation: f32 = 0,
|
|
origin: Vec2 = .init(0, 0),
|
|
};
|
|
|
|
pub const DrawLine = struct {
|
|
pos1: Vec2,
|
|
pos2: Vec2,
|
|
color: Vec4,
|
|
width: f32
|
|
};
|
|
|
|
pub const DrawText = struct {
|
|
pos: Vec2,
|
|
text: []const u8,
|
|
font: FontId,
|
|
size: f32,
|
|
color: Vec4,
|
|
};
|
|
|
|
push_scissor: Rect,
|
|
pop_scissor: void,
|
|
draw_rectangle: DrawRectangle,
|
|
draw_line: DrawLine,
|
|
draw_text: DrawText,
|
|
push_transformation: struct {
|
|
translation: Vec2,
|
|
scale: Vec2
|
|
},
|
|
pop_transformation: void
|
|
};
|
|
|
|
pub const AudioCommand = union(enum) {
|
|
pub const Play = struct {
|
|
id: AudioId,
|
|
volume: f32 = 1
|
|
};
|
|
|
|
play: Play,
|
|
};
|
|
|
|
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 Frame = struct {
|
|
arena: std.heap.ArenaAllocator,
|
|
time_ns: Nanoseconds,
|
|
dt_ns: Nanoseconds,
|
|
|
|
char_mapping: std.EnumMap(KeyCode, u21),
|
|
keyboard: ButtonStateSet(KeyCode),
|
|
mouse_button: ButtonStateSet(MouseButton),
|
|
mouse_position: ?Vec2,
|
|
hide_cursor: bool,
|
|
|
|
audio_commands: std.ArrayList(AudioCommand),
|
|
|
|
graphics_commands: std.ArrayList(GraphicsCommand),
|
|
canvas_size: ?Vec2,
|
|
screen_size: Vec2,
|
|
clear_color: Vec4,
|
|
|
|
pub fn init(gpa: std.mem.Allocator) Frame {
|
|
return Frame{
|
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
|
.time_ns = 0,
|
|
.dt_ns = 0,
|
|
.char_mapping = .{},
|
|
.keyboard = .empty,
|
|
.mouse_button = .empty,
|
|
.mouse_position = null,
|
|
.graphics_commands = .empty,
|
|
.canvas_size = null,
|
|
.clear_color = rgb(0, 0, 0),
|
|
.audio_commands = .empty,
|
|
.hide_cursor = false,
|
|
.screen_size = .init(0, 0),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Frame) void {
|
|
self.arena.deinit();
|
|
}
|
|
|
|
pub fn deltaTime(self: Frame) f32 {
|
|
return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s;
|
|
}
|
|
|
|
pub fn time(self: Frame) f64 {
|
|
return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s;
|
|
}
|
|
|
|
// --------------------- Audio ----------------------- //
|
|
|
|
fn pushAudioCommand(self: *Frame, command: AudioCommand) void {
|
|
const arena = self.arena.allocator();
|
|
|
|
self.audio_commands.append(arena, command) catch |e| {
|
|
log.warn("Failed to play audio: {}", .{e});
|
|
};
|
|
}
|
|
|
|
pub fn playAudio(self: *Frame, options: AudioCommand.Play) void {
|
|
self.pushAudioCommand(.{
|
|
.play = options,
|
|
});
|
|
}
|
|
|
|
// --------------------- Graphics ----------------------- //
|
|
|
|
fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
|
|
const arena = self.arena.allocator();
|
|
|
|
self.graphics_commands.append(arena, command) catch |e|{
|
|
log.warn("Failed to push graphics command: {}", .{e});
|
|
};
|
|
}
|
|
|
|
pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void {
|
|
self.pushGraphicsCommand(.{ .draw_rectangle = opts });
|
|
}
|
|
|
|
pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void {
|
|
self.pushGraphicsCommand(.{
|
|
.draw_line = .{
|
|
.pos1 = pos1,
|
|
.pos2 = pos2,
|
|
.width = width,
|
|
.color = color
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
|
|
// TODO: Don't use line segments
|
|
self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
|
|
self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
|
|
self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
|
|
self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
|
|
}
|
|
|
|
pub fn pushScissor(self: *Frame, rect: Rect) void {
|
|
self.pushGraphicsCommand(.{
|
|
.push_scissor = rect
|
|
});
|
|
}
|
|
|
|
pub fn popScissor(self: *Frame) void {
|
|
self.pushGraphicsCommand(.{
|
|
.pop_scissor = {}
|
|
});
|
|
}
|
|
|
|
pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void {
|
|
self.pushGraphicsCommand(.{
|
|
.push_transformation = .{
|
|
.translation = translation,
|
|
.scale = scale
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn popTransform(self: *Frame) void {
|
|
self.pushGraphicsCommand(.{
|
|
.pop_transformation = {}
|
|
});
|
|
}
|
|
|
|
pub const DrawTextOptions = struct {
|
|
font: FontId,
|
|
size: f32 = 16,
|
|
color: Vec4 = rgb(255, 255, 255),
|
|
};
|
|
|
|
pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) void {
|
|
const arena = self.arena.allocator();
|
|
const text_dupe = arena.dupe(u8, text) catch |e| {
|
|
log.warn("Failed to draw text: {}", .{e});
|
|
return;
|
|
};
|
|
|
|
self.pushGraphicsCommand(.{
|
|
.draw_text = .{
|
|
.pos = position,
|
|
.text = text_dupe,
|
|
.size = opts.size,
|
|
.font = opts.font,
|
|
.color = opts.color,
|
|
}
|
|
});
|
|
}
|
|
|
|
// --------------------- Mouse ----------------------- //
|
|
|
|
pub fn isMousePressed(self: Frame, button: MouseButton) bool {
|
|
return self.mouse_button.pressed.contains(button);
|
|
}
|
|
|
|
pub fn isMouseDown(self: Frame, button: MouseButton) bool {
|
|
return self.mouse_button.down.contains(button);
|
|
}
|
|
|
|
pub fn isMouseReleased(self: Frame, button: MouseButton) bool {
|
|
return self.mouse_button.released.contains(button);
|
|
}
|
|
|
|
// --------------------- Keyboard ----------------------- //
|
|
|
|
pub fn isKeyDown(self: Frame, key_code: KeyCode) bool {
|
|
return self.keyboard.down.contains(key_code);
|
|
}
|
|
|
|
pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool {
|
|
return self.keyboard.pressed.contains(key_code);
|
|
}
|
|
|
|
pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool {
|
|
return self.keyboard.released.contains(key_code);
|
|
}
|
|
|
|
pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 {
|
|
if (!self.isKeyDown(key_code)) {
|
|
return null;
|
|
}
|
|
|
|
const pressed_at_ns = self.keyboard.pressed_at.get(key_code).?;
|
|
const duration_ns = self.time_ns - pressed_at_ns;
|
|
|
|
return @as(f32, @floatFromInt(duration_ns)) / std.time.ns_per_s;
|
|
}
|
|
|
|
pub fn getKeyState(self: 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(key_code)
|
|
};
|
|
}
|
|
};
|
|
|
|
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 });
|
|
}
|
|
}
|