From 8434ea6a64134bd6a1a700dbb9c3766f68cbdf30 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 7 Feb 2026 14:57:47 +0200 Subject: [PATCH] update engine --- src/engine/audio/data.zig | 10 + src/engine/audio/mixer.zig | 35 +++- src/engine/audio/root.zig | 31 ++- src/engine/frame.zig | 291 +++++++++++++++++++++++++++ src/engine/graphics.zig | 376 +++++++++++++++++++++++++---------- src/engine/input.zig | 325 ++++++++++-------------------- src/engine/math.zig | 62 +++++- src/engine/root.zig | 247 +++++++++++------------ src/engine/screen_scaler.zig | 71 ++++--- src/game.zig | 47 +++-- 10 files changed, 986 insertions(+), 509 deletions(-) create mode 100644 src/engine/frame.zig diff --git a/src/engine/audio/data.zig b/src/engine/audio/data.zig index 36c9d67..0fd8bfe 100644 --- a/src/engine/audio/data.zig +++ b/src/engine/audio/data.zig @@ -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) { _ }; }; diff --git a/src/engine/audio/mixer.zig b/src/engine/audio/mixer.zig index 31b0f89..2cc6daf 100644 --- a/src/engine/audio/mixer.zig +++ b/src/engine/audio/mixer.zig @@ -13,13 +13,17 @@ const Mixer = @This(); pub const Instance = struct { data_id: AudioData.Id, + volume: f32 = 0, cursor: u32 = 0, }; pub const Command = union(enum) { - play: struct { - data_id: AudioData.Id, - }, + pub const Play = struct { + id: AudioData.Id, + volume: f32 = 1 + }; + + play: Play, pub const RingBuffer = struct { // 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), commands: Command.RingBuffer, +working_buffer: []f32, pub fn init( gpa: Allocator, max_instances: u32, - max_commands: u32 + max_commands: u32, + working_buffer_size: u32 ) !Mixer { var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances); errdefer instances.deinit(gpa); @@ -74,7 +80,11 @@ pub fn init( const commands = try gpa.alloc(Command, max_commands); errdefer gpa.free(commands); + const working_buffer = try gpa.alloc(f32, working_buffer_size); + errdefer gpa.free(working_buffer); + return Mixer{ + .working_buffer = working_buffer, .instances = instances, .commands = .{ .items = commands @@ -85,6 +95,7 @@ pub fn init( pub fn deinit(self: *Mixer, gpa: Allocator) void { self.instances.deinit(gpa); gpa.free(self.commands.items); + gpa.free(self.working_buffer); } 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| { switch (command) { .play => |opts| { + const volume = @max(opts.volume, 0); + if (volume == 0) { + log.warn("Attempt to play audio with 0 volume", .{}); + continue; + } + self.instances.appendBounded(.{ - .data_id = opts.data_id + .data_id = opts.id, + .volume = volume, }) 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()); @memset(buffer, 0); + assert(self.working_buffer.len >= num_frames); - // var written: u32 = 0; for (self.instances.items) |*instance| { 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); } diff --git a/src/engine/audio/root.zig b/src/engine/audio/root.zig index c384a4d..a1d9365 100644 --- a/src/engine/audio/root.zig +++ b/src/engine/audio/root.zig @@ -11,6 +11,9 @@ pub const Data = @import("./data.zig").Data; pub const Store = @import("./store.zig"); pub const Mixer = @import("./mixer.zig"); +pub const Command = Mixer.Command; + +const Nanoseconds = @import("../root.zig").Nanoseconds; const sokol = @import("sokol"); 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, }); - 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(.{ .logger = opts.logger, @@ -62,17 +69,21 @@ pub fn load(opts: Store.LoadOptions) !Data.Id { return try store.load(opts); } -pub const PlayOptions = struct { - id: Data.Id, - delay: f32 = 0 +const Info = struct { + sample_count: u32, + 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 { - mixer.queue(.{ - .play = .{ - .data_id = opts.id - } - }); +pub fn getInfo(id: Data.Id) Info { + const data = store.get(id); + return Info{ + .sample_count = data.getSampleCount(), + .sample_rate = data.getSampleRate(), + }; } fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void { diff --git a/src/engine/frame.zig b/src/engine/frame.zig new file mode 100644 index 0000000..4b4f2bf --- /dev/null +++ b/src/engine/frame.zig @@ -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 = {} + }); +} diff --git a/src/engine/graphics.zig b/src/engine/graphics.zig index d996475..2ea0894 100644 --- a/src/engine/graphics.zig +++ b/src/engine/graphics.zig @@ -21,6 +21,8 @@ const tracy = @import("tracy"); const fontstash = @import("./fontstash/root.zig"); 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. // From other peoples research it seems that disabling vsync when a resize event occurs fixes it. // Maybe a patch for sokol could be made? @@ -39,29 +41,75 @@ const Options = struct { imgui_font: ?ImguiFont = null }; -const DrawFrame = struct { - screen_size: Vec2 = Vec2.zero, - bg_color: Vec4 = rgb(0, 0, 0), +pub const Command = union(enum) { + pub const DrawRectangle = struct { + rect: Rect, + color: Vec4, + sprite: ?Sprite = null, + rotation: f32 = 0, + origin: Vec2 = .init(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) - }; - } + 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 linear_sampler: sg.Sampler = .{}; var nearest_sampler: sg.Sampler = .{}; 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 { - draw_frame.init(); + gpa = options.allocator; sg.setup(.{ .logger = options.logger, @@ -134,49 +182,110 @@ pub fn init(options: Options) !void { } pub fn deinit() void { + textures.deinit(gpa); imgui.shutdown(); font_context.deinit(); sgl.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 { 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), + .width = sapp.width(), + .height = sapp.height(), .delta_time = sapp.frameDuration(), .dpi_scale = sapp.dpiScale() }); + scale_stack = .initBuffer(&scale_stack_buffer); + scale_stack.appendAssumeCapacity(.init(1, 1)); + font_context.clearState(); sgl.defaults(); 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); } -pub fn endFrame() void { +pub fn endFrame(clear_color: Vec4) 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 + .r = clear_color.x, + .g = clear_color.y, + .b = clear_color.z, + .a = clear_color.w } }; @@ -196,53 +305,95 @@ pub fn endFrame() void { sg.commit(); } -pub fn pushTransform(translation: Vec2, scale: f32) void { +fn pushTransform(translation: Vec2, scale: Vec2) void { sgl.pushMatrix(); 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(); + _ = 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(); defer sgl.end(); sgl.c4f(color.x, color.y, color.z, color.w); - for (quad) |position| { - sgl.v2f(position.x, position.y); + for (quad) |vertex| { + 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 { - 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 drawTriangle(tri: [3]Vec2, color: Vec4) void { - sgl.beginTriangles(); +fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void { + sgl.beginQuads(); defer sgl.end(); sgl.c4f(color.x, color.y, color.z, color.w); - for (tri) |position| { - sgl.v2f(position.x, position.y); + for (quad) |pos| { + 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 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 bottom_right = to.add(step.rotateRight90()); - drawQuad( + drawQuadNoUVs( .{ top_right, top_left, bottom_left, bottom_right }, 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 { return try font_context.addFont(name, data); } -pub const DrawTextOptions = struct { - font: Font.Id, - size: f32 = 16, - color: Vec4 = rgb(255, 255, 255), -}; - -pub fn drawText(position: Vec2, text: []const u8, opts: DrawTextOptions) void { - pushTransform(.{ .x = 0, .y = 0}, 1/font_resolution_scale); - defer popTransform(); - - font_context.setFont(opts.font); - font_context.setSize(opts.size * font_resolution_scale); - 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( - position.x * font_resolution_scale, - position.y * font_resolution_scale, - text - ); +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; +} + +fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image { + if (mipmaps.len == 0) { + return error.NoMipMaps; + } + + var data: sg.ImageData = .{}; + var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels); + + for (mipmaps) |mipmap| { + try mip_levels.appendBounded(.{ + .ptr = mipmap.rgba, + .size = mipmap.width * mipmap.height * 4 + }); + } + + 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; } diff --git a/src/engine/input.zig b/src/engine/input.zig index 0cf6518..ee7ed33 100644 --- a/src/engine/input.zig +++ b/src/engine/input.zig @@ -1,239 +1,124 @@ const std = @import("std"); const sokol = @import("sokol"); -const Engine = @import("./root.zig"); -const Nanoseconds = Engine.Nanoseconds; +const Frame = @import("./frame.zig"); +const Nanoseconds = Frame.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; +const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode); +pub const KeyCode = @Type(.{ + .@"enum" = .{ + .tag_type = std.math.IntFittingRange(0, sokol.app.max_keycodes-1), + .decls = SokolKeyCodeInfo.@"enum".decls, + .fields = SokolKeyCodeInfo.@"enum".fields, + .is_exhaustive = false } +}); + +pub const MouseButton = enum(i3) { + left = @intFromEnum(sokol.app.Mousebutton.LEFT), + right = @intFromEnum(sokol.app.Mousebutton.RIGHT), + middle = @intFromEnum(sokol.app.Mousebutton.MIDDLE), + _ }; -pub const Mouse = struct { - pub const Button = enum { - left, - right, - middle, +pub fn ButtonStateSet(E: type) type { + return struct { + const Self = @This(); - pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?Button { - return switch(mouse_button) { - .LEFT => Button.left, - .RIGHT => Button.right, - .MIDDLE => Button.middle, - else => null - }; + down: std.EnumSet(E), + pressed: std.EnumSet(E), + released: std.EnumSet(E), + pressed_at: std.EnumMap(E, Nanoseconds), + + pub const empty = Self{ + .down = .initEmpty(), + .pressed = .initEmpty(), + .released = .initEmpty(), + .pressed_at = .init(.{}), + }; + + fn press(self: *Self, button: E, now: Nanoseconds) void { + self.pressed_at.put(button, now); + self.pressed.insert(button); + self.down.insert(button); + } + + fn release(self: *Self, button: E) void { + self.down.remove(button); + self.released.insert(button); + self.pressed_at.remove(button); + } + + 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(.{}); } }; - - 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; +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 fn processEvent(frame: *Frame, event: Event) void { + const input = &frame.input; + + switch (event) { + .key_pressed => |opts| { + if (!opts.repeat) { + input.keyboard.press(opts.code, frame.time_ns); + } + }, + .key_released => |key_code| { + input.keyboard.release(key_code); + }, + .mouse_leave => { + input.keyboard.releaseAll(); + + input.mouse_position = null; + input.mouse_button = .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_button.press(opts.button, frame.time_ns); + }, + .mouse_released => |opts| { + input.mouse_position = opts.position; + input.mouse_button.release(opts.button); + }, + else => {} } - - 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) - }; } diff --git a/src/engine/math.zig b/src/engine/math.zig index f620d92..95d0178 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -9,6 +9,28 @@ pub const bytes_per_kb = 1000; pub const bytes_per_mb = bytes_per_kb * 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 { x: 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 { return Vec2{ .x = @cos(angle), @@ -37,6 +63,21 @@ pub const Vec2 = extern struct { 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 { 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 { - return @sqrt(self.x*self.x + self.y*self.y); + return @sqrt(self.lengthSqr()); } pub fn distance(self: Vec2, other: Vec2) f32 { 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 { const self_length = self.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 { return self; } @@ -105,7 +157,7 @@ pub const Vec2 = extern struct { if (self_length == 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 { @@ -351,6 +403,10 @@ pub const Rect = struct { 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 { return Rect{ .pos = self.pos.multiply(xy), diff --git a/src/engine/root.zig b/src/engine/root.zig index 1a7bfe2..36b9e3e 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -7,9 +7,12 @@ const sapp = sokol.app; pub const Math = @import("./math.zig"); pub const Vec2 = Math.Vec2; +const rgb = Math.rgb; pub const Input = @import("./input.zig"); +pub const Frame = @import("./frame.zig"); + const ScreenScalar = @import("./screen_scaler.zig"); pub const imgui = @import("./imgui.zig"); pub const Graphics = @import("./graphics.zig"); @@ -27,43 +30,16 @@ 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, +frame: Frame, + +canvas_size: ?Vec2 = null, const RunOptions = struct { window_title: [*:0]const u8 = "Game", @@ -75,11 +51,11 @@ 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 + .game = undefined, + .frame = undefined }; 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.frame.init(self.allocator); + tracy.setThreadName("Main"); if (builtin.os.tag == .linux) { @@ -154,14 +132,17 @@ fn sokolInit(self: *Engine) !void { .logger = .{ .func = sokolLogCallback }, }); + const seed: u64 = @bitCast(std.time.milliTimestamp()); + 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 { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); + self.frame.deinit(); Audio.deinit(); self.game.deinit(); self.assets.deinit(self.allocator); @@ -171,84 +152,111 @@ fn sokolCleanup(self: *Engine) void { 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 now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); + 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()); - const ctx = ScreenScalar.push(window_size, self.game.canvas_size); - defer ctx.pop(); + _ = frame.arena.reset(.retain_capacity); + const arena = frame.arena.allocator(); - 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{ - .time_ns = time_passed, - .dt_ns = dt_ns, - .dt = @as(f32, @floatFromInt(dt_ns)) / std.time.ns_per_s, - .input = &self.input - }); + const graphics_commands_capacity = frame.graphics.commands.capacity; + const scissor_stack_capacity = frame.graphics.scissor_stack.capacity; + frame.graphics = .empty; + frame.graphics.screen_size = screen_size; + 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(); } - 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 => {} + 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 showDebugWindow(&self.frame); + } + } + + for (frame.audio.commands.items) |command| { + try Audio.mixer.commands.push(command); + } + + if (revert_mouse_position) |pos| { + self.frame.input.mouse_position = pos; + } +} + +fn showDebugWindow(frame: *Frame) !void { + if (!imgui.beginWindow(.{ + .name = "Engine", + .pos = Vec2.init(240, 20), + .size = Vec2.init(200, 200), + })) { + return; + } + defer imgui.endWindow(); + + imgui.textFmt("Draw commands: {}\n", .{ + frame.graphics.commands.items.len, + }); + imgui.textFmt("Audio instances: {}/{}\n", .{ + Audio.mixer.instances.items.len, + Audio.mixer.instances.capacity + }); } 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(); const e = e_ptr.*; - const MouseButton = Input.Mouse.Button; if (imgui.handleEvent(e)) { if (self.mouse_inside) { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_leave = {} }); } @@ -268,13 +275,11 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; } - blk: switch (e.type) { + switch (e.type) { .MOUSE_DOWN => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_pressed = .{ - .button = mouse_button, + .button = @enumFromInt(@intFromEnum(e.mouse_button)), .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; }, .MOUSE_UP => { - const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; - - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_released = .{ - .button = mouse_button, + .button = @enumFromInt(@intFromEnum(e.mouse_button)), .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 => { if (!self.mouse_inside) { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) }); } else { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .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 => { if (!self.mouse_inside) { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .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 => { if (self.mouse_inside) { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_leave = {} }); } - try self.event(Event{ + Input.processEvent(&self.frame, .{ .window_resize = {} }); @@ -333,7 +336,7 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { }, .MOUSE_LEAVE => { if (self.mouse_inside) { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_leave = {} }); } @@ -342,14 +345,14 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; }, .MOUSE_SCROLL => { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) }); return true; }, .KEY_DOWN => { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .key_pressed = .{ .code = @enumFromInt(@intFromEnum(e.key_code)), .repeat = e.key_repeat @@ -359,14 +362,14 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; }, .KEY_UP => { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .key_released = @enumFromInt(@intFromEnum(e.key_code)) }); return true; }, .CHAR => { - try self.event(Event{ + Input.processEvent(&self.frame, .{ .char = @intCast(e.char_code) }); diff --git a/src/engine/screen_scaler.zig b/src/engine/screen_scaler.zig index e7860f3..08a57ea 100644 --- a/src/engine/screen_scaler.zig +++ b/src/engine/screen_scaler.zig @@ -2,19 +2,21 @@ const Gfx = @import("./graphics.zig"); const Math = @import("./math.zig"); const Vec2 = Math.Vec2; +const Vec4 = Math.Vec4; const rgb = Math.rgb; +const Frame = @import("./frame.zig"); + const ScreenScalar = @This(); // TODO: Implement a fractional pixel perfect scalar // 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/ -window_size: Vec2, translation: Vec2, 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. // To avoid pixel bleeding in spritesheet artifacts const scale = @floor(@min( @@ -26,42 +28,53 @@ pub fn push(window_size: Vec2, canvas_size: Vec2) ScreenScalar { 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(); +pub fn apply(self: ScreenScalar, window_size: Vec2, frame: *Frame, color: Vec4) void { + const scale = self.scale; + const translation = self.translation; - const bg_color = rgb(0, 0, 0); - const filler_size = self.translation; + frame.prependGraphicsCommand(.{ + .push_transformation = .{ + .translation = translation, + .scale = .init(scale, scale) + } + }); + frame.popTransform(); - Gfx.drawRectangle( - .init(0, 0), - .init(self.window_size.x, filler_size.y), - bg_color - ); + frame.drawRectangle(.{ + .rect = .{ + .pos = .init(0, 0), + .size = .init(window_size.x, translation.y), + }, + .color = color + }); - Gfx.drawRectangle( - .init(0, self.window_size.y - filler_size.y), - .init(self.window_size.x, filler_size.y), - bg_color - ); + frame.drawRectangle(.{ + .rect = .{ + .pos = .init(0, window_size.y - translation.y), + .size = .init(window_size.x, translation.y), + }, + .color = color + }); - Gfx.drawRectangle( - .init(0, 0), - .init(filler_size.x, self.window_size.y), - bg_color - ); + frame.drawRectangle(.{ + .rect = .{ + .pos = .init(0, 0), + .size = .init(translation.x, window_size.y), + }, + .color = color + }); - Gfx.drawRectangle( - .init(self.window_size.x - filler_size.x, 0), - .init(filler_size.x, self.window_size.y), - bg_color - ); + frame.drawRectangle(.{ + .rect = .{ + .pos = .init(window_size.x - translation.x, 0), + .size = .init(translation.x, window_size.y), + }, + .color = color + }); } diff --git a/src/game.zig b/src/game.zig index a3a1836..4d47a66 100644 --- a/src/game.zig +++ b/src/game.zig @@ -15,15 +15,14 @@ const Game = @This(); gpa: Allocator, assets: *Assets, -canvas_size: 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{ .gpa = gpa, .assets = assets, - .canvas_size = Vec2.init(100, 100), .player = .init(50, 50) }; } @@ -32,41 +31,59 @@ pub fn deinit(self: *Game) void { _ = 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); - if (frame.input.isKeyDown(.W)) { + if (frame.isKeyDown(.W)) { dir.y -= 1; } - if (frame.input.isKeyDown(.S)) { + if (frame.isKeyDown(.S)) { dir.y += 1; } - if (frame.input.isKeyDown(.A)) { + if (frame.isKeyDown(.A)) { dir.x -= 1; } - if (frame.input.isKeyDown(.D)) { + if (frame.isKeyDown(.D)) { dir.x += 1; } dir = dir.normalized(); if (dir.x != 0 or dir.y != 0) { - Audio.play(.{ - .id = self.assets.wood01 + frame.playAudio(.{ + .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); - 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); - 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) { - 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, .size = 10 });