add animations

This commit is contained in:
Rokas Puzonas 2026-07-14 00:55:26 +03:00
parent 9332649a63
commit 17acc49282
6 changed files with 286 additions and 35 deletions

View File

@ -7,7 +7,10 @@ pub fn build(b: *std.Build) !void {
const assets_dir = "assets"; const assets_dir = "assets";
const asset_files = [_][]const u8{ 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", // "sokoban/sokoban_tilesheet.png",
// "roboto-font/Roboto-Regular.ttf", // "roboto-font/Roboto-Regular.ttf",
// "tiled/map.tmx", // "tiled/map.tmx",

View File

@ -15,9 +15,24 @@ const Gfx = Platform.Gfx;
const ImGUI = Platform.ImGUI; const ImGUI = Platform.ImGUI;
const Rect = Platform.Rect; const Rect = Platform.Rect;
const SlotMapType = @import("./slot_map.zig").SlotMapType;
const App = @This(); 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 { const Tilesheet = struct {
image: Gfx.ImageData, 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 { pub fn init(self: *App, plt: Platform.Init) !?u8 {
// const robot_font = Gfx.initFont(); // const robot_font = Gfx.initFont();
// Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf")); // 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 idle_sheet = try Tilesheet.init(
const tilesheet = try Tilesheet.init(
plt.arena, plt.arena,
Gfx.ImageData{ initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/idle.png"))),
.width = tilesheet_png.width, 2,
.height = tilesheet_png.height, 3,
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } .init(32, 32)
},
13,
8,
.init(64, 64)
); );
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{ 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; 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 { pub fn frame(self: *App, plt: Platform.Frame) !void {
_ = self; // autofix
const input = plt.input; const input = plt.input;
_ = input; // autofix _ = input; // autofix
const dt = plt.deltaTime(); 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.setClearColor(.rgb(40, 40, 40));
Gfx.drawLine(.init(0, 200), .init(300, 200), .white, 5); Gfx.drawLine(.init(-800, 0), .init(800, 0), .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); const character_size = Vec2.init(64, 64);
// Gfx.drawText(self.roboto_font, .{
// .pos = .init(300, 100), var minion_iter = self.minions.iterator();
// .text = "Hello, World!", while (minion_iter.next()) |minion_id| {
// .height = 64, 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 { pub fn deinit(self: *App, plt: Platform.Deinit) void {
self.minions.deinit(self.gpa);
_ = plt; // autofix _ = plt; // autofix
_ = self; // autofix
} }

View File

@ -26,6 +26,7 @@ const State = struct {
frame_index: u64, frame_index: u64,
view: Mat4,
shader: sg.Shader, shader: sg.Shader,
pipeline: sg.Pipeline, pipeline: sg.Pipeline,
bindings: sg.Bindings, bindings: sg.Bindings,
@ -150,6 +151,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
self.* = State{ self.* = State{
.gpa = gpa, .gpa = gpa,
.frame_index = 0, .frame_index = 0,
.view = .initIdentity(),
.shader = shader, .shader = shader,
.pipeline = pipeline, .pipeline = pipeline,
.bindings = bindings, .bindings = bindings,
@ -163,7 +165,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.linear_sampler = linear_sampler, .linear_sampler = linear_sampler,
.nearest_sampler = nearest_sampler, .nearest_sampler = nearest_sampler,
.default_sampler = .linear, .default_sampler = .nearest,
.textures_buffer = undefined, .textures_buffer = undefined,
.textures = .init(.initBuffer(&self.textures_buffer)), .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; var pixels: [width * height * 4]u8 = undefined;
@memset(&pixels, 0xFF); @memset(&pixels, 0xFF);
self.default_sprite = initSprite(.{ .padding = 1 }); self.default_sprite = initSprite(.{ });
setSprite(self.default_sprite, .{ setSprite(self.default_sprite, .{
.image = ImageData{ .image = ImageData{
.width = width, .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, .{ setSprite(self.error_sprite, .{
.image = .{ .image = .{
.width = width, .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 { pub fn beginFrame() void {
const self = &g_state; const self = &g_state;
@ -342,6 +358,8 @@ pub fn beginFrame() void {
.swapchain = sglue.swapchain() .swapchain = sglue.swapchain()
}); });
self.view = .initIdentity();
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet); const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture); const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view; self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
@ -430,7 +448,7 @@ pub fn flush(opts: FlushOptions) void {
var vs_params: shd.VsParams = .{ var vs_params: shd.VsParams = .{
// TODO: Maybe 'view' matrix is not needed. // TODO: Maybe 'view' matrix is not needed.
// We probably should only have a combined 'mvp' matrix // We probably should only have a combined 'mvp' matrix
.view = .initIdentity(), .view = self.view,
.projection = createProjectionMatrix(window_size) .projection = createProjectionMatrix(window_size)
}; };
@ -1070,8 +1088,16 @@ fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void {
if (getSpriteUVRect(id)) |sprite_uv| { if (getSpriteUVRect(id)) |sprite_uv| {
const sprite = self.sprites.getAssumeExists(id); const sprite = self.sprites.getAssumeExists(id);
const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); 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); quad.setUVRect(sprite_uv);
texture = spritesheet.texture; texture = spritesheet.texture;
} }

View File

@ -15,6 +15,7 @@ focused: bool,
keyboard: KeyStateType(KeyCode), keyboard: KeyStateType(KeyCode),
mouse_buttons: KeyStateType(MouseButton), mouse_buttons: KeyStateType(MouseButton),
mouse_position: ?Vec2, mouse_position: ?Vec2,
mouse_delta: Vec2,
mouse_scroll: Vec2, mouse_scroll: Vec2,
pub fn init(window_size: Vec2) Input { pub fn init(window_size: Vec2) Input {
@ -24,6 +25,7 @@ pub fn init(window_size: Vec2) Input {
.keyboard = .empty, .keyboard = .empty,
.mouse_buttons = .empty, .mouse_buttons = .empty,
.mouse_position = null, .mouse_position = null,
.mouse_delta = .init(0, 0),
.mouse_scroll = .init(0, 0), .mouse_scroll = .init(0, 0),
.window_size = window_size .window_size = window_size
}; };

View File

@ -336,11 +336,11 @@ pub const Mat4 = extern struct {
return self; return self;
} }
pub fn initScale(scale: Vec3) Mat4 { pub fn initScale(vec: Vec3) Mat4 {
var self = Mat4.initIdentity(); var self = Mat4.initIdentity();
self.columns[0][0] = scale.x; self.columns[0][0] = vec.x;
self.columns[1][1] = scale.y; self.columns[1][1] = vec.y;
self.columns[2][2] = scale.z; self.columns[2][2] = vec.z;
return self; return self;
} }
@ -352,6 +352,18 @@ pub const Mat4 = extern struct {
return self; 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 { pub fn asArray(self: *Mat4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns))));
return ptr[0..16]; 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; const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;
return x_overlap and y_overlap; 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 { pub const Line = struct {

View File

@ -44,6 +44,7 @@ fn PlatformType(App: type) type {
last_frame_at: Io.Timestamp, last_frame_at: Io.Timestamp,
assets: Assets, assets: Assets,
last_mouse_position: ?Vec2,
input: Input, input: Input,
events_buffer: [256]Input.Event, events_buffer: [256]Input.Event,
events_overflow: bool, events_overflow: bool,
@ -108,6 +109,13 @@ fn PlatformType(App: type) type {
self.show_imgui = !self.show_imgui; 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(); Gfx.beginFrame();
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); 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.keyboard.released = .empty;
self.input.mouse_buttons.pressed = .empty; self.input.mouse_buttons.pressed = .empty;
self.input.mouse_buttons.released = .empty; self.input.mouse_buttons.released = .empty;
self.input.mouse_scroll = .init(0, 0);
} }
fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void {
@ -566,7 +575,8 @@ pub fn run(App: type, opts: RunOptions) void {
.last_key_pressed = null, .last_key_pressed = null,
.frame_arena = .init(gpa), .frame_arena = .init(gpa),
.show_imgui = builtin.mode == .Debug, .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| { inline for (EmbeddedAssets.files) |file| {