Compare commits
3 Commits
8b7c50375c
...
b73ea021f7
| Author | SHA1 | Date | |
|---|---|---|---|
| b73ea021f7 | |||
| 9f3c41f991 | |||
| 4df4f42022 |
11
build.zig
11
build.zig
@ -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,
|
||||
|
||||
@ -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
37
libs/stb_image/build.zig
Normal 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);
|
||||
}
|
||||
}
|
||||
17
libs/stb_image/build.zig.zon
Normal file
17
libs/stb_image/build.zig.zon
Normal 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",
|
||||
},
|
||||
}
|
||||
30
libs/stb_image/src/root.zig
Normal file
30
libs/stb_image/src/root.zig
Normal 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
66
src/assets.zig
Normal 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
614
src/engine/font.zig
Normal 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
460
src/engine/graphics.zig
Normal 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
239
src/engine/input.zig
Normal 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
445
src/engine/root.zig
Normal 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();
|
||||
}
|
||||
63
src/engine/screen_scaler.zig
Normal file
63
src/engine/screen_scaler.zig
Normal 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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
98
src/game.zig
98
src/game.zig
@ -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",
|
||||
|
||||
1135
src/graphics.zig
1135
src/graphics.zig
File diff suppressed because it is too large
Load Diff
117
src/main.zig
117
src/main.zig
@ -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(.{});
|
||||
}
|
||||
|
||||
|
||||
551
src/window.zig
551
src/window.zig
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user