diff --git a/build.zig b/build.zig index d39f4a2..a27137c 100644 --- a/build.zig +++ b/build.zig @@ -7,7 +7,10 @@ pub fn build(b: *std.Build) !void { const assets_dir = "assets"; const asset_files = [_][]const u8{ - "tranquil_tunnels_transparent.png" + "tranquil_tunnels_transparent.png", + "Prototype_Character/Default/idle.png", + "Prototype_Character/Default/walk.png", + "Prototype_Character/Default/death.png" // "sokoban/sokoban_tilesheet.png", // "roboto-font/Roboto-Regular.ttf", // "tiled/map.tmx", diff --git a/src/app.zig b/src/app.zig index d0b0ac5..e79e325 100644 --- a/src/app.zig +++ b/src/app.zig @@ -15,9 +15,24 @@ const Gfx = Platform.Gfx; const ImGUI = Platform.ImGUI; const Rect = Platform.Rect; +const SlotMapType = @import("./slot_map.zig").SlotMapType; + const App = @This(); -tilesheet: Tilesheet, +const Prng = std.Random.DefaultPrng; + +gpa: Allocator, +camera_pos: Vec2 = .init(0, 0), +prng: Prng, + +minions: MinionSlotMap, + +idle_animations: AnimationDirections, +walk_animations: AnimationDirections, +death_animations: AnimationDirections, + +const MinionSlotMap = SlotMapType(u16, u16, Minion); +const MinionId = MinionSlotMap.Id; const Tilesheet = struct { image: Gfx.ImageData, @@ -66,50 +81,226 @@ const Tilesheet = struct { } }; +const Animation = struct { + frames: []Gfx.Sprite.Id, + frame_duration: Platform.Nanoseconds, + + pub fn init(gpa: Allocator, tilesheet: Tilesheet, frame_duration: Platform.Nanoseconds, tiles: []const Vec2) !Animation { + const frames = try gpa.alloc(Gfx.Sprite.Id, tiles.len); + for (0.., tiles) |i, tile| { + frames[i] = tilesheet.get(@intFromFloat(tile.x), @intFromFloat(tile.y)); + } + + return Animation{ + .frames = frames, + .frame_duration = frame_duration + }; + } +}; + +const AnimationDirections = struct { + down: Animation, + right: Animation, + up: Animation, + + pub fn init(gpa: Allocator, sheet: Tilesheet, frame_duration: Platform.Nanoseconds, frames_count: u32) !AnimationDirections { + var tiles_buffer: [16]Vec2 = undefined; + for (0..frames_count) |i| { + tiles_buffer[i] = .initFromInt(usize, i, 0); + } + const down = try Animation.init(gpa, sheet, frame_duration, tiles_buffer[0..frames_count]); + + for (0..frames_count) |i| { + tiles_buffer[i] = .initFromInt(usize, i, 1); + } + const right = try Animation.init(gpa, sheet, frame_duration, tiles_buffer[0..frames_count]); + + for (0..frames_count) |i| { + tiles_buffer[i] = .initFromInt(usize, i, 2); + } + const up = try Animation.init(gpa, sheet, frame_duration, tiles_buffer[0..frames_count]); + + return AnimationDirections{ + .down = down, + .right = right, + .up = up, + }; + } +}; + +const AnimationName = enum { + idle, + walk, + death, +}; + +const AnimationDirection = enum { + left, + right, + up, + down +}; + +const Minion = struct { + pos: Vec2, + animation: AnimationName, + animation_dir: AnimationDirection, + animation_index: u32, + animation_timer: Platform.Nanoseconds, +}; + +fn initImageDataFromSTB(img: STBImage) Gfx.ImageData { + return Gfx.ImageData{ + .width = img.width, + .height = img.height, + .pixels = .{ .rgba8 = img.rgba8_pixels } + }; +} + pub fn init(self: *App, plt: Platform.Init) !?u8 { // const robot_font = Gfx.initFont(); // Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf")); - const tilesheet_png = try STBImage.load(plt.assets.readFile("tranquil_tunnels_transparent.png")); - const tilesheet = try Tilesheet.init( + const idle_sheet = try Tilesheet.init( plt.arena, - Gfx.ImageData{ - .width = tilesheet_png.width, - .height = tilesheet_png.height, - .pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } - }, - 13, - 8, - .init(64, 64) + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/idle.png"))), + 2, + 3, + .init(32, 32) ); + const walk_sheet = try Tilesheet.init( + plt.arena, + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/walk.png"))), + 4, + 3, + .init(32, 32) + ); + + const death_sheet = try Tilesheet.init( + plt.arena, + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/death.png"))), + 3, + 3, + .init(32, 32) + ); + + const ns_per_s = std.time.ns_per_s; + const idle_fps = 5; + const walk_fps = 5; + const death_fps = 3; + + const now = std.Io.Timestamp.now(plt.io, .awake); + self.* = App{ - .tilesheet = tilesheet - }; + .gpa = plt.gpa, + .idle_animations = try AnimationDirections.init(plt.arena, idle_sheet, ns_per_s / idle_fps, 2), + .walk_animations = try AnimationDirections.init(plt.arena, walk_sheet, ns_per_s / walk_fps, 4), + .death_animations = try AnimationDirections.init(plt.arena, death_sheet, ns_per_s / death_fps, 3), + .minions = .init(.empty), + .prng = Prng.init(@truncate(@as(u96, @intCast(now.nanoseconds)))) + }; + + try self.spawnMinion(.init(0, 0)); + try self.spawnMinion(.init(32, 0)); return null; } +pub fn spawnMinion(self: *App, pos: Vec2) !void { + const minion_id = try self.minions.insert(self.gpa); + const minion = self.minions.getAssumeExists(minion_id); + minion.* = Minion{ + .pos = pos, + .animation = .death, + .animation_dir = .right, + .animation_index = 0, + .animation_timer = 0 + }; +} + pub fn frame(self: *App, plt: Platform.Frame) !void { - _ = self; // autofix const input = plt.input; _ = input; // autofix const dt = plt.deltaTime(); - _ = dt; // autofix + + if (plt.input.isKeyDown(.D)) { + self.camera_pos.x += 50 * dt; + } + if (plt.input.isKeyDown(.A)) { + self.camera_pos.x -= 50 * dt; + } + + if (plt.input.isMouseDown(.left)) { + self.camera_pos = self.camera_pos.sub(plt.input.mouse_delta); + } + + Gfx.viewTranslate(plt.input.window_size.x/2, plt.input.window_size.y/2); + Gfx.viewTranslate(-self.camera_pos.x, -self.camera_pos.y); Gfx.setClearColor(.rgb(40, 40, 40)); - Gfx.drawLine(.init(0, 200), .init(300, 200), .white, 5); - // Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white); - // Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white); - // Gfx.drawText(self.roboto_font, .{ - // .pos = .init(300, 100), - // .text = "Hello, World!", - // .height = 64, - // }); + Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5); + + const character_size = Vec2.init(64, 64); + + var minion_iter = self.minions.iterator(); + while (minion_iter.next()) |minion_id| { + const minion = self.minions.getAssumeExists(minion_id); + + const animations = switch (minion.animation) { + .idle => self.idle_animations, + .walk => self.walk_animations, + .death => self.death_animations + }; + var flip_animation = false; + const animation = switch (minion.animation_dir) { + .right => animations.right, + .up => animations.up, + .down => animations.down, + .left => blk: { + flip_animation = true; + break :blk animations.right; + } + }; + + minion.animation_timer += plt.dt; + while (minion.animation_timer >= animation.frame_duration) { + minion.animation_timer -= animation.frame_duration; + minion.animation_index += 1; + } + + minion.animation_index = minion.animation_index % @as(u32, @intCast(animation.frames.len)); + const sprite = animation.frames[minion.animation_index]; + + var size = character_size; + var pos = minion.pos; + pos.y -= character_size.y*0.7; + if (flip_animation) { + size.x *= -1; + pos.x += character_size.x/2; + } else { + pos.x -= character_size.x/2; + } + + Gfx.drawRectangle( + minion.pos.sub(.init(5, 5)), + .init(10, 10), + .white + ); + Gfx.drawSprite(sprite, pos, size, .white); + } + + if (ImGUI.beginWindow(.{ .name = "Debug", .size = .init(200, 100) })) { + defer ImGUI.endWindow(); + + if (ImGUI.button("spawn")) { + try self.spawnMinion(.init(32, 0)); + } + } } pub fn deinit(self: *App, plt: Platform.Deinit) void { + self.minions.deinit(self.gpa); _ = plt; // autofix - _ = self; // autofix } diff --git a/src/graphics.zig b/src/graphics.zig index 8a29f1a..d513ac6 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -26,6 +26,7 @@ const State = struct { frame_index: u64, + view: Mat4, shader: sg.Shader, pipeline: sg.Pipeline, bindings: sg.Bindings, @@ -150,6 +151,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { self.* = State{ .gpa = gpa, .frame_index = 0, + .view = .initIdentity(), .shader = shader, .pipeline = pipeline, .bindings = bindings, @@ -163,7 +165,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .linear_sampler = linear_sampler, .nearest_sampler = nearest_sampler, - .default_sampler = .linear, + .default_sampler = .nearest, .textures_buffer = undefined, .textures = .init(.initBuffer(&self.textures_buffer)), @@ -238,7 +240,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { var pixels: [width * height * 4]u8 = undefined; @memset(&pixels, 0xFF); - self.default_sprite = initSprite(.{ .padding = 1 }); + self.default_sprite = initSprite(.{ }); setSprite(self.default_sprite, .{ .image = ImageData{ .width = width, @@ -272,7 +274,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { } } - self.error_sprite = initSprite(.{ .padding = 1 }); + self.error_sprite = initSprite(.{ }); setSprite(self.error_sprite, .{ .image = .{ .width = width, @@ -328,6 +330,20 @@ fn toSokolColor(color: Color) sokol.gfx.Color { }; } +pub fn viewTranslate(x: f32, y: f32) void { + flush(.{}); + + const self = &g_state; + self.view.translate(x, y, 0); +} + +pub fn viewScale(x: f32, y: f32) void { + flush(.{}); + + const self = &g_state; + self.view.scale(x, y, 1); +} + pub fn beginFrame() void { const self = &g_state; @@ -342,6 +358,8 @@ pub fn beginFrame() void { .swapchain = sglue.swapchain() }); + self.view = .initIdentity(); + const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet); const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture); self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view; @@ -430,7 +448,7 @@ pub fn flush(opts: FlushOptions) void { var vs_params: shd.VsParams = .{ // TODO: Maybe 'view' matrix is not needed. // We probably should only have a combined 'mvp' matrix - .view = .initIdentity(), + .view = self.view, .projection = createProjectionMatrix(window_size) }; @@ -1070,8 +1088,16 @@ fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void { if (getSpriteUVRect(id)) |sprite_uv| { const sprite = self.sprites.getAssumeExists(id); const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); - const texel_size = spritesheet.size; + const texel_size = Vec2.init(1, 1).divide(spritesheet.size); + _ = texel_size; // autofix + // TODO: I hope this doesn't bite me in the ass. + // The shrink of the UV by half a texel + // This is to avoid linear sampling artifacts at edges + // + // Yes I did regret this. Fuck. + // + // quad.setUVRect(sprite_uv.shrink(texel_size.divideScalar(2))); quad.setUVRect(sprite_uv); texture = spritesheet.texture; } diff --git a/src/input.zig b/src/input.zig index dcd58c4..fda9811 100644 --- a/src/input.zig +++ b/src/input.zig @@ -15,6 +15,7 @@ focused: bool, keyboard: KeyStateType(KeyCode), mouse_buttons: KeyStateType(MouseButton), mouse_position: ?Vec2, +mouse_delta: Vec2, mouse_scroll: Vec2, pub fn init(window_size: Vec2) Input { @@ -24,6 +25,7 @@ pub fn init(window_size: Vec2) Input { .keyboard = .empty, .mouse_buttons = .empty, .mouse_position = null, + .mouse_delta = .init(0, 0), .mouse_scroll = .init(0, 0), .window_size = window_size }; diff --git a/src/math.zig b/src/math.zig index abe04e5..08cf77d 100644 --- a/src/math.zig +++ b/src/math.zig @@ -336,11 +336,11 @@ pub const Mat4 = extern struct { return self; } - pub fn initScale(scale: Vec3) Mat4 { + pub fn initScale(vec: Vec3) Mat4 { var self = Mat4.initIdentity(); - self.columns[0][0] = scale.x; - self.columns[1][1] = scale.y; - self.columns[2][2] = scale.z; + self.columns[0][0] = vec.x; + self.columns[1][1] = vec.y; + self.columns[2][2] = vec.z; return self; } @@ -352,6 +352,18 @@ pub const Mat4 = extern struct { return self; } + pub fn translate(self: *Mat4, x: f32, y: f32, z: f32) void { + self.columns[3][0] += x; + self.columns[3][1] += y; + self.columns[3][2] += z; + } + + pub fn scale(self: *Mat4, x: f32, y: f32, z: f32) void { + self.columns[0][0] *= x; + self.columns[1][1] *= y; + self.columns[2][2] *= z; + } + pub fn asArray(self: *Mat4) []f32 { const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); return ptr[0..16]; @@ -431,6 +443,13 @@ pub const Rect = struct { const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y; return x_overlap and y_overlap; } + + pub fn shrink(self: Rect, margin: Vec2) Rect { + return Rect{ + .pos = self.pos.add(margin), + .size = self.pos.add(margin.multiplyScalar(2)) + }; + } }; pub const Line = struct { diff --git a/src/platform.zig b/src/platform.zig index 5d6b26f..f93f3b8 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -44,6 +44,7 @@ fn PlatformType(App: type) type { last_frame_at: Io.Timestamp, assets: Assets, + last_mouse_position: ?Vec2, input: Input, events_buffer: [256]Input.Event, events_overflow: bool, @@ -108,6 +109,13 @@ fn PlatformType(App: type) type { self.show_imgui = !self.show_imgui; } + if (self.last_mouse_position != null and self.input.mouse_position != null) { + self.input.mouse_delta = self.input.mouse_position.?.sub(self.last_mouse_position.?); + } else { + self.input.mouse_delta = .init(0, 0); + } + self.last_mouse_position = self.input.mouse_position; + { Gfx.beginFrame(); ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); @@ -140,6 +148,7 @@ fn PlatformType(App: type) type { self.input.keyboard.released = .empty; self.input.mouse_buttons.pressed = .empty; self.input.mouse_buttons.released = .empty; + self.input.mouse_scroll = .init(0, 0); } fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { @@ -566,7 +575,8 @@ pub fn run(App: type, opts: RunOptions) void { .last_key_pressed = null, .frame_arena = .init(gpa), .show_imgui = builtin.mode == .Debug, - .assets = .init(gpa, io, assets_dir) + .assets = .init(gpa, io, assets_dir), + .last_mouse_position = null }; inline for (EmbeddedAssets.files) |file| {