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.linkLibrary(tracy_dependency.artifact("tracy"));
|
||||||
exe_mod.addImport("tracy", tracy_dependency.module("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", .{});
|
const sokol_c_dependency = b.dependency("sokol_c", .{});
|
||||||
exe_mod.addIncludePath(sokol_c_dependency.path("util"));
|
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", .{});
|
const tiled_dependency = b.dependency("tiled", .{});
|
||||||
exe_mod.addImport("tiled", tiled_dependency.module("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", .{
|
const sokol_dependency = b.dependency("sokol", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
@ -66,11 +66,6 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.flags = cflags.items
|
.flags = cflags.items
|
||||||
});
|
});
|
||||||
|
|
||||||
exe_mod.addCSourceFile(.{
|
|
||||||
.file = b.path("src/libs/stb_image.c"),
|
|
||||||
.flags = &.{}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (has_imgui) {
|
if (has_imgui) {
|
||||||
if (b.lazyDependency("cimgui", .{
|
if (b.lazyDependency("cimgui", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
|
|||||||
@ -17,10 +17,6 @@
|
|||||||
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
|
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
|
||||||
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
|
.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 = .{
|
.sokol_c = .{
|
||||||
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
|
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
|
||||||
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
|
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
|
||||||
@ -31,6 +27,9 @@
|
|||||||
},
|
},
|
||||||
.tiled = .{
|
.tiled = .{
|
||||||
.path = "./libs/tiled"
|
.path = "./libs/tiled"
|
||||||
|
},
|
||||||
|
.stb_image = .{
|
||||||
|
.path = "./libs/stb_image"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.paths = .{
|
.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 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 Assets = @import("./assets.zig");
|
||||||
const Vec2 = Math.Vec2;
|
|
||||||
|
|
||||||
const Entity = @This();
|
const Entity = @This();
|
||||||
|
|
||||||
@ -27,5 +28,5 @@ locked: bool = false,
|
|||||||
|
|
||||||
render_tile: ?union(enum) {
|
render_tile: ?union(enum) {
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
id: Gfx.TileId
|
id: Assets.TileId
|
||||||
} = null
|
} = null
|
||||||
|
|||||||
98
src/game.zig
98
src/game.zig
@ -2,28 +2,31 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
const Math = @import("./math.zig");
|
const Engine = @import("./engine/root.zig");
|
||||||
const Vec2 = Math.Vec2;
|
const imgui = Engine.imgui;
|
||||||
const Vec4 = Math.Vec4;
|
const Gfx = Engine.Graphics;
|
||||||
const rgb = Math.rgb;
|
const Vec2 = Engine.Math.Vec2;
|
||||||
const rgb_hex = Math.rgb_hex;
|
const Vec4 = Engine.Math.Vec4;
|
||||||
|
const Rect = Engine.Math.Rect;
|
||||||
|
const rgb = Engine.Math.rgb;
|
||||||
|
const rgb_hex = Engine.Math.rgb_hex;
|
||||||
|
|
||||||
const Timer = @import("./timer.zig");
|
const Timer = @import("./timer.zig");
|
||||||
const Window = @import("./window.zig");
|
|
||||||
const imgui = @import("./imgui.zig");
|
|
||||||
const Gfx = @import("./graphics.zig");
|
|
||||||
const Entity = @import("./entity.zig");
|
const Entity = @import("./entity.zig");
|
||||||
|
const Assets = @import("./assets.zig");
|
||||||
|
|
||||||
const tiled = @import("tiled");
|
const tiled = @import("tiled");
|
||||||
|
|
||||||
const Game = @This();
|
const Game = @This();
|
||||||
|
|
||||||
|
const KeyState = Engine.Input.KeyState;
|
||||||
|
|
||||||
pub const Input = struct {
|
pub const Input = struct {
|
||||||
dt: f64,
|
dt: f64,
|
||||||
move_up: Window.KeyState,
|
move_up: KeyState,
|
||||||
move_down: Window.KeyState,
|
move_down: KeyState,
|
||||||
move_left: Window.KeyState,
|
move_left: KeyState,
|
||||||
move_right: Window.KeyState,
|
move_right: KeyState,
|
||||||
restart: bool
|
restart: bool
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,6 +64,7 @@ gpa: Allocator,
|
|||||||
|
|
||||||
canvas_size: Vec2,
|
canvas_size: Vec2,
|
||||||
level: Level,
|
level: Level,
|
||||||
|
assets: *Assets,
|
||||||
|
|
||||||
current_level: u32,
|
current_level: u32,
|
||||||
levels: std.ArrayList(Level),
|
levels: std.ArrayList(Level),
|
||||||
@ -80,13 +84,14 @@ finale: bool = false,
|
|||||||
finale_timer: ?Timer.Id = null,
|
finale_timer: ?Timer.Id = null,
|
||||||
finale_counter: u32 = 0,
|
finale_counter: u32 = 0,
|
||||||
|
|
||||||
pub fn init(gpa: Allocator) !Game {
|
pub fn init(gpa: Allocator, assets: *Assets) !Game {
|
||||||
var self = Game{
|
var self = Game{
|
||||||
.gpa = gpa,
|
.gpa = gpa,
|
||||||
.canvas_size = (Vec2.init(20, 15)),
|
.canvas_size = (Vec2.init(20, 15)),
|
||||||
.level = .empty,
|
.level = .empty,
|
||||||
.levels = .empty,
|
.levels = .empty,
|
||||||
.current_level = 0,
|
.current_level = 0,
|
||||||
|
.assets = assets
|
||||||
};
|
};
|
||||||
errdefer self.deinit();
|
errdefer self.deinit();
|
||||||
|
|
||||||
@ -323,37 +328,35 @@ fn moveEntity(self: *Game, entity_id: Entity.Id, dir: Vec2) bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getInput(self: *Game, window: *Window) Input {
|
pub fn getInput(self: *Game, frame: Engine.Frame) Input {
|
||||||
_ = self; // autofix
|
_ = self; // autofix
|
||||||
const dt = @as(f32, @floatFromInt(window.frame_dt_ns)) / std.time.ns_per_s;
|
const input = frame.input;
|
||||||
|
|
||||||
return Input{
|
return Input{
|
||||||
.dt = dt,
|
.dt = frame.dt,
|
||||||
.move_up = window.getKeyState(.W),
|
.move_up = input.getKeyState(frame, .W),
|
||||||
.move_down = window.getKeyState(.S),
|
.move_down = input.getKeyState(frame, .S),
|
||||||
.move_left = window.getKeyState(.A),
|
.move_left = input.getKeyState(frame, .A),
|
||||||
.move_right = window.getKeyState(.D),
|
.move_right = input.getKeyState(frame, .D),
|
||||||
.restart = window.isKeyPressed(.R)
|
.restart = input.isKeyPressed(.R)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawEntity(self: *Game, entity: *Entity) void {
|
fn drawEntity(self: *Game, entity: *Entity) void {
|
||||||
_ = self; // autofix
|
|
||||||
if (entity.render_tile) |render_tile| {
|
if (entity.render_tile) |render_tile| {
|
||||||
var tile_coord = switch (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
|
.position => |position| position
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entity.type == .door) {
|
if (entity.type == .door) {
|
||||||
if (entity.locked) {
|
if (entity.locked) {
|
||||||
tile_coord = Gfx.getTileCoords(.locked_door);
|
tile_coord = self.assets.tile_coords.get(.locked_door);
|
||||||
} else {
|
} 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);
|
var move: Vec2 = .init(0, 0);
|
||||||
if (can_move) {
|
if (can_move) {
|
||||||
const repeat_options = Window.KeyState.RepeatOptions{
|
const repeat_options = KeyState.RepeatOptions{
|
||||||
.first_at = 0.3,
|
.first_at = 0.3,
|
||||||
.period = 0.1
|
.period = 0.1
|
||||||
};
|
};
|
||||||
@ -482,30 +485,40 @@ pub fn tickFinale(self: *Game) !void {
|
|||||||
text: []const u8,
|
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{
|
const lines = [5]Line{
|
||||||
.{
|
.{
|
||||||
.pos = Vec2.init(1, 1),
|
.pos = Vec2.init(1, 1),
|
||||||
.font = Gfx.Font.default,
|
.font = regular_font,
|
||||||
.text = "Congratulations scientist"
|
.text = "Congratulations scientist"
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.pos = Vec2.init(1, 2),
|
.pos = Vec2.init(1, 2),
|
||||||
.font = Gfx.Font.default,
|
.font = regular_font,
|
||||||
.text = "You have passed the entrance exam"
|
.text = "You have passed the entrance exam"
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.pos = Vec2.init(1, 3),
|
.pos = Vec2.init(1, 3),
|
||||||
.font = Gfx.Font.default,
|
.font = regular_font,
|
||||||
.text = "Here is your entry code"
|
.text = "Here is your entry code"
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.pos = Vec2.init(1, 5),
|
.pos = Vec2.init(1, 5),
|
||||||
.font = Gfx.Font.bold,
|
.font = bold_font,
|
||||||
.text = key
|
.text = key
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.pos = Vec2.init(1, 7),
|
.pos = Vec2.init(1, 7),
|
||||||
.font = Gfx.Font.default,
|
.font = regular_font,
|
||||||
.text = "I'll meet you at the lab"
|
.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").?;
|
const bg_color = rgb_hex("#222323").?;
|
||||||
|
|
||||||
Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color);
|
Gfx.drawRectangle(.init(0, 0), .init(self.canvas_size.x, self.canvas_size.y), bg_color);
|
||||||
@ -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 {
|
pub fn debug(self: *Game) !void {
|
||||||
if (!imgui.beginWindow(.{
|
if (!imgui.beginWindow(.{
|
||||||
.name = "Debug",
|
.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 Engine = @import("./engine/root.zig");
|
||||||
const tracy = @import("tracy");
|
|
||||||
const Gfx = @import("./graphics.zig");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
|
|
||||||
const Window = @import("./window.zig");
|
|
||||||
const Event = Window.Event;
|
|
||||||
const MouseButton = Window.MouseButton;
|
|
||||||
|
|
||||||
const Math = @import("./math.zig");
|
|
||||||
const Vec2 = Math.Vec2;
|
|
||||||
|
|
||||||
const sokol = @import("sokol");
|
|
||||||
const slog = sokol.log;
|
|
||||||
const sg = sokol.gfx;
|
|
||||||
const sapp = sokol.app;
|
|
||||||
const sglue = sokol.glue;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.main);
|
|
||||||
|
|
||||||
var gpa: std.mem.Allocator = undefined;
|
|
||||||
|
|
||||||
var window: Window = undefined;
|
|
||||||
var event_queue_full_shown = false;
|
|
||||||
|
|
||||||
fn signalHandler(sig: i32) callconv(.c) void {
|
|
||||||
_ = sig;
|
|
||||||
sapp.requestQuit();
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn init() void {
|
|
||||||
var zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
Window.init(&window, gpa) catch |e| {
|
|
||||||
log.err("init() failed: {}", .{e});
|
|
||||||
sapp.requestQuit();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn frame() void {
|
|
||||||
tracy.frameMark();
|
|
||||||
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
window.frame() catch |e| {
|
|
||||||
log.err("frame() failed: {}", .{e});
|
|
||||||
if (@errorReturnTrace()) |trace| {
|
|
||||||
std.debug.dumpStackTrace(trace.*);
|
|
||||||
}
|
|
||||||
sapp.requestQuit();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn cleanup() void {
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
window.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn event(e_ptr: [*c]const sapp.Event) void {
|
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
|
||||||
defer zone.deinit();
|
|
||||||
|
|
||||||
const consumed_event = window.event(e_ptr) catch |e| switch (e) {
|
|
||||||
error.EventQueueFull => blk: {
|
|
||||||
if (!event_queue_full_shown) {
|
|
||||||
log.warn("Event queue is full! Frame is taking too long to process", .{});
|
|
||||||
event_queue_full_shown = true;
|
|
||||||
}
|
|
||||||
break :blk false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (consumed_event) {
|
|
||||||
event_queue_full_shown = false;
|
|
||||||
sapp.consumeEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
var engine: Engine = undefined;
|
||||||
defer _ = debug_allocator.deinit();
|
try engine.run(.{});
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
|
||||||
gpa = debug_allocator.allocator();
|
|
||||||
} else {
|
|
||||||
gpa = std.heap.smp_allocator;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use tracy TracingAllocator
|
|
||||||
|
|
||||||
tracy.setThreadName("Main");
|
|
||||||
|
|
||||||
if (builtin.os.tag == .linux) {
|
|
||||||
var sa: std.posix.Sigaction = .{
|
|
||||||
.handler = .{ .handler = signalHandler },
|
|
||||||
.mask = std.posix.sigemptyset(),
|
|
||||||
.flags = std.posix.SA.RESTART,
|
|
||||||
};
|
|
||||||
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
sapp.run(.{
|
|
||||||
.init_cb = init,
|
|
||||||
.frame_cb = frame,
|
|
||||||
.cleanup_cb = cleanup,
|
|
||||||
.event_cb = event,
|
|
||||||
.width = 640,
|
|
||||||
.height = 480,
|
|
||||||
.icon = .{ .sokol_default = true },
|
|
||||||
.window_title = "Game",
|
|
||||||
.logger = .{ .func = Window.sokolLogCallback },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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