update engine

This commit is contained in:
Rokas Puzonas 2026-02-07 14:57:47 +02:00
parent 3c0c6d64ca
commit 8434ea6a64
10 changed files with 986 additions and 509 deletions

View File

@ -60,5 +60,15 @@ pub const Data = union(enum) {
}; };
} }
pub fn getSampleRate(self: Data) u32 {
return switch (self) {
.raw => |opts| opts.sample_rate,
.vorbis => |opts| blk: {
const info = opts.stb_vorbis.getInfo();
break :blk info.sample_rate;
}
};
}
pub const Id = enum (u16) { _ }; pub const Id = enum (u16) { _ };
}; };

View File

@ -13,13 +13,17 @@ const Mixer = @This();
pub const Instance = struct { pub const Instance = struct {
data_id: AudioData.Id, data_id: AudioData.Id,
volume: f32 = 0,
cursor: u32 = 0, cursor: u32 = 0,
}; };
pub const Command = union(enum) { pub const Command = union(enum) {
play: struct { pub const Play = struct {
data_id: AudioData.Id, id: AudioData.Id,
}, volume: f32 = 1
};
play: Play,
pub const RingBuffer = struct { pub const RingBuffer = struct {
// TODO: This ring buffer will work in a single producer single consumer configuration // TODO: This ring buffer will work in a single producer single consumer configuration
@ -62,11 +66,13 @@ pub const Command = union(enum) {
instances: std.ArrayList(Instance), instances: std.ArrayList(Instance),
commands: Command.RingBuffer, commands: Command.RingBuffer,
working_buffer: []f32,
pub fn init( pub fn init(
gpa: Allocator, gpa: Allocator,
max_instances: u32, max_instances: u32,
max_commands: u32 max_commands: u32,
working_buffer_size: u32
) !Mixer { ) !Mixer {
var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances); var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances);
errdefer instances.deinit(gpa); errdefer instances.deinit(gpa);
@ -74,7 +80,11 @@ pub fn init(
const commands = try gpa.alloc(Command, max_commands); const commands = try gpa.alloc(Command, max_commands);
errdefer gpa.free(commands); errdefer gpa.free(commands);
const working_buffer = try gpa.alloc(f32, working_buffer_size);
errdefer gpa.free(working_buffer);
return Mixer{ return Mixer{
.working_buffer = working_buffer,
.instances = instances, .instances = instances,
.commands = .{ .commands = .{
.items = commands .items = commands
@ -85,6 +95,7 @@ pub fn init(
pub fn deinit(self: *Mixer, gpa: Allocator) void { pub fn deinit(self: *Mixer, gpa: Allocator) void {
self.instances.deinit(gpa); self.instances.deinit(gpa);
gpa.free(self.commands.items); gpa.free(self.commands.items);
gpa.free(self.working_buffer);
} }
pub fn queue(self: *Mixer, command: Command) void { pub fn queue(self: *Mixer, command: Command) void {
@ -95,8 +106,15 @@ pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_ch
while (self.commands.pop()) |command| { while (self.commands.pop()) |command| {
switch (command) { switch (command) {
.play => |opts| { .play => |opts| {
const volume = @max(opts.volume, 0);
if (volume == 0) {
log.warn("Attempt to play audio with 0 volume", .{});
continue;
}
self.instances.appendBounded(.{ self.instances.appendBounded(.{
.data_id = opts.data_id .data_id = opts.id,
.volume = volume,
}) catch log.warn("Maximum number of audio instances reached!", .{}); }) catch log.warn("Maximum number of audio instances reached!", .{});
} }
} }
@ -106,11 +124,14 @@ pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_ch
const sample_rate: u32 = @intCast(saudio.sampleRate()); const sample_rate: u32 = @intCast(saudio.sampleRate());
@memset(buffer, 0); @memset(buffer, 0);
assert(self.working_buffer.len >= num_frames);
// var written: u32 = 0;
for (self.instances.items) |*instance| { for (self.instances.items) |*instance| {
const audio_data = store.get(instance.data_id); const audio_data = store.get(instance.data_id);
const samples = audio_data.streamChannel(buffer[0..num_frames], instance.cursor, 0, sample_rate); const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate);
for (0.., samples) |i, sample| {
buffer[i] += sample * instance.volume;
}
instance.cursor += @intCast(samples.len); instance.cursor += @intCast(samples.len);
} }

View File

@ -11,6 +11,9 @@ pub const Data = @import("./data.zig").Data;
pub const Store = @import("./store.zig"); pub const Store = @import("./store.zig");
pub const Mixer = @import("./mixer.zig"); pub const Mixer = @import("./mixer.zig");
pub const Command = Mixer.Command;
const Nanoseconds = @import("../root.zig").Nanoseconds;
const sokol = @import("sokol"); const sokol = @import("sokol");
const saudio = sokol.audio; const saudio = sokol.audio;
@ -36,7 +39,11 @@ pub fn init(opts: Options) !void {
.max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size, .max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size,
}); });
mixer = try Mixer.init(gpa, opts.max_instances, opts.max_instances); mixer = try Mixer.init(gpa,
opts.max_instances,
opts.max_instances,
opts.buffer_frames
);
saudio.setup(.{ saudio.setup(.{
.logger = opts.logger, .logger = opts.logger,
@ -62,17 +69,21 @@ pub fn load(opts: Store.LoadOptions) !Data.Id {
return try store.load(opts); return try store.load(opts);
} }
pub const PlayOptions = struct { const Info = struct {
id: Data.Id, sample_count: u32,
delay: f32 = 0 sample_rate: u32,
pub fn getDuration(self: Info) Nanoseconds {
return @as(Nanoseconds, self.sample_count) * std.time.ns_per_s / self.sample_rate;
}
}; };
pub fn play(opts: PlayOptions) void { pub fn getInfo(id: Data.Id) Info {
mixer.queue(.{ const data = store.get(id);
.play = .{ return Info{
.data_id = opts.id .sample_count = data.getSampleCount(),
} .sample_rate = data.getSampleRate(),
}); };
} }
fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void { fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void {

291
src/engine/frame.zig Normal file
View File

@ -0,0 +1,291 @@
const std = @import("std");
const build_options = @import("build_options");
const log = std.log.scoped(.engine);
const InputSystem = @import("./input.zig");
const KeyCode = InputSystem.KeyCode;
const MouseButton = InputSystem.MouseButton;
const AudioSystem = @import("./audio/root.zig");
const AudioData = AudioSystem.Data;
const AudioCommand = AudioSystem.Command;
const GraphicsSystem = @import("./graphics.zig");
const TextureId = GraphicsSystem.TextureId;
const GraphicsCommand = GraphicsSystem.Command;
const Font = GraphicsSystem.Font;
const Sprite = GraphicsSystem.Sprite;
const Math = @import("./math.zig");
const Rect = Math.Rect;
const Vec4 = Math.Vec4;
const Vec2 = Math.Vec2;
const rgb = Math.rgb;
pub const Nanoseconds = u64;
const Frame = @This();
pub const Input = struct {
keyboard: InputSystem.ButtonStateSet(KeyCode),
mouse_button: InputSystem.ButtonStateSet(MouseButton),
mouse_position: ?Vec2,
pub const empty = Input{
.keyboard = .empty,
.mouse_button = .empty,
.mouse_position = null,
};
};
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 Audio = struct {
commands: std.ArrayList(AudioCommand),
pub const empty = Audio{
.commands = .empty
};
};
pub const Graphics = struct {
clear_color: Vec4,
screen_size: Vec2,
canvas_size: ?Vec2,
scissor_stack: std.ArrayList(Rect),
commands: std.ArrayList(GraphicsCommand),
pub const empty = Graphics{
.clear_color = rgb(0, 0, 0),
.screen_size = .init(0, 0),
.canvas_size = null,
.scissor_stack = .empty,
.commands = .empty
};
};
arena: std.heap.ArenaAllocator,
time_ns: Nanoseconds,
dt_ns: Nanoseconds,
input: Input,
audio: Audio,
graphics: Graphics,
show_debug: bool,
hide_cursor: bool,
pub fn init(self: *Frame, gpa: std.mem.Allocator) void {
self.* = Frame{
.arena = std.heap.ArenaAllocator.init(gpa),
.time_ns = 0,
.dt_ns = 0,
.input = .empty,
.audio = .empty,
.graphics = .empty,
.show_debug = false,
.hide_cursor = false
};
}
pub fn deinit(self: *Frame) void {
self.arena.deinit();
}
pub fn deltaTime(self: Frame) f32 {
return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s;
}
pub fn time(self: Frame) f64 {
return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s;
}
pub fn isKeyDown(self: Frame, key_code: KeyCode) bool {
const keyboard = &self.input.keyboard;
return keyboard.down.contains(key_code);
}
pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 {
if (!self.isKeyDown(key_code)) {
return null;
}
const keyboard = &self.input.keyboard;
const pressed_at_ns = keyboard.pressed_at.get(key_code).?;
const duration_ns = self.time_ns - pressed_at_ns;
return @as(f32, @floatFromInt(duration_ns)) / std.time.ns_per_s;
}
pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool {
const keyboard = &self.input.keyboard;
return keyboard.pressed.contains(key_code);
}
pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool {
const keyboard = &self.input.keyboard;
return keyboard.released.contains(key_code);
}
pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState {
return KeyState{
.down = self.isKeyDown(key_code),
.released = self.isKeyReleased(key_code),
.pressed = self.isKeyPressed(key_code),
.down_duration = self.getKeyDownDuration(key_code)
};
}
pub fn isMousePressed(self: Frame, button: MouseButton) bool {
return self.input.mouse_button.pressed.contains(button);
}
pub fn isMouseDown(self: Frame, button: MouseButton) bool {
return self.input.mouse_button.down.contains(button);
}
fn pushAudioCommand(self: *Frame, command: AudioCommand) void {
const arena = self.arena.allocator();
self.audio.commands.append(arena, command) catch |e| {
log.warn("Failed to play audio: {}", .{e});
};
}
pub fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
const arena = self.arena.allocator();
self.graphics.commands.append(arena, command) catch |e|{
log.warn("Failed to push graphics command: {}", .{e});
};
}
pub fn prependGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
const arena = self.arena.allocator();
self.graphics.commands.insert(arena, 0, command) catch |e|{
log.warn("Failed to push graphics command: {}", .{e});
};
}
pub fn playAudio(self: *Frame, options: AudioCommand.Play) void {
self.pushAudioCommand(.{
.play = options,
});
}
pub fn pushScissor(self: *Frame, rect: Rect) void {
const arena = self.arena.allocator();
self.graphics.scissor_stack.append(arena, rect) catch |e| {
log.warn("Failed to push scissor region: {}", .{e});
return;
};
self.pushGraphicsCommand(.{
.set_scissor = rect
});
}
pub fn popScissor(self: *Frame) void {
_ = self.graphics.scissor_stack.pop().?;
const rect = self.graphics.scissor_stack.getLast();
self.pushGraphicsCommand(.{
.set_scissor = rect
});
}
pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void {
self.pushGraphicsCommand(.{ .draw_rectangle = opts });
}
pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void {
self.pushGraphicsCommand(.{
.draw_line = .{
.pos1 = pos1,
.pos2 = pos2,
.width = width,
.color = color
}
});
}
pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
// TODO: Don't use line segments
self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
}
pub const DrawTextOptions = struct {
font: Font.Id,
size: f32 = 16,
color: Vec4 = rgb(255, 255, 255),
};
pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) void {
const arena = self.arena.allocator();
const text_dupe = arena.dupe(u8, text) catch |e| {
log.warn("Failed to draw text: {}", .{e});
return;
};
self.pushGraphicsCommand(.{
.draw_text = .{
.pos = position,
.text = text_dupe,
.size = opts.size,
.font = opts.font,
.color = opts.color,
}
});
}
pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void {
self.pushGraphicsCommand(.{
.push_transformation = .{
.translation = translation,
.scale = scale
}
});
}
pub fn popTransform(self: *Frame) void {
self.pushGraphicsCommand(.{
.pop_transformation = {}
});
}

View File

@ -21,6 +21,8 @@ const tracy = @import("tracy");
const fontstash = @import("./fontstash/root.zig"); const fontstash = @import("./fontstash/root.zig");
pub const Font = fontstash.Font; pub const Font = fontstash.Font;
const GraphicsFrame = @import("./frame.zig").Graphics;
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug. // 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. // From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
// Maybe a patch for sokol could be made? // Maybe a patch for sokol could be made?
@ -39,29 +41,75 @@ const Options = struct {
imgui_font: ?ImguiFont = null imgui_font: ?ImguiFont = null
}; };
const DrawFrame = struct { pub const Command = union(enum) {
screen_size: Vec2 = Vec2.zero, pub const DrawRectangle = struct {
bg_color: Vec4 = rgb(0, 0, 0), rect: Rect,
color: Vec4,
scissor_stack_buffer: [32]Rect = undefined, sprite: ?Sprite = null,
scissor_stack: std.ArrayListUnmanaged(Rect) = .empty, rotation: f32 = 0,
origin: Vec2 = .init(0, 0),
fn init(self: *DrawFrame) void {
self.* = DrawFrame{
.scissor_stack = .initBuffer(&self.scissor_stack_buffer)
}; };
}
set_scissor: Rect,
draw_rectangle: DrawRectangle,
draw_line: struct {
pos1: Vec2,
pos2: Vec2,
color: Vec4,
width: f32
},
draw_text: struct {
pos: Vec2,
text: []const u8,
font: Font.Id,
size: f32,
color: Vec4,
},
push_transformation: struct {
translation: Vec2,
scale: Vec2
},
pop_transformation: void
}; };
var draw_frame: DrawFrame = undefined; const Texture = struct {
image: sg.Image,
view: sg.View,
info: Info,
const Info = struct {
width: u32,
height: u32,
};
const Id = enum(u32) { _ };
const Data = struct {
width: u32,
height: u32,
rgba: [*]u8
};
};
pub const TextureId = Texture.Id;
pub const TextureInfo = Texture.Info;
pub const Sprite = struct {
texture: TextureId,
uv: Rect,
};
var gpa: std.mem.Allocator = undefined;
var main_pipeline: sgl.Pipeline = .{}; var main_pipeline: sgl.Pipeline = .{};
var linear_sampler: sg.Sampler = .{}; var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{}; var nearest_sampler: sg.Sampler = .{};
var font_context: fontstash.Context = undefined; var font_context: fontstash.Context = undefined;
pub var font_resolution_scale: f32 = 1; var textures: std.ArrayList(Texture) = .empty;
var scale_stack_buffer: [32]Vec2 = undefined;
var scale_stack: std.ArrayList(Vec2) = .empty;
pub fn init(options: Options) !void { pub fn init(options: Options) !void {
draw_frame.init(); gpa = options.allocator;
sg.setup(.{ sg.setup(.{
.logger = options.logger, .logger = options.logger,
@ -134,49 +182,110 @@ pub fn init(options: Options) !void {
} }
pub fn deinit() void { pub fn deinit() void {
textures.deinit(gpa);
imgui.shutdown(); imgui.shutdown();
font_context.deinit(); font_context.deinit();
sgl.shutdown(); sgl.shutdown();
sg.shutdown(); sg.shutdown();
} }
pub fn drawCommand(command: Command) void {
switch(command) {
.push_transformation => |opts| {
pushTransform(opts.translation, opts.scale);
// font_resolution_scale = font_resolution_scale.multiply(opts.scale);
},
.pop_transformation => {
popTransform();
},
.draw_rectangle => |opts| {
drawRectangle(opts);
},
.set_scissor => |opts| {
sgl.scissorRectf(
opts.pos.x,
opts.pos.y,
opts.size.x,
opts.size.y,
true
);
},
.draw_line => |opts| {
drawLine(
opts.pos1,
opts.pos2,
opts.color,
opts.width
);
},
.draw_text => |opts| {
const font_resolution_scale = scale_stack.getLast();
sgl.pushMatrix();
defer sgl.popMatrix();
sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1);
font_context.setFont(opts.font);
font_context.setSize(opts.size * font_resolution_scale.y);
font_context.setAlign(.{ .x = .left, .y = .top });
font_context.setSpacing(0);
const r: u8 = @intFromFloat(opts.color.x * 255);
const g: u8 = @intFromFloat(opts.color.y * 255);
const b: u8 = @intFromFloat(opts.color.z * 255);
const a: u8 = @intFromFloat(opts.color.w * 255);
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
font_context.setColor(color);
font_context.drawText(
opts.pos.x * font_resolution_scale.x,
opts.pos.y * font_resolution_scale.y,
opts.text
);
}
}
}
pub fn drawCommands(commands: []const Command) void {
for (commands) |command| {
drawCommand(command);
}
}
pub fn beginFrame() void { pub fn beginFrame() void {
const zone = tracy.initZone(@src(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); 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(.{ imgui.newFrame(.{
.width = @intFromFloat(draw_frame.screen_size.x), .width = sapp.width(),
.height = @intFromFloat(draw_frame.screen_size.y), .height = sapp.height(),
.delta_time = sapp.frameDuration(), .delta_time = sapp.frameDuration(),
.dpi_scale = sapp.dpiScale() .dpi_scale = sapp.dpiScale()
}); });
scale_stack = .initBuffer(&scale_stack_buffer);
scale_stack.appendAssumeCapacity(.init(1, 1));
font_context.clearState(); font_context.clearState();
sgl.defaults(); sgl.defaults();
sgl.matrixModeProjection(); sgl.matrixModeProjection();
sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1); sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
sgl.loadPipeline(main_pipeline); sgl.loadPipeline(main_pipeline);
} }
pub fn endFrame() void { pub fn endFrame(clear_color: Vec4) void {
const zone = tracy.initZone(@src(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); defer zone.deinit();
assert(draw_frame.scissor_stack.items.len == 1);
var pass_action: sg.PassAction = .{}; var pass_action: sg.PassAction = .{};
pass_action.colors[0] = sg.ColorAttachmentAction{ pass_action.colors[0] = sg.ColorAttachmentAction{
.load_action = .CLEAR, .load_action = .CLEAR,
.clear_value = .{ .clear_value = .{
.r = draw_frame.bg_color.x, .r = clear_color.x,
.g = draw_frame.bg_color.y, .g = clear_color.y,
.b = draw_frame.bg_color.z, .b = clear_color.z,
.a = draw_frame.bg_color.w .a = clear_color.w
} }
}; };
@ -196,53 +305,95 @@ pub fn endFrame() void {
sg.commit(); sg.commit();
} }
pub fn pushTransform(translation: Vec2, scale: f32) void { fn pushTransform(translation: Vec2, scale: Vec2) void {
sgl.pushMatrix(); sgl.pushMatrix();
sgl.translate(translation.x, translation.y, 0); sgl.translate(translation.x, translation.y, 0);
sgl.scale(scale, scale, 1); sgl.scale(scale.x, scale.y, 1);
scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale));
} }
pub fn popTransform() void { fn popTransform() void {
sgl.popMatrix(); sgl.popMatrix();
_ = scale_stack.pop().?;
} }
pub fn drawQuad(quad: [4]Vec2, color: Vec4) void { const Vertex = struct {
pos: Vec2,
uv: Vec2
};
fn drawQuad(quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
sgl.enableTexture();
defer sgl.disableTexture();
const view = textures.items[@intFromEnum(texture_id)].view;
// TODO: Make sampler configurable
sgl.texture(view, nearest_sampler);
sgl.beginQuads(); sgl.beginQuads();
defer sgl.end(); defer sgl.end();
sgl.c4f(color.x, color.y, color.z, color.w); sgl.c4f(color.x, color.y, color.z, color.w);
for (quad) |position| { for (quad) |vertex| {
sgl.v2f(position.x, position.y); const pos = vertex.pos;
const uv = vertex.uv;
sgl.v2fT2f(pos.x, pos.y, uv.x, uv.y);
} }
} }
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void { fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void {
drawQuad( sgl.beginQuads();
.{
// 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 drawTriangle(tri: [3]Vec2, color: Vec4) void {
sgl.beginTriangles();
defer sgl.end(); defer sgl.end();
sgl.c4f(color.x, color.y, color.z, color.w); sgl.c4f(color.x, color.y, color.z, color.w);
for (tri) |position| { for (quad) |pos| {
sgl.v2f(position.x, position.y); sgl.v2f(pos.x, pos.y);
} }
} }
pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { fn drawRectangle(opts: Command.DrawRectangle) void {
const pos = opts.rect.pos;
const size = opts.rect.size;
const top_left = Vec2.init(0, 0).rotateAround(opts.rotation, opts.origin);
const top_right = Vec2.init(size.x, 0).rotateAround(opts.rotation, opts.origin);
const bottom_right = size.rotateAround(opts.rotation, opts.origin);
const bottom_left = Vec2.init(0, size.y).rotateAround(opts.rotation, opts.origin);
if (opts.sprite) |sprite| {
const uv = sprite.uv;
const quad = [4]Vertex{
.{
.pos = pos.add(top_left),
.uv = .init(uv.left(), uv.top())
},
.{
.pos = pos.add(top_right),
.uv = .init(uv.right(), uv.top())
},
.{
.pos = pos.add(bottom_right),
.uv = .init(uv.right(), uv.bottom())
},
.{
.pos = pos.add(bottom_left),
.uv = .init(uv.left(), uv.bottom())
}
};
drawQuad(quad, opts.color, sprite.texture);
} else {
const quad = .{
pos.add(top_left),
pos.add(top_right),
pos.add(bottom_right),
pos.add(bottom_left)
};
drawQuadNoUVs(quad, opts.color);
}
}
fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const step = to.sub(from).normalized().multiplyScalar(width/2); const step = to.sub(from).normalized().multiplyScalar(width/2);
const top_left = from.add(step.rotateLeft90()); const top_left = from.add(step.rotateLeft90());
@ -250,61 +401,80 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const top_right = to.add(step.rotateLeft90()); const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90()); const bottom_right = to.add(step.rotateRight90());
drawQuad( drawQuadNoUVs(
.{ top_right, top_left, bottom_left, bottom_right }, .{ top_right, top_left, bottom_left, bottom_right },
color color
); );
} }
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 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 addFont(name: [*c]const u8, data: []const u8) !Font.Id { pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
return try font_context.addFont(name, data); return try font_context.addFont(name, data);
} }
pub const DrawTextOptions = struct { fn makeView(image: sg.Image) !sg.View {
font: Font.Id, const image_view = sg.makeView(.{
size: f32 = 16, .texture = .{ .image = image }
color: Vec4 = rgb(255, 255, 255), });
}; if (image_view.id == sg.invalid_id) {
return error.InvalidView;
pub fn drawText(position: Vec2, text: []const u8, opts: DrawTextOptions) void { }
pushTransform(.{ .x = 0, .y = 0}, 1/font_resolution_scale); return image_view;
defer popTransform(); }
font_context.setFont(opts.font); fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
font_context.setSize(opts.size * font_resolution_scale); if (mipmaps.len == 0) {
font_context.setAlign(.{ .x = .left, .y = .top }); return error.NoMipMaps;
font_context.setSpacing(0); }
const r: u8 = @intFromFloat(opts.color.x * 255); var data: sg.ImageData = .{};
const g: u8 = @intFromFloat(opts.color.y * 255); var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
const b: u8 = @intFromFloat(opts.color.z * 255);
const a: u8 = @intFromFloat(opts.color.w * 255); for (mipmaps) |mipmap| {
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24); try mip_levels.appendBounded(.{
font_context.setColor(color); .ptr = mipmap.rgba,
font_context.drawText( .size = mipmap.width * mipmap.height * 4
position.x * font_resolution_scale, });
position.y * font_resolution_scale, }
text
); const image = sg.makeImage(.{
.width = @intCast(mipmaps[0].width),
.height = @intCast(mipmaps[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 addTexture(mipmaps: []const Texture.Data) !TextureId {
const image = try makeImageWithMipMaps(mipmaps);
errdefer sg.deallocImage(image);
const view = try makeView(image);
errdefer sg.deallocView(view);
assert(mipmaps.len > 0);
const index = textures.items.len;
try textures.append(gpa, .{
.image = image,
.view = view,
.info = .{
.width = mipmaps[0].width,
.height = mipmaps[0].height,
}
});
return @enumFromInt(index);
}
pub fn getTextureInfo(id: TextureId) TextureInfo {
const texture = textures.items[@intFromEnum(id)];
return texture.info;
} }

View File

@ -1,239 +1,124 @@
const std = @import("std"); const std = @import("std");
const sokol = @import("sokol"); const sokol = @import("sokol");
const Engine = @import("./root.zig"); const Frame = @import("./frame.zig");
const Nanoseconds = Engine.Nanoseconds; const Nanoseconds = Frame.Nanoseconds;
const Math = @import("./math.zig"); const Math = @import("./math.zig");
const Vec2 = Math.Vec2; const Vec2 = Math.Vec2;
const Input = @This(); const Input = @This();
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode);
SPACE = 32, pub const KeyCode = @Type(.{
APOSTROPHE = 39, .@"enum" = .{
COMMA = 44, .tag_type = std.math.IntFittingRange(0, sokol.app.max_keycodes-1),
MINUS = 45, .decls = SokolKeyCodeInfo.@"enum".decls,
PERIOD = 46, .fields = SokolKeyCodeInfo.@"enum".fields,
SLASH = 47, .is_exhaustive = false
_0 = 48, }
_1 = 49, });
_2 = 50,
_3 = 51, pub const MouseButton = enum(i3) {
_4 = 52, left = @intFromEnum(sokol.app.Mousebutton.LEFT),
_5 = 53, right = @intFromEnum(sokol.app.Mousebutton.RIGHT),
_6 = 54, middle = @intFromEnum(sokol.app.Mousebutton.MIDDLE),
_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 { pub fn ButtonStateSet(E: type) type {
down: bool, return struct {
pressed: bool, const Self = @This();
released: bool,
down_duration: ?f64,
pub const RepeatOptions = struct { down: std.EnumSet(E),
first_at: f64 = 0, pressed: std.EnumSet(E),
period: f64 released: std.EnumSet(E),
pressed_at: std.EnumMap(E, Nanoseconds),
pub const empty = Self{
.down = .initEmpty(),
.pressed = .initEmpty(),
.released = .initEmpty(),
.pressed_at = .init(.{}),
}; };
pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool { fn press(self: *Self, button: E, now: Nanoseconds) void {
if (!self.down) { self.pressed_at.put(button, now);
last_repeat_at.* = null; self.pressed.insert(button);
return false; self.down.insert(button);
} }
const down_duration = self.down_duration.?; fn release(self: *Self, button: E) void {
if (last_repeat_at.* != null) { self.down.remove(button);
if (down_duration >= last_repeat_at.*.? + opts.period) { self.released.insert(button);
last_repeat_at.* = last_repeat_at.*.? + opts.period; self.pressed_at.remove(button);
return true;
}
} else {
if (down_duration >= opts.first_at) {
last_repeat_at.* = opts.first_at;
return true;
}
} }
return false; fn releaseAll(self: *Self) void {
var iter = self.down.iterator();
while (iter.next()) |key_code| {
self.released.insert(key_code);
} }
self.down = .initEmpty();
self.pressed_at = .init(.{});
}
};
}
pub const 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: Input.KeyCode,
window_resize,
char: u21,
}; };
pub const Mouse = struct { pub fn processEvent(frame: *Frame, event: Event) void {
pub const Button = enum { const input = &frame.input;
left,
right,
middle,
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?Button { switch (event) {
return switch(mouse_button) { .key_pressed => |opts| {
.LEFT => Button.left, if (!opts.repeat) {
.RIGHT => Button.right, input.keyboard.press(opts.code, frame.time_ns);
.MIDDLE => Button.middle,
else => null
};
} }
}; },
.key_released => |key_code| {
input.keyboard.release(key_code);
},
.mouse_leave => {
input.keyboard.releaseAll();
position: ?Vec2, input.mouse_position = null;
buttons: std.EnumSet(Button), input.mouse_button = .empty;
},
pub const empty = Mouse{ .mouse_enter => |pos| {
.position = null, input.mouse_position = pos;
.buttons = .initEmpty() },
}; .mouse_move => |pos| {
}; input.mouse_position = pos;
},
down_keys: std.EnumSet(KeyCode), .mouse_pressed => |opts| {
pressed_keys: std.EnumSet(KeyCode), input.mouse_position = opts.position;
released_keys: std.EnumSet(KeyCode), input.mouse_button.press(opts.button, frame.time_ns);
pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds), },
.mouse_released => |opts| {
mouse: Mouse, input.mouse_position = opts.position;
input.mouse_button.release(opts.button);
pub const empty = Input{ },
.down_keys = .initEmpty(), else => {}
.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)
};
} }

View File

@ -9,6 +9,28 @@ pub const bytes_per_kb = 1000;
pub const bytes_per_mb = bytes_per_kb * 1000; pub const bytes_per_mb = bytes_per_kb * 1000;
pub const bytes_per_gb = bytes_per_mb * 1000; pub const bytes_per_gb = bytes_per_mb * 1000;
pub const Range = struct {
from: f32,
to: f32,
pub const zero = init(0, 0);
pub fn init(from: f32, to: f32) Range {
return Range{
.from = from,
.to = to
};
}
pub fn getSize(self: Range) f32 {
return @abs(self.from - self.to);
}
pub fn random(self: Range, rng: std.Random) f32 {
return self.from + rng.float(f32) * (self.to - self.from);
}
};
pub const Vec2 = extern struct { pub const Vec2 = extern struct {
x: f32, x: f32,
y: f32, y: f32,
@ -22,6 +44,10 @@ pub const Vec2 = extern struct {
}; };
} }
pub fn initFromInt(T: type, x: T, y: T) Vec2 {
return .init(@floatFromInt(x), @floatFromInt(y));
}
pub fn initAngle(angle: f32) Vec2 { pub fn initAngle(angle: f32) Vec2 {
return Vec2{ return Vec2{
.x = @cos(angle), .x = @cos(angle),
@ -37,6 +63,21 @@ pub const Vec2 = extern struct {
return Vec2.init(-self.y, self.x); return Vec2.init(-self.y, self.x);
} }
pub fn rotate(self: Vec2, angle: f32) Vec2 {
return init(
@cos(angle) * self.x - @sin(angle) * self.y,
@sin(angle) * self.x + @cos(angle) * self.y,
);
}
pub fn rotateAround(self: Vec2, angle: f32, origin: Vec2) Vec2 {
return self.sub(origin).rotate(angle).add(origin);
}
pub fn getAngle(self: Vec2) f32 {
return std.math.atan2(self.y, self.x);
}
pub fn flip(self: Vec2) Vec2 { pub fn flip(self: Vec2) Vec2 {
return Vec2.init(-self.x, -self.y); return Vec2.init(-self.x, -self.y);
} }
@ -83,18 +124,29 @@ pub const Vec2 = extern struct {
); );
} }
pub fn lengthSqr(self: Vec2) f32 {
return self.x*self.x + self.y*self.y;
}
pub fn length(self: Vec2) f32 { pub fn length(self: Vec2) f32 {
return @sqrt(self.x*self.x + self.y*self.y); return @sqrt(self.lengthSqr());
} }
pub fn distance(self: Vec2, other: Vec2) f32 { pub fn distance(self: Vec2, other: Vec2) f32 {
return self.sub(other).length(); return self.sub(other).length();
} }
pub fn distanceSqr(self: Vec2, other: Vec2) f32 {
return self.sub(other).lengthSqr();
}
pub fn limitLength(self: Vec2, max_length: f32) Vec2 { pub fn limitLength(self: Vec2, max_length: f32) Vec2 {
const self_length = self.length(); const self_length = self.length();
if (self_length > max_length) { if (self_length > max_length) {
return Vec2.init(self.x / self_length * max_length, self.y / self_length * max_length); if (self_length == 0) {
return Vec2.init(0, 0);
}
return self.divideScalar(self_length / max_length);
} else { } else {
return self; return self;
} }
@ -105,7 +157,7 @@ pub const Vec2 = extern struct {
if (self_length == 0) { if (self_length == 0) {
return Vec2.init(0, 0); return Vec2.init(0, 0);
} }
return Vec2.init(self.x / self_length, self.y / self_length); return self.divideScalar(self_length);
} }
pub fn initScalar(value: f32) Vec2 { pub fn initScalar(value: f32) Vec2 {
@ -351,6 +403,10 @@ pub const Rect = struct {
return self.pos.y + self.size.y; return self.pos.y + self.size.y;
} }
pub fn center(self: Rect) Vec2 {
return self.pos.add(self.size.multiplyScalar(0.5));
}
pub fn multiply(self: Rect, xy: Vec2) Rect { pub fn multiply(self: Rect, xy: Vec2) Rect {
return Rect{ return Rect{
.pos = self.pos.multiply(xy), .pos = self.pos.multiply(xy),

View File

@ -7,9 +7,12 @@ const sapp = sokol.app;
pub const Math = @import("./math.zig"); pub const Math = @import("./math.zig");
pub const Vec2 = Math.Vec2; pub const Vec2 = Math.Vec2;
const rgb = Math.rgb;
pub const Input = @import("./input.zig"); pub const Input = @import("./input.zig");
pub const Frame = @import("./frame.zig");
const ScreenScalar = @import("./screen_scaler.zig"); const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig"); pub const imgui = @import("./imgui.zig");
pub const Graphics = @import("./graphics.zig"); pub const Graphics = @import("./graphics.zig");
@ -27,43 +30,16 @@ const Engine = @This();
pub const Nanoseconds = u64; 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, allocator: std.mem.Allocator,
started_at: std.time.Instant, started_at: std.time.Instant,
mouse_inside: bool, mouse_inside: bool,
last_frame_at: Nanoseconds, last_frame_at: Nanoseconds,
input: Input,
game: Game, game: Game,
assets: Assets, assets: Assets,
frame: Frame,
canvas_size: ?Vec2 = null,
const RunOptions = struct { const RunOptions = struct {
window_title: [*:0]const u8 = "Game", window_title: [*:0]const u8 = "Game",
@ -75,11 +51,11 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
self.* = Engine{ self.* = Engine{
.allocator = undefined, .allocator = undefined,
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.input = .empty,
.mouse_inside = false, .mouse_inside = false,
.last_frame_at = 0, .last_frame_at = 0,
.assets = undefined, .assets = undefined,
.game = undefined .game = undefined,
.frame = undefined
}; };
var debug_allocator: std.heap.DebugAllocator(.{}) = .init; var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
@ -94,6 +70,8 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
self.allocator = std.heap.smp_allocator; self.allocator = std.heap.smp_allocator;
} }
self.frame.init(self.allocator);
tracy.setThreadName("Main"); tracy.setThreadName("Main");
if (builtin.os.tag == .linux) { if (builtin.os.tag == .linux) {
@ -154,14 +132,17 @@ fn sokolInit(self: *Engine) !void {
.logger = .{ .func = sokolLogCallback }, .logger = .{ .func = sokolLogCallback },
}); });
const seed: u64 = @bitCast(std.time.milliTimestamp());
self.assets = try Assets.init(self.allocator); self.assets = try Assets.init(self.allocator);
self.game = try Game.init(self.allocator, &self.assets); self.game = try Game.init(self.allocator, seed, &self.assets);
} }
fn sokolCleanup(self: *Engine) void { fn sokolCleanup(self: *Engine) void {
const zone = tracy.initZone(@src(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); defer zone.deinit();
self.frame.deinit();
Audio.deinit(); Audio.deinit();
self.game.deinit(); self.game.deinit();
self.assets.deinit(self.allocator); self.assets.deinit(self.allocator);
@ -171,84 +152,111 @@ fn sokolCleanup(self: *Engine) void {
fn sokolFrame(self: *Engine) !void { fn sokolFrame(self: *Engine) !void {
tracy.frameMark(); 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(), .{ }); const zone = tracy.initZone(@src(), .{ });
defer zone.deinit(); defer zone.deinit();
Gfx.beginFrame(); const now = std.time.Instant.now() catch @panic("Instant.now() unsupported");
defer Gfx.endFrame(); const time_passed = now.since(self.started_at);
defer self.last_frame_at = time_passed;
const frame = &self.frame;
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
var revert_mouse_position: ?Vec2 = null;
if (self.canvas_size) |canvas_size| {
if (self.frame.input.mouse_position) |mouse| {
const transform = ScreenScalar.init(screen_size, canvas_size);
revert_mouse_position = mouse;
self.frame.input.mouse_position = mouse.sub(transform.translation).divideScalar(transform.scale);
}
}
{ {
const window_size: Vec2 = .init(sapp.widthf(), sapp.heightf()); _ = frame.arena.reset(.retain_capacity);
const ctx = ScreenScalar.push(window_size, self.game.canvas_size); const arena = frame.arena.allocator();
defer ctx.pop();
Graphics.font_resolution_scale = ctx.scale; const audio_commands_capacity = frame.audio.commands.capacity;
frame.audio = .empty;
try frame.audio.commands.ensureTotalCapacity(arena, audio_commands_capacity);
try self.game.tick(Frame{ const graphics_commands_capacity = frame.graphics.commands.capacity;
.time_ns = time_passed, const scissor_stack_capacity = frame.graphics.scissor_stack.capacity;
.dt_ns = dt_ns, frame.graphics = .empty;
.dt = @as(f32, @floatFromInt(dt_ns)) / std.time.ns_per_s, frame.graphics.screen_size = screen_size;
.input = &self.input try frame.graphics.commands.ensureTotalCapacity(arena, graphics_commands_capacity);
}); try frame.graphics.scissor_stack.ensureTotalCapacity(arena, scissor_stack_capacity);
frame.pushScissor(.init(0, 0, sapp.widthf(), sapp.heightf()));
frame.time_ns = time_passed;
frame.dt_ns = time_passed - self.last_frame_at;
frame.hide_cursor = false;
try self.game.tick(&self.frame);
frame.input.keyboard.pressed = .initEmpty();
frame.input.keyboard.released = .initEmpty();
frame.input.mouse_button.pressed = .initEmpty();
frame.input.mouse_button.released = .initEmpty();
} }
if (self.canvas_size) |canvas_size| {
const transform = ScreenScalar.init(
screen_size,
canvas_size
);
transform.apply(
screen_size,
&self.frame,
rgb(0, 0, 0)
);
}
sapp.showMouse(!self.frame.hide_cursor);
// Canvas size modification must always be applied a frame later.
// So that mouse coordinate transformations are consistent.
self.canvas_size = self.frame.graphics.canvas_size;
{
Gfx.beginFrame();
defer Gfx.endFrame(frame.graphics.clear_color);
Gfx.drawCommands(frame.graphics.commands.items);
if (frame.show_debug) {
try self.game.debug(); try self.game.debug();
try showDebugWindow(&self.frame);
}
}
self.input.pressed_keys = .initEmpty(); for (frame.audio.commands.items) |command| {
self.input.released_keys = .initEmpty(); try Audio.mixer.commands.push(command);
}
if (revert_mouse_position) |pos| {
self.frame.input.mouse_position = pos;
}
} }
fn timePassed(self: *Engine) Nanoseconds { fn showDebugWindow(frame: *Frame) !void {
const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); if (!imgui.beginWindow(.{
return now.since(self.started_at); .name = "Engine",
} .pos = Vec2.init(240, 20),
.size = Vec2.init(200, 200),
fn event(self: *Engine, e: Event) !void { })) {
const input = &self.input; return;
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);
} }
}, defer imgui.endWindow();
.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; imgui.textFmt("Draw commands: {}\n", .{
}, frame.graphics.commands.items.len,
.mouse_enter => |pos| { });
input.mouse.position = pos; imgui.textFmt("Audio instances: {}/{}\n", .{
}, Audio.mixer.instances.items.len,
.mouse_move => |pos| { Audio.mixer.instances.capacity
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 { fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
@ -256,11 +264,10 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
defer zone.deinit(); defer zone.deinit();
const e = e_ptr.*; const e = e_ptr.*;
const MouseButton = Input.Mouse.Button;
if (imgui.handleEvent(e)) { if (imgui.handleEvent(e)) {
if (self.mouse_inside) { if (self.mouse_inside) {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_leave = {} .mouse_leave = {}
}); });
} }
@ -268,13 +275,11 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
return true; return true;
} }
blk: switch (e.type) { switch (e.type) {
.MOUSE_DOWN => { .MOUSE_DOWN => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; Input.processEvent(&self.frame, .{
try self.event(Event{
.mouse_pressed = .{ .mouse_pressed = .{
.button = mouse_button, .button = @enumFromInt(@intFromEnum(e.mouse_button)),
.position = Vec2.init(e.mouse_x, e.mouse_y) .position = Vec2.init(e.mouse_x, e.mouse_y)
} }
}); });
@ -282,11 +287,9 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
return true; return true;
}, },
.MOUSE_UP => { .MOUSE_UP => {
const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; Input.processEvent(&self.frame, .{
try self.event(Event{
.mouse_released = .{ .mouse_released = .{
.button = mouse_button, .button = @enumFromInt(@intFromEnum(e.mouse_button)),
.position = Vec2.init(e.mouse_x, e.mouse_y) .position = Vec2.init(e.mouse_x, e.mouse_y)
} }
}); });
@ -295,11 +298,11 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
}, },
.MOUSE_MOVE => { .MOUSE_MOVE => {
if (!self.mouse_inside) { if (!self.mouse_inside) {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
}); });
} else { } else {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_move = Vec2.init(e.mouse_x, e.mouse_y) .mouse_move = Vec2.init(e.mouse_x, e.mouse_y)
}); });
} }
@ -309,7 +312,7 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
}, },
.MOUSE_ENTER => { .MOUSE_ENTER => {
if (!self.mouse_inside) { if (!self.mouse_inside) {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y)
}); });
} }
@ -319,12 +322,12 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
}, },
.RESIZED => { .RESIZED => {
if (self.mouse_inside) { if (self.mouse_inside) {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_leave = {} .mouse_leave = {}
}); });
} }
try self.event(Event{ Input.processEvent(&self.frame, .{
.window_resize = {} .window_resize = {}
}); });
@ -333,7 +336,7 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
}, },
.MOUSE_LEAVE => { .MOUSE_LEAVE => {
if (self.mouse_inside) { if (self.mouse_inside) {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_leave = {} .mouse_leave = {}
}); });
} }
@ -342,14 +345,14 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
return true; return true;
}, },
.MOUSE_SCROLL => { .MOUSE_SCROLL => {
try self.event(Event{ Input.processEvent(&self.frame, .{
.mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y)
}); });
return true; return true;
}, },
.KEY_DOWN => { .KEY_DOWN => {
try self.event(Event{ Input.processEvent(&self.frame, .{
.key_pressed = .{ .key_pressed = .{
.code = @enumFromInt(@intFromEnum(e.key_code)), .code = @enumFromInt(@intFromEnum(e.key_code)),
.repeat = e.key_repeat .repeat = e.key_repeat
@ -359,14 +362,14 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {
return true; return true;
}, },
.KEY_UP => { .KEY_UP => {
try self.event(Event{ Input.processEvent(&self.frame, .{
.key_released = @enumFromInt(@intFromEnum(e.key_code)) .key_released = @enumFromInt(@intFromEnum(e.key_code))
}); });
return true; return true;
}, },
.CHAR => { .CHAR => {
try self.event(Event{ Input.processEvent(&self.frame, .{
.char = @intCast(e.char_code) .char = @intCast(e.char_code)
}); });

View File

@ -2,19 +2,21 @@ const Gfx = @import("./graphics.zig");
const Math = @import("./math.zig"); const Math = @import("./math.zig");
const Vec2 = Math.Vec2; const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb; const rgb = Math.rgb;
const Frame = @import("./frame.zig");
const ScreenScalar = @This(); const ScreenScalar = @This();
// TODO: Implement a fractional pixel perfect scalar // TODO: Implement a fractional pixel perfect scalar
// Based on this video: https://www.youtube.com/watch?v=d6tp43wZqps // Based on this video: https://www.youtube.com/watch?v=d6tp43wZqps
// And this blog: https://colececil.dev/blog/2017/scaling-pixel-art-without-destroying-it/ // And this blog: https://colececil.dev/blog/2017/scaling-pixel-art-without-destroying-it/
window_size: Vec2,
translation: Vec2, translation: Vec2,
scale: f32, scale: f32,
pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar { pub fn init(window_size: Vec2, canvas_size: Vec2) ScreenScalar {
// TODO: Render to a lower resolution instead of scaling. // TODO: Render to a lower resolution instead of scaling.
// To avoid pixel bleeding in spritesheet artifacts // To avoid pixel bleeding in spritesheet artifacts
const scale = @floor(@min( const scale = @floor(@min(
@ -26,42 +28,53 @@ pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar {
translation.x = @round(translation.x); translation.x = @round(translation.x);
translation.y = @round(translation.y); translation.y = @round(translation.y);
Gfx.pushTransform(translation, scale);
return ScreenScalar{ return ScreenScalar{
.window_size = window_size,
.translation = translation, .translation = translation,
.scale = scale .scale = scale
}; };
} }
pub fn pop(self: ScreenScalar) void { pub fn apply(self: ScreenScalar, window_size: Vec2, frame: *Frame, color: Vec4) void {
Gfx.popTransform(); const scale = self.scale;
const translation = self.translation;
const bg_color = rgb(0, 0, 0); frame.prependGraphicsCommand(.{
const filler_size = self.translation; .push_transformation = .{
.translation = translation,
.scale = .init(scale, scale)
}
});
frame.popTransform();
Gfx.drawRectangle( frame.drawRectangle(.{
.init(0, 0), .rect = .{
.init(self.window_size.x, filler_size.y), .pos = .init(0, 0),
bg_color .size = .init(window_size.x, translation.y),
); },
.color = color
});
Gfx.drawRectangle( frame.drawRectangle(.{
.init(0, self.window_size.y - filler_size.y), .rect = .{
.init(self.window_size.x, filler_size.y), .pos = .init(0, window_size.y - translation.y),
bg_color .size = .init(window_size.x, translation.y),
); },
.color = color
});
Gfx.drawRectangle( frame.drawRectangle(.{
.init(0, 0), .rect = .{
.init(filler_size.x, self.window_size.y), .pos = .init(0, 0),
bg_color .size = .init(translation.x, window_size.y),
); },
.color = color
});
Gfx.drawRectangle( frame.drawRectangle(.{
.init(self.window_size.x - filler_size.x, 0), .rect = .{
.init(filler_size.x, self.window_size.y), .pos = .init(window_size.x - translation.x, 0),
bg_color .size = .init(translation.x, window_size.y),
); },
.color = color
});
} }

View File

@ -15,15 +15,14 @@ const Game = @This();
gpa: Allocator, gpa: Allocator,
assets: *Assets, assets: *Assets,
canvas_size: Vec2,
player: Vec2, player: Vec2,
pub fn init(gpa: Allocator, assets: *Assets) !Game { pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
_ = seed; // autofix
return Game{ return Game{
.gpa = gpa, .gpa = gpa,
.assets = assets, .assets = assets,
.canvas_size = Vec2.init(100, 100),
.player = .init(50, 50) .player = .init(50, 50)
}; };
} }
@ -32,41 +31,59 @@ pub fn deinit(self: *Game) void {
_ = self; // autofix _ = self; // autofix
} }
pub fn tick(self: *Game, frame: Engine.Frame) !void { pub fn tick(self: *Game, frame: *Engine.Frame) !void {
const dt = frame.deltaTime();
const canvas_size = Vec2.init(100, 100);
frame.graphics.canvas_size = canvas_size;
if (frame.isKeyPressed(.F3)) {
frame.show_debug = !frame.show_debug;
}
var dir = Vec2.init(0, 0); var dir = Vec2.init(0, 0);
if (frame.input.isKeyDown(.W)) { if (frame.isKeyDown(.W)) {
dir.y -= 1; dir.y -= 1;
} }
if (frame.input.isKeyDown(.S)) { if (frame.isKeyDown(.S)) {
dir.y += 1; dir.y += 1;
} }
if (frame.input.isKeyDown(.A)) { if (frame.isKeyDown(.A)) {
dir.x -= 1; dir.x -= 1;
} }
if (frame.input.isKeyDown(.D)) { if (frame.isKeyDown(.D)) {
dir.x += 1; dir.x += 1;
} }
dir = dir.normalized(); dir = dir.normalized();
if (dir.x != 0 or dir.y != 0) { if (dir.x != 0 or dir.y != 0) {
Audio.play(.{ frame.playAudio(.{
.id = self.assets.wood01 .id = self.assets.wood01,
.volume = 0.1
}); });
} }
self.player = self.player.add(dir.multiplyScalar(50 * frame.dt)); self.player = self.player.add(dir.multiplyScalar(50 * dt));
const regular_font = self.assets.font_id.get(.regular); const regular_font = self.assets.font_id.get(.regular);
Gfx.drawRectangle(.init(0, 0), self.canvas_size, rgb(20, 20, 20)); frame.drawRectangle(.{
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
.color = rgb(20, 20, 20)
});
const size = Vec2.init(20, 20); const size = Vec2.init(20, 20);
Gfx.drawRectangle(self.player.sub(size.divideScalar(2)), size, rgb(200, 20, 20)); frame.drawRectangle(.{
.rect = .{
.pos = self.player.sub(size.divideScalar(2)),
.size = size
},
.color = rgb(200, 20, 20)
});
if (dir.x != 0 or dir.y != 0) { if (dir.x != 0 or dir.y != 0) {
Gfx.drawRectanglOutline(self.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3); frame.drawRectanglOutline(self.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3);
} }
Gfx.drawText(self.player, "Player", .{ frame.drawText(self.player, "Player", .{
.font = regular_font, .font = regular_font,
.size = 10 .size = 10
}); });