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 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",

View File

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

View File

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

View File

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

View File

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

View File

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