Compare commits

...

3 Commits

19 changed files with 2049 additions and 1850 deletions

View File

@ -29,9 +29,6 @@ pub fn build(b: *std.Build) !void {
exe_mod.linkLibrary(tracy_dependency.artifact("tracy"));
exe_mod.addImport("tracy", tracy_dependency.module("tracy"));
const stb_dependency = b.dependency("stb", .{});
exe_mod.addIncludePath(stb_dependency.path("."));
const sokol_c_dependency = b.dependency("sokol_c", .{});
exe_mod.addIncludePath(sokol_c_dependency.path("util"));
@ -41,6 +38,9 @@ pub fn build(b: *std.Build) !void {
const tiled_dependency = b.dependency("tiled", .{});
exe_mod.addImport("tiled", tiled_dependency.module("tiled"));
const stb_image_dependency = b.dependency("stb_image", .{});
exe_mod.addImport("stb_image", stb_image_dependency.module("stb_image"));
const sokol_dependency = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
@ -66,11 +66,6 @@ pub fn build(b: *std.Build) !void {
.flags = cflags.items
});
exe_mod.addCSourceFile(.{
.file = b.path("src/libs/stb_image.c"),
.flags = &.{}
});
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = target,

View File

@ -17,10 +17,6 @@
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
},
.stb = .{
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
},
.sokol_c = .{
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
@ -31,6 +27,9 @@
},
.tiled = .{
.path = "./libs/tiled"
},
.stb_image = .{
.path = "./libs/stb_image"
}
},
.paths = .{

37
libs/stb_image/build.zig Normal file
View File

@ -0,0 +1,37 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("stb_image", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
const stb_dependency = b.dependency("stb", .{});
mod.addIncludePath(stb_dependency.path("."));
mod.addCSourceFile(.{
.file = b.path("src/stb_image_impl.c"),
.flags = &.{}
});
const lib = b.addLibrary(.{
.name = "stb_image",
.root_module = mod
});
b.installArtifact(lib);
{
const tests = b.addTest(.{
.root_module = mod
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}
}

View File

@ -0,0 +1,17 @@
.{
.name = .stb_image,
.version = "0.0.0",
.fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.stb = .{
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,30 @@
const std = @import("std");
const c = @cImport({
@cInclude("stb_image.h");
});
const STBImage = @This();
rgba8_pixels: [*c]u8,
width: u32,
height: u32,
pub fn load(png_data: []const u8) !STBImage {
var width: c_int = undefined;
var height: c_int = undefined;
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
if (pixels == null) {
return error.InvalidPng;
}
return STBImage{
.rgba8_pixels = pixels,
.width = @intCast(width),
.height = @intCast(height)
};
}
pub fn deinit(self: *const STBImage) void {
c.stbi_image_free(self.rgba8_pixels);
}

66
src/assets.zig Normal file
View File

@ -0,0 +1,66 @@
const std = @import("std");
const Engine = @import("./engine/root.zig");
const Gfx = Engine.Graphics;
const Font = Gfx.Font;
const Math = @import("./engine/math.zig");
const Vec2 = Math.Vec2;
const Assets = @This();
pub const FontVariant = enum {
regular,
italic,
bold,
const Map = std.EnumArray(FontVariant, Font.Id);
};
pub const ImageId = enum {
tilemap
};
pub const TileId = enum {
player,
open_door,
locked_door
};
font_map: FontVariant.Map,
tile_coords: std.EnumArray(TileId, Vec2),
tilemap: Gfx.Image,
tile_size: Vec2,
tilemap_size: Vec2,
tilemap_view: Gfx.View,
pub fn init() !Assets {
const tilemap_data = try Gfx.STBImage.load(@embedFile("./assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
defer tilemap_data.deinit();
const tilemap = try Gfx.makeImageWithMipMaps(&.{
tilemap_data
});
const tilemap_view = try Gfx.makeView(tilemap);
return Assets{
.font_map = .init(.{
.regular = try Gfx.fonts.add("regular", @embedFile("./assets/roboto-font/Roboto-Regular.ttf")),
.italic = try Gfx.fonts.add("italic", @embedFile("./assets/roboto-font/Roboto-Italic.ttf")),
.bold = try Gfx.fonts.add("bold", @embedFile("./assets/roboto-font/Roboto-Bold.ttf")),
}),
.tile_coords = .init(.{
.player = .init(4, 0),
.locked_door = .init(7, 2),
.open_door = .init(5, 2)
}),
.tilemap = tilemap,
.tilemap_view = tilemap_view,
.tile_size = .init(8, 8),
.tilemap_size = .init(@floatFromInt(tilemap_data.width), @floatFromInt(tilemap_data.height))
};
}
pub fn deinit(self: *Assets) void {
_ = self; // autofix
}

614
src/engine/font.zig Normal file
View File

@ -0,0 +1,614 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const sokol = @import("sokol");
const sapp = sokol.app;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Rect = Math.Rect;
const Vec4 = Math.Vec4;
const Gfx = @import("./graphics.zig");
const c = @cImport({
@cInclude("sokol/sokol_gfx.h");
@cInclude("fontstash.h");
@cInclude("sokol_fontstash.h");
});
// Struct definition copies from fonstash.h
// TODO: Create a getter, to avoid copying this struct by hand
const FONSstate = extern struct {
font: c_int,
@"align": c_int,
size: f32,
color: c_uint,
blur: f32,
spacing: f32
};
const FONSfont = opaque{};
const FONSglyph = opaque{};
extern fn zig_isTopLeft(stash: ?*c.FONScontext) bool;
extern fn zig_getGlyphIndex(glyph: ?*FONSglyph) c_int;
extern fn zig_fons__getState(stash: ?*c.struct_FONScontext) ?*FONSstate;
extern fn zig_getFont(stash: ?*c.FONScontext, index: c_int) ?*FONSfont;
extern fn zig_fons__tt_getPixelHeightScale(font: ?*FONSfont, size: f32) f32;
extern fn zig_fons__getVertAlign(stash: ?*c.FONScontext, font: ?*FONSfont, @"align": c_int, isize: i16) f32;
extern fn zig_fons__getGlyph(
stash: ?*c.FONScontext,
font: ?*FONSfont,
codepoint: c_uint,
@"isize": i16,
iblur: i16
) ?*FONSglyph;
extern fn zig_fons__getQuad(
stash: ?*c.FONScontext,
font: ?*FONSfont,
prevGlyphIndex: c_int, glyph: ?*FONSglyph,
scale: f32,
spacing: f32,
x: ?*f32, y: ?*f32,
q: ?*c.FONSquad
) void;
const Font = @This();
pub const Id = enum(c_int) {
_,
pub const invalid: Id = @enumFromInt(c.FONS_INVALID);
};
pub const AlignX = enum {
left,
right,
center,
fn toFONSAlignment(self: AlignX) c_int {
return switch (self) {
.left => c.FONS_ALIGN_LEFT,
.center => c.FONS_ALIGN_CENTER,
.right => c.FONS_ALIGN_RIGHT,
};
}
};
pub const AlignY = enum {
top,
middle,
bottom,
baseline,
fn toFONSAlignment(self: AlignY) c_int {
return switch (self) {
.top => c.FONS_ALIGN_TOP,
.middle => c.FONS_ALIGN_MIDDLE,
.bottom => c.FONS_ALIGN_BOTTOM,
.baseline => c.FONS_ALIGN_BASELINE,
};
}
};
pub const QuadIterator = struct {
ctx: Context,
x: f32,
y: f32,
next_x: f32,
next_y: f32,
spacing: f32,
scale: f32,
isize: c_short,
iblur: c_short,
prev_glyph_index: c_int,
font: *FONSfont,
pub fn init(ctx: Context, font: Font) QuadIterator {
const stash = ctx.fons_context;
ctx.setAsCurrent(font);
const x: f32 = 0;
var y: f32 = 0;
const state = zig_fons__getState(stash).?;
const fons_font = zig_getFont(stash, state.font) orelse @panic("Invalid font");
const font_isize: c_short = @intFromFloat(@trunc(state.size * 10.0));
const font_iblur: c_short = @intFromFloat(@trunc(state.blur));
const scale = zig_fons__tt_getPixelHeightScale(fons_font, @as(f32, @floatFromInt(font_isize)) / 10.0);
assert(state.@"align" & c.FONS_ALIGN_LEFT != 0);
assert(state.@"align" & c.FONS_ALIGN_RIGHT == 0);
assert(state.@"align" & c.FONS_ALIGN_CENTER == 0);
// Align vertically.
y += zig_fons__getVertAlign(stash, fons_font, @truncate(state.@"align"), font_isize);
return QuadIterator{
.ctx = ctx,
.x = x,
.y = y,
.next_x = x,
.next_y = y,
.isize = font_isize,
.iblur = font_iblur,
.scale = scale,
.spacing = state.spacing,
.font = fons_font,
.prev_glyph_index = -1
};
}
pub fn next(self: *QuadIterator, codepoint: u21) ?Rect {
const stash = self.ctx.fons_context;
self.x = self.next_x;
self.y = self.next_y;
const glyph = zig_fons__getGlyph(stash, self.font, codepoint, self.isize, self.iblur);
if (glyph != null) {
var q: c.FONSquad = .{};
zig_fons__getQuad(stash, self.font, self.prev_glyph_index, glyph, self.scale, self.spacing, &self.x, &self.y, &q);
self.prev_glyph_index = zig_getGlyphIndex(glyph);
return Rect.init(q.x0, q.y0, q.x1 - q.x0, q.y1 - q.y0);
} else {
self.prev_glyph_index = -1;
return null;
}
}
pub const Ascii = struct {
iter: QuadIterator,
text: []const u8,
index: usize,
pub fn init(ctx: Context, font: Font, text: []const u8) Ascii {
return Ascii{
.iter = .init(ctx, font),
.text = text,
.index = 0
};
}
pub fn next(self: *Ascii) ?Rect {
while (self.index < self.text.len) {
const codepoint_len = std.unicode.utf8ByteSequenceLength(self.text[self.index]) catch {
self.index += 1;
continue;
};
if (self.index + codepoint_len > self.text.len) {
self.index = self.text.len;
return null;
}
const codepoint = std.unicode.utf8Decode(self.text[self.index..][0..codepoint_len]) catch {
self.index += 1;
continue;
};
self.index += codepoint_len;
if (self.iter.next(codepoint)) |rect| {
return rect;
}
}
return null;
}
pub fn last(self: *Ascii) ?Rect {
var last_rect: ?Rect = null;
while (self.next()) |rect| {
last_rect = rect;
}
return last_rect;
}
};
pub const Unicode = struct {
iter: QuadIterator,
text: []const u21,
index: usize,
pub fn init(ctx: Context, font: Font, text: []const u21) Unicode {
return Unicode{
.iter = .init(ctx, font),
.text = text,
.index = 0
};
}
pub fn next(self: *Unicode) ?Rect {
while (self.index < self.text.len) {
const next_rect = self.iter.next(self.text[self.index]);
self.index += 1;
if (next_rect) |rect| {
return rect;
}
}
return null;
}
pub fn last(self: *Unicode) ?Rect {
var last_rect: ?Rect = null;
while (self.next()) |rect| {
last_rect = rect;
}
return last_rect;
}
};
};
pub const Context = struct {
fons_context: *c.struct_FONScontext,
pub fn init(atlas_size: f32) !Context {
const dpi_scale = sapp.dpiScale();
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
const fons_context = c.sfons_create(&c.sfons_desc_t{
.width = @intCast(atlas_dim),
.height = @intCast(atlas_dim),
});
if (fons_context == null) {
return error.sfons_create;
}
return Context{
.fons_context = fons_context.?
};
}
pub fn deinit(self: Context) void {
c.sfons_destroy(self.fons_context);
}
pub fn add(self: Context, name: [*c]const u8, data: []const u8) !Id {
const font_id = c.fonsAddFontMem(
self.fons_context,
name,
@constCast(data.ptr),
@intCast(data.len),
0
);
if (font_id == c.FONS_INVALID) {
return error.fonsAddFontMem;
}
return @enumFromInt(font_id);
}
pub fn clearState(self: Context) void {
c.fonsClearState(self.fons_context);
}
pub fn flush(self: Context) void {
c.sfons_flush(self.fons_context);
}
fn setAsCurrent(self: Context, font: Font) void {
const fs = self.fons_context;
const dpi_scale = sapp.dpiScale();
c.fonsSetFont(fs, @intFromEnum(font.id));
c.fonsSetSize(fs, font.size * dpi_scale);
c.fonsSetAlign(fs, Font.AlignX.left.toFONSAlignment() | Font.AlignY.top.toFONSAlignment());
c.fonsSetSpacing(fs, font.spacing);
}
pub fn drawText(self: Context, font: Font, pos: Vec2, color: Vec4, text: []const u8) void {
const fs = self.fons_context;
// TODO: Test if calling `.setAsCurrent()` isn't expensive
self.setAsCurrent(font);
const fons_color = c.sfons_rgba(
@intFromFloat(color.x * 255),
@intFromFloat(color.y * 255),
@intFromFloat(color.z * 255),
@intFromFloat(color.w * 255)
);
c.fonsSetColor(fs, fons_color);
Gfx.pushTransform(.init(0, 0), 1.0/64.0);
defer Gfx.popTransform();
_ = c.fonsDrawText(
fs,
pos.x*64,
pos.y*64,
text.ptr,
text.ptr + text.len
);
}
pub fn getBounds(self: Context, font: Font, text: []const u8) Rect {
const fs = self.fons_context;
self.setAsCurrent(font);
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, 0, 0, text.ptr, text.ptr + text.len, &line_bounds);
const min_x = line_bounds[0];
const min_y = line_bounds[1];
const max_x = line_bounds[2];
const max_y = line_bounds[3];
const width = max_x - min_x;
const height = max_y - min_y;
return Rect.init(min_x, min_y, width, height);
}
pub fn getBoundsUtf8(self: Context, font: Font, text: []const u21) Rect {
const fs = self.fons_context;
self.setAsCurrent(font);
var line_bounds: [4]f32 = undefined;
_ = fonsTextBoundsUtf8(fs, 0, 0, text, &line_bounds);
const min_x = line_bounds[0];
const min_y = line_bounds[1];
const max_x = line_bounds[2];
const max_y = line_bounds[3];
const width = max_x - min_x;
const height = max_y - min_y;
return Rect.init(min_x, min_y, width, height);
}
pub fn measure(self: Context, font: Font, text: []const u8) Vec2 {
const bounds = self.getBounds(font, text);
return bounds.size;
}
pub fn quadIter(self: Context, font: Font, text: []const u8) QuadIterator.Ascii {
return QuadIterator.Ascii.init(self, font, text);
}
pub fn quadUnicodeIter(self: Context, font: Font, text: []const u21) QuadIterator.Unicode {
return QuadIterator.Unicode.init(self, font, text);
}
// A reimplementation of `fonsTextBounds` which uses an array of already decoded codepoints
fn fonsTextBoundsUtf8(stash: ?*c.struct_FONScontext, initial_x: f32, initial_y: f32, str: []const u21, bounds: [*c]f32) f32 {
if (stash == null) {
return 0;
}
var x = initial_x;
var y = initial_y;
const state = zig_fons__getState(stash).?;
const font = zig_getFont(stash, state.font) orelse return 0;
const font_isize: i16 = @intFromFloat(@trunc(state.size * 10.0));
const font_iblur: i16 = @intFromFloat(@trunc(state.blur));
const scale = zig_fons__tt_getPixelHeightScale(font, @as(f32, @floatFromInt(font_isize)) / 10.0);
// Align vertically.
y += zig_fons__getVertAlign(stash, font, @truncate(state.@"align"), font_isize);
var minx = x;
var maxx = x;
var miny = y;
var maxy = y;
const startx = x;
var prevGlyphIndex: c_int = -1;
for (str) |codepoint| {
const glyph = zig_fons__getGlyph(stash, font, codepoint, font_isize, font_iblur);
if (glyph != null) {
var q: c.FONSquad = .{};
zig_fons__getQuad(stash, font, prevGlyphIndex, glyph, scale, state.spacing, &x, &y, &q);
if (q.x0 < minx) minx = q.x0;
if (q.x1 > maxx) maxx = q.x1;
if (zig_isTopLeft(stash)) {
if (q.y0 < miny) miny = q.y0;
if (q.y1 > maxy) maxy = q.y1;
} else {
if (q.y1 < miny) miny = q.y1;
if (q.y0 > maxy) maxy = q.y0;
}
prevGlyphIndex = zig_getGlyphIndex(glyph);
} else {
prevGlyphIndex = -1;
}
}
const advance = x - startx;
// Align horizontally
if ((state.@"align" & c.FONS_ALIGN_LEFT) != 0) {
// empty
} else if ((state.@"align" & c.FONS_ALIGN_RIGHT) != 0) {
minx -= advance;
maxx -= advance;
} else if ((state.@"align" & c.FONS_ALIGN_CENTER) != 0) {
minx -= advance * 0.5;
maxx -= advance * 0.5;
}
if (bounds != null) {
bounds[0] = minx;
bounds[1] = miny;
bounds[2] = maxx;
bounds[3] = maxy;
}
return advance;
}
};
pub const DrawTextContext = struct {
pub const Line = struct {
// Assumes that the text won't be invalidted between `queue*` and `draw` functions
text: []const u8,
pos: Vec2,
bounds: Rect,
};
// TODO: Add support for multiple font in a single context.
// I.e. each command should be able to have a separate font
ctx: Context,
font: Font,
line_height: f32 = 0,
pos: Vec2 = Vec2.zero,
lines: std.ArrayListUnmanaged(Line) = .empty,
bounds: Rect = Rect.zero,
pub fn init(ctx: Context, font: Font) DrawTextContext {
var self = DrawTextContext{
.ctx = ctx,
.font = font
};
const fs = ctx.fons_context;
self.ctx.setAsCurrent(font);
c.fonsVertMetrics(fs, null, null, &self.line_height);
return self;
}
pub fn deinit(self: *DrawTextContext, allocator: Allocator) void {
self.lines.deinit(allocator);
}
pub fn queueText(self: *DrawTextContext, allocator: Allocator, text: []const u8) !void {
var line_iter = std.mem.splitScalar(u8, text, '\n');
while (line_iter.next()) |line| {
try self.queueLine(allocator, line);
}
}
pub fn measureText(self: *const DrawTextContext, text: []const u8) Rect {
const fs = self.ctx.fons_context;
self.ctx.setAsCurrent(self.font);
var min_x: f32 = 0;
var max_x: f32 = 0;
var min_y: f32 = 0;
var max_y: f32 = 0;
var pos = Vec2.zero;
var line_iter = std.mem.splitScalar(u8, text, '\n');
while (line_iter.next()) |line| {
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, pos.x, pos.y, line.ptr, line.ptr + line.len, &line_bounds);
min_x = @min(min_x, line_bounds[0]);
min_y = @min(min_y, line_bounds[1]);
max_x = @max(max_x, line_bounds[2]);
max_y = @max(max_y, line_bounds[3]);
pos.y += self.line_height * self.font.line_spacing;
}
return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
}
pub fn queueLine(self: *DrawTextContext, allocator: Allocator, line: []const u8) !void {
const fs = self.ctx.fons_context;
self.ctx.setAsCurrent(self.font);
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len, &line_bounds);
try self.lines.append(allocator, .{
.bounds = Rect.init(
line_bounds[0],
line_bounds[1],
line_bounds[2] - line_bounds[0],
line_bounds[3] - line_bounds[1],
),
.pos = self.pos,
.text = line
});
self.pos.y += self.line_height * self.font.line_spacing;
var min_x = self.bounds.pos.x;
var max_x = self.bounds.pos.x + self.bounds.size.x;
var min_y = self.bounds.pos.y;
var max_y = self.bounds.pos.y + self.bounds.size.y;
min_x = @min(min_x, line_bounds[0]);
min_y = @min(min_y, line_bounds[1]);
max_x = @max(max_x, line_bounds[2]);
max_y = @max(max_y, line_bounds[3]);
self.bounds = Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
}
pub fn draw(self: *DrawTextContext, pos: Vec2, color: Vec4) void {
for (self.lines.items) |cmd| {
self.ctx.drawText(self.font, pos.add(cmd.pos), color, cmd.text);
}
}
pub fn isEmpty(self: *DrawTextContext) bool {
return self.lines.items.len == 0;
}
// pub fn drawLine(self: *DrawTextContext, line: []const u8) void {
// const fs = fons_context;
//
// _ = c.fonsDrawText(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len);
// self.pos.y += self.line_height * self.font.line_spacing;
// }
//
// pub fn drawLines(self: *DrawTextContext, lines: []const []const u8) void {
// for (lines) |line| {
// self.drawLine(line);
// }
// }
//
// pub fn drawText(self: *DrawTextContext, text: []const u8) void {
// var line_iter = std.mem.splitScalar(u8, text, '\n');
// while (line_iter.next()) |line| {
// self.drawLine(line);
// }
// }
//
// pub fn measureLine(self: *DrawTextContext, line: []const u8) void {
// const fs = fons_context;
//
// var min_x: f32 = 0;
// var min_y: f32 = 0;
// var max_x: f32 = 0;
// var max_y: f32 = 0;
//
// var line_iter = std.mem.splitScalar(u8, text, '\n');
// var y: f32 = 0;
// while (line_iter.next()) |line| {
// var line_bounds: [4]f32 = undefined;
//
// _ = c.fonsTextBounds(fs, 0, y, line.ptr, line.ptr + line.len, &line_bounds);
// y += line_height * font.line_spacing;
//
// min_x = @min(min_x, line_bounds[0]);
// min_y = @min(min_y, line_bounds[1]);
// max_x = @max(max_x, line_bounds[2]);
// max_y = @max(max_y, line_bounds[3]);
// }
//
// return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
// }
pub fn end(self: *DrawTextContext) void {
_ = self;
}
};
id: Id,
size: f32,
spacing: f32 = 0,
line_spacing: f32 = 1,

460
src/engine/graphics.zig Normal file
View File

@ -0,0 +1,460 @@
const tracy = @import("tracy");
const sokol = @import("sokol");
const sg = sokol.gfx;
const sglue = sokol.glue;
const slog = sokol.log;
const sapp = sokol.app;
const simgui = sokol.imgui;
const sgl = sokol.gl;
const imgui = @import("imgui.zig");
const std = @import("std");
const assert = std.debug.assert;
pub const STBImage = @import("stb_image");
pub const Image = sg.Image;
pub const View = sg.View;
const c = @cImport({
@cInclude("sokol/sokol_gfx.h");
@cInclude("sokol/sokol_gl.h");
@cInclude("fontstash.h");
@cInclude("sokol_fontstash.h");
});
const Allocator = std.mem.Allocator;
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug.
// From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
// Maybe a patch for sokol could be made?
// More info:
// * https://github.com/libsdl-org/SDL/issues/11618
// * https://github.com/nimgl/nimgl/issues/59
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Vec3 = Math.Vec3;
const Vec4 = Math.Vec4;
const Mat4 = Math.Mat4;
const Rect = Math.Rect;
const rgb = Math.rgb;
const hex = Math.rgb_hex;
const rgba = Math.rgba;
const log = std.log.scoped(.graphics);
pub const Font = @import("./font.zig");
pub const white = rgb(255, 255, 255);
pub const black = rgb(0, 0, 0);
const Vertex = extern struct {
position: Vec2,
color: Vec4,
};
const Quad = [4]Vertex;
const DrawFrame = struct {
screen_size: Vec2 = Vec2.zero,
bg_color: Vec4 = rgb(0, 0, 0),
scissor_stack_buffer: [32]Rect = undefined,
scissor_stack: std.ArrayListUnmanaged(Rect) = .empty,
fn init(self: *DrawFrame) void {
self.* = DrawFrame{
.scissor_stack = .initBuffer(&self.scissor_stack_buffer)
};
}
};
pub const Borders = struct {
size: f32 = 0,
left: Vec4 = rgba(0, 0, 0, 0),
right: Vec4 = rgba(0, 0, 0, 0),
top: Vec4 = rgba(0, 0, 0, 0),
bottom: Vec4 = rgba(0, 0, 0, 0),
pub fn initAll(size: f32, color: Vec4) Borders {
return Borders{
.size = size,
.left = color,
.right = color,
.top = color,
.bottom = color,
};
}
};
pub const Corners = struct {
top_left: f32 = 0,
top_right: f32 = 0,
bottom_left: f32 = 0,
bottom_right: f32 = 0,
pub fn initAll(size: f32) Corners {
return Corners{
.top_left = size,
.top_right = size,
.bottom_left = size,
.bottom_right = size,
};
}
};
// The lower the better. Best quality with 1
pub var circle_quality: f32 = 6;
pub var draw_frame: DrawFrame = undefined;
var main_pipeline: sgl.Pipeline = .{};
var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{};
pub var fonts: Font.Context = undefined;
const Options = struct {
allocator: std.mem.Allocator,
logger: sg.Logger = .{},
};
inline fn structCast(T: type, value: anytype) T {
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
}
pub fn makeImageWithMipMaps(image_datas: []const STBImage) !sg.Image {
assert(image_datas.len > 0);
var data: sg.ImageData = .{};
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
for (image_datas) |mipmap_image| {
try mip_levels.appendBounded(.{
.ptr = mipmap_image.rgba8_pixels,
.size = mipmap_image.width * mipmap_image.height * 4
});
}
// TODO: Should error be checked?
const image = sg.makeImage(.{
.width = @intCast(image_datas[0].width),
.height = @intCast(image_datas[0].height),
.pixel_format = .RGBA8,
.usage = .{
.immutable = true
},
.num_mipmaps = @intCast(mip_levels.items.len),
.data = data
});
if (image.id == sg.invalid_id) {
return error.InvalidImage;
}
return image;
}
pub fn makeView(image: sg.Image) !sg.View {
const image_view = sg.makeView(.{
.texture = .{ .image = image }
});
if (image_view.id == sg.invalid_id) {
return error.InvalidView;
}
return image_view;
}
pub fn init(options: Options) !void {
draw_frame.init();
sg.setup(.{
.logger = options.logger,
.environment = sglue.environment(),
});
sgl.setup(.{
.logger = structCast(sgl.Logger, options.logger),
});
main_pipeline = sgl.makePipeline(.{
.colors = init: {
var colors: [8]sg.ColorTargetState = @splat(.{});
colors[0] = .{
.blend = .{
.enabled = true,
.src_factor_rgb = .SRC_ALPHA,
.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
.op_rgb = .ADD,
.src_factor_alpha = .ONE,
.dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
.op_alpha = .ADD,
},
};
break :init colors;
},
});
imgui.setup(options.allocator, .{
.logger = structCast(simgui.Logger, options.logger),
.no_default_font = true,
// TODO: Figure out a way to make imgui play nicely with UI
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
// Otherwise our own cursor should be used.
.disable_set_mouse_cursor = true
});
// TODO: Move this font loading to assets
imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16);
fonts = try Font.Context.init(512);
linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR,
.mag_filter = .LINEAR,
.mipmap_filter = .LINEAR,
.label = "linear-sampler",
});
nearest_sampler = sg.makeSampler(.{
.min_filter = .NEAREST,
.mag_filter = .NEAREST,
.mipmap_filter = .NEAREST,
.label = "nearest-sampler",
});
}
pub fn deinit() void {
imgui.shutdown();
fonts.deinit();
sgl.shutdown();
sg.shutdown();
}
inline fn v2fColor(x: f32, y: f32, color: Vec4) void {
sgl.v2fC4f(x, y, color.x, color.y, color.z, color.w);
}
inline fn v2fT2Color(x: f32, y: f32, u: f32, v: f32, color: Vec4) void {
sgl.v2fT2fC4f(x, y, u, v, color.x, color.y, color.z, color.w);
}
inline fn vertexesQuad(top_left: Vec2, top_right: Vec2, bottom_right: Vec2, bottom_left: Vec2, color: Vec4) void {
v2fColor(top_left.x, top_left.y, color);
v2fColor(top_right.x, top_right.y, color);
v2fColor(bottom_right.x, bottom_right.y, color);
v2fColor(bottom_left.x, bottom_left.y, color);
}
pub fn drawQuad(quad: [4]Vec2, color: Vec4) void {
sgl.beginQuads();
defer sgl.end();
for (quad) |position| {
v2fColor(position.x, position.y, color);
}
}
pub fn drawTriangle(tri: [3]Vec2, color: Vec4) void {
sgl.beginTriangles();
defer sgl.end();
for (tri) |position| {
v2fColor(position.x, position.y, color);
}
}
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void {
drawQuad(
.{
// Top left
pos,
// Top right
pos.add(.{ .x = size.x, .y = 0 }),
// Bottom right
pos.add(size),
// Bottom left
pos.add(.{ .x = 0, .y = size.y }),
},
color
);
}
pub fn drawTexturedRectangle(
pos: Vec2,
size: Vec2,
color: Vec4,
view: sg.View,
view_quad: Rect,
) void {
sgl.enableTexture();
defer sgl.disableTexture();
sgl.texture(
view,
nearest_sampler
);
const top_left = pos;
const top_right = pos.add(.{ .x = size.x, .y = 0 });
const bottom_right = pos.add(size);
const bottom_left = pos.add(.{ .x = 0, .y = size.y });
sgl.beginQuads();
defer sgl.end();
sgl.c4f(color.x, color.y, color.z, color.w);
sgl.v2fT2f(
top_left.x,
top_left.y,
view_quad.left(),
view_quad.top(),
);
sgl.v2fT2f(
top_right.x,
top_right.y,
view_quad.right(),
view_quad.top(),
);
sgl.v2fT2f(
bottom_right.x,
bottom_right.y,
view_quad.right(),
view_quad.bottom(),
);
sgl.v2fT2f(
bottom_left.x,
bottom_left.y,
view_quad.left(),
view_quad.bottom(),
);
}
pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
// TODO: Don't use line segments
drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
}
pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const step = to.sub(from).normalized().multiplyScalar(width/2);
const top_left = from.add(step.rotateLeft90());
const bottom_left = from.add(step.rotateRight90());
const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90());
drawQuad(
.{ top_right, top_left, bottom_left, bottom_right },
color
);
}
pub fn drawText(allocator: Allocator, pos: Vec2, font: Font, color: Vec4, text: []const u8) !void {
var ctx = Font.DrawTextContext.init(fonts, font);
defer ctx.deinit(allocator);
try ctx.queueText(allocator, text);
ctx.draw(pos, color);
}
pub fn measureText(font: Font, text: []const u8) Rect {
var ctx = Font.DrawTextContext.init(fonts, font);
return ctx.measureText(text);
}
pub fn pushScissor(rect: Rect) void {
draw_frame.scissor_stack.appendAssumeCapacity(rect);
sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true);
}
pub fn popScissor() void {
_ = draw_frame.scissor_stack.pop().?;
const rect = draw_frame.scissor_stack.getLast();
sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true);
}
pub fn pushTransform(translation: Vec2, scale: f32) void {
sgl.pushMatrix();
sgl.translate(translation.x, translation.y, 0);
sgl.scale(scale, scale, 1);
}
pub fn popTransform() void {
sgl.popMatrix();
}
fn createProjectionMatrix() Mat4 {
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
return Mat4.initIdentity()
.multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 }))
.multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 }))
.multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 }))
.multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 }))
.multiply(Mat4.initScale(.{
.x = 1/screen_size.x,
.y = 1/screen_size.y,
.z = 0
}));
}
pub fn beginFrame() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
draw_frame.init();
draw_frame.screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
draw_frame.scissor_stack.appendAssumeCapacity(Rect.init(0, 0, sapp.widthf(), sapp.heightf()));
imgui.newFrame(.{
.width = @intFromFloat(draw_frame.screen_size.x),
.height = @intFromFloat(draw_frame.screen_size.y),
.delta_time = sapp.frameDuration(),
.dpi_scale = sapp.dpiScale()
});
fonts.clearState();
sgl.defaults();
sgl.matrixModeProjection();
sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1);
sgl.loadPipeline(main_pipeline);
}
pub fn endFrame() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
assert(draw_frame.scissor_stack.items.len == 1);
var pass_action: sg.PassAction = .{};
pass_action.colors[0] = sg.ColorAttachmentAction{
.load_action = .CLEAR,
.clear_value = .{
.r = draw_frame.bg_color.x,
.g = draw_frame.bg_color.y,
.b = draw_frame.bg_color.z,
.a = draw_frame.bg_color.w
}
};
fonts.flush();
{
sg.beginPass(.{
.action = pass_action,
.swapchain = sglue.swapchain()
});
defer sg.endPass();
sgl.draw();
imgui.render();
}
sg.commit();
}

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,10 @@
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 Gfx = Engine.Graphics;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Assets = @import("./assets.zig");
const Entity = @This();
@ -27,5 +28,5 @@ locked: bool = false,
render_tile: ?union(enum) {
position: Vec2,
id: Gfx.TileId
id: Assets.TileId
} = null

View File

@ -2,28 +2,31 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb;
const rgb_hex = Math.rgb_hex;
const Engine = @import("./engine/root.zig");
const imgui = Engine.imgui;
const Gfx = Engine.Graphics;
const Vec2 = Engine.Math.Vec2;
const Vec4 = Engine.Math.Vec4;
const Rect = Engine.Math.Rect;
const rgb = Engine.Math.rgb;
const rgb_hex = Engine.Math.rgb_hex;
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 Assets = @import("./assets.zig");
const tiled = @import("tiled");
const Game = @This();
const KeyState = Engine.Input.KeyState;
pub const Input = struct {
dt: f64,
move_up: Window.KeyState,
move_down: Window.KeyState,
move_left: Window.KeyState,
move_right: Window.KeyState,
move_up: KeyState,
move_down: KeyState,
move_left: KeyState,
move_right: KeyState,
restart: bool
};
@ -61,6 +64,7 @@ gpa: Allocator,
canvas_size: Vec2,
level: Level,
assets: *Assets,
current_level: u32,
levels: std.ArrayList(Level),
@ -80,13 +84,14 @@ finale: bool = false,
finale_timer: ?Timer.Id = null,
finale_counter: u32 = 0,
pub fn init(gpa: Allocator) !Game {
pub fn init(gpa: Allocator, assets: *Assets) !Game {
var self = Game{
.gpa = gpa,
.canvas_size = (Vec2.init(20, 15)),
.level = .empty,
.levels = .empty,
.current_level = 0,
.assets = assets
};
errdefer self.deinit();
@ -323,37 +328,35 @@ fn moveEntity(self: *Game, entity_id: Entity.Id, dir: Vec2) bool {
return true;
}
pub fn getInput(self: *Game, window: *Window) Input {
pub fn getInput(self: *Game, frame: Engine.Frame) Input {
_ = self; // autofix
const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s;
const input = frame.input;
return Input{
.dt = dt,
.move_up = window.getKeyState(.W),
.move_down = window.getKeyState(.S),
.move_left = window.getKeyState(.A),
.move_right = window.getKeyState(.D),
.restart = window.isKeyPressed(.R)
.dt = frame.dt,
.move_up = input.getKeyState(frame, .W),
.move_down = input.getKeyState(frame, .S),
.move_left = input.getKeyState(frame, .A),
.move_right = input.getKeyState(frame, .D),
.restart = input.isKeyPressed(.R)
};
}
fn drawEntity(self: *Game, entity: *Entity) void {
_ = self; // autofix
if (entity.render_tile) |render_tile| {
var tile_coord = switch (render_tile) {
.id => |tile_id| Gfx.getTileCoords(tile_id),
.id => |tile_id| self.assets.tile_coords.get(tile_id),
.position => |position| position
};
if (entity.type == .door) {
if (entity.locked) {
tile_coord = Gfx.getTileCoords(.locked_door);
tile_coord = self.assets.tile_coords.get(.locked_door);
} else {
tile_coord = Gfx.getTileCoords(.open_door);
tile_coord = self.assets.tile_coords.get(.open_door);
}
}
Gfx.drawTile(tile_coord, entity.position, .init(1,1), rgb(255, 255, 255));
self.drawTile(tile_coord, entity.position);
}
}
@ -410,7 +413,7 @@ pub fn tickLevel(self: *Game, input: Input) !void {
var move: Vec2 = .init(0, 0);
if (can_move) {
const repeat_options = Window.KeyState.RepeatOptions{
const repeat_options = KeyState.RepeatOptions{
.first_at = 0.3,
.period = 0.1
};
@ -482,30 +485,40 @@ pub fn tickFinale(self: *Game) !void {
text: []const u8,
};
const regular_font = Gfx.Font{
.id = self.assets.font_map.get(.regular),
.size = 64
};
const bold_font = Gfx.Font{
.id = self.assets.font_map.get(.bold),
.size = 64
};
const lines = [5]Line{
.{
.pos = Vec2.init(1, 1),
.font = Gfx.Font.default,
.font = regular_font,
.text = "Congratulations scientist"
},
.{
.pos = Vec2.init(1, 2),
.font = Gfx.Font.default,
.font = regular_font,
.text = "You have passed the entrance exam"
},
.{
.pos = Vec2.init(1, 3),
.font = Gfx.Font.default,
.font = regular_font,
.text = "Here is your entry code"
},
.{
.pos = Vec2.init(1, 5),
.font = Gfx.Font.bold,
.font = bold_font,
.text = key
},
.{
.pos = Vec2.init(1, 7),
.font = Gfx.Font.default,
.font = regular_font,
.text = "I'll meet you at the lab"
}
};
@ -541,7 +554,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").?;
Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color);
@ -558,6 +573,21 @@ pub fn tick(self: *Game, input: Input) !void {
}
}
fn drawTile(self: *Game, tile_coord: Vec2, pos: Vec2) void {
Gfx.drawTexturedRectangle(
pos,
.init(1, 1),
Gfx.white,
self.assets.tilemap_view,
Rect.init(
tile_coord.x,
tile_coord.y,
1,
1
).multiply(self.assets.tile_size).divide(self.assets.tilemap_size)
);
}
pub fn debug(self: *Game) !void {
if (!imgui.beginWindow(.{
.name = "Debug",

File diff suppressed because it is too large Load Diff

View File

@ -1,118 +1,7 @@
const std = @import("std");
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();
}
}
const Engine = @import("./engine/root.zig");
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
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 },
});
var engine: Engine = undefined;
try engine.run(.{});
}

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);
}
}