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

View File

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

View File

@ -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 {

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

View File

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

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_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),

View File

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

View File

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

View File

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