Skip to content

Instantly share code, notes, and snippets.

@Falconerd
Created March 12, 2025 05:05
Show Gist options
  • Save Falconerd/aa83898788e27d3f75200f0d5766b57e to your computer and use it in GitHub Desktop.
Save Falconerd/aa83898788e27d3f75200f0d5766b57e to your computer and use it in GitHub Desktop.
A simple, yet effective way of handling sprite animations with (immediate) callbacks.
package main
import "core:fmt"
import rl "vendor:raylib"
Vec2 :: [2]f32
Rect :: rl.Rectangle
Sprite_Animation_Frame :: struct {
time: f32,
index: i32,
}
Sprite_Animation_Flags :: bit_set[Sprite_Animation_Flag]
Sprite_Animation_Flag :: enum {
Loop,
Ping_Pong,
Reverse,
Done,
}
Sprite_Animation_Event :: struct {
callback: proc(payload: rawptr),
time: f32,
flags: Sprite_Animation_Event_Flags,
}
Sprite_Animation_Event_Flags :: bit_set[Sprite_Animation_Event_Flag]
Sprite_Animation_Event_Flag :: enum {
Timed,
On_Loop,
On_Finish,
On_Ping_Pong,
}
Sprite_Animation :: struct {
texture: rl.Texture,
frames: []Sprite_Animation_Frame,
events: []Sprite_Animation_Event,
rect: Rect,
size: Vec2,
flags: Sprite_Animation_Flags,
frame: i32,
cycle: i32,
frame_timer: f32,
event_timer: f32,
events_done: []bool,
event_payloads: []rawptr,
}
Entity :: struct {
position: Vec2,
animation: Sprite_Animation,
}
anim_construct_frames :: proc(start, end: int, time: f32, allocator := context.allocator) -> []Sprite_Animation_Frame {
frames := make([dynamic]Sprite_Animation_Frame)
for index in start..= end {
append(&frames, Sprite_Animation_Frame {
time = time,
index = i32(index),
})
}
return frames[:]
}
anim_update :: proc(anim: ^Sprite_Animation, delta_time: f32) {
finished_this_frame := false
ping_ponged_this_frame := false
looped_this_frame := false
if anim.frame_timer >= anim.frames[anim.frame].time && .Done not_in anim.flags {
frame_count := i32(len(anim.frames))
is_reverse := .Reverse in anim.flags
next_frame := (anim.frame + 1) % frame_count
if is_reverse do next_frame = (anim.frame - 1 + frame_count) % frame_count
at_last_frame := is_reverse ? next_frame == frame_count - 1 : next_frame == 0
if at_last_frame {
anim.cycle += 1
if .Ping_Pong in anim.flags {
if is_reverse {
next_frame = 1
anim.flags -= {.Reverse}
ping_ponged_this_frame = true
} else {
// Handle 1-frame 'animations'
next_frame = frame_count - min(frame_count, 2)
anim.flags += {.Reverse}
ping_ponged_this_frame = true
}
// Finished if not looping
if .Loop not_in anim.flags && anim.cycle == 2 {
anim.flags += {.Done}
finished_this_frame = true
next_frame = anim.frame
}
} else if .Loop not_in anim.flags && anim.cycle == 1 {
// Finished if not looping
anim.flags += {.Done}
finished_this_frame = true
next_frame = anim.frame
}
if .Loop in anim.flags {
looped_this_frame = true
for &v in anim.events_done {
v = false
}
}
}
anim.frame = next_frame
anim.frame_timer = 0
anim.rect = {f32(anim.frame) * anim.size.x, 0, anim.size.x, anim.size.y}
}
if .Done not_in anim.flags {
anim.frame_timer += delta_time
}
for &event, i in anim.events {
is_done := anim.events_done[i]
if is_done do continue
call_event := false
if .Timed in event.flags && anim.event_timer >= event.time {
call_event = true
}
if .On_Finish in event.flags && finished_this_frame {
call_event = true
}
if .On_Loop in event.flags && looped_this_frame {
call_event = true
}
if .On_Ping_Pong in event.flags && ping_ponged_this_frame {
call_event = true
}
if call_event {
payload := anim.event_payloads[i]
event.callback(payload)
anim.events_done[i] = true
}
}
anim.event_timer += delta_time
}
callback_value := 0
anim_callback :: proc(payload: rawptr) {
callback_value += 1
}
callback2_value := 0
anim2_callback :: proc(payload: rawptr) {
callback2_value += 1
}
callback3_value := 0
anim3_callback :: proc(payload: rawptr) {
ptr := cast(^int)payload
callback3_value = ptr^
}
WINDOW_WIDTH :: 600
WINDOW_HEIGHT :: 600
RENDER_WIDTH :: 110
RENDER_HEIGHT :: 110
ZOOM :: WINDOW_WIDTH / RENDER_WIDTH
main :: proc() {
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Sprite Animations!")
rl.SetTargetFPS(60)
camera := rl.Camera2D {
zoom = ZOOM
}
vampire_death_texture := rl.LoadTexture("enemies-vampire_death.png")
// Initialise the animation data
// I'd probably turn this into procedures for convenince
vampire_death_anim := Sprite_Animation {
texture = vampire_death_texture,
frames = anim_construct_frames(0, 13, 0.10),
size = {32, 32},
flags = {.Loop, .Ping_Pong},
rect = {0, 0, 32, 32}
}
vampire := Entity {
position = Vec2{RENDER_WIDTH, RENDER_HEIGHT} / 2 - 16 - {-32, 32},
animation = vampire_death_anim
}
vampire.animation.event_payloads = []rawptr {nil}
vampire.animation.events = []Sprite_Animation_Event {
{callback = anim_callback, flags = {.On_Ping_Pong}}
}
vampire.animation.events_done = make([]bool, 1)
vampire2 := Entity {
position = Vec2{RENDER_WIDTH, RENDER_HEIGHT} / 2 - 16 - {-32, 0},
animation = vampire_death_anim
}
vampire2.animation.flags = {.Loop}
vampire2.animation.event_payloads = []rawptr {nil}
vampire2.animation.events = []Sprite_Animation_Event {
{callback = anim2_callback, flags = {.On_Loop}}
}
vampire2.animation.events_done = make([]bool, 1)
vampire3 := Entity {
position = Vec2{RENDER_WIDTH, RENDER_HEIGHT} / 2 - 16 + {32, 32},
animation = vampire_death_anim
}
vampire3.animation.rect = {12 * 32, 0, 32, 32} // Last frame
vampire3.animation.flags = {.Loop, .Reverse}
one := 1
two := 2
three := 3
vampire3.animation.event_payloads = []rawptr {&one, &two, &three}
vampire3.animation.events = []Sprite_Animation_Event {
{callback = anim3_callback, time = 1, flags = {.Timed}},
{callback = anim3_callback, time = 2, flags = {.Timed}},
{callback = anim3_callback, time = 3, flags = {.Timed}},
}
vampire3.animation.events_done = make([]bool, 3)
started := false
// Drawing code below here
for !rl.WindowShouldClose() {
if rl.IsKeyPressed(.SPACE) {
started = true
}
delta_time := rl.GetFrameTime()
if started {
anim_update(&vampire.animation, delta_time)
anim_update(&vampire2.animation, delta_time)
anim_update(&vampire3.animation, delta_time)
}
rl.BeginDrawing()
rl.BeginMode2D(camera)
rl.ClearBackground({0, 0, 28, 255})
rl.DrawTextureRec(vampire.animation.texture, vampire.animation.rect, vampire.position, rl.WHITE)
rl.DrawTextureRec(vampire2.animation.texture, vampire2.animation.rect, vampire2.position, rl.WHITE)
rl.DrawTextureRec(vampire3.animation.texture, vampire3.animation.rect, vampire3.position, rl.WHITE)
rl.EndMode2D()
rl.DrawText(fmt.ctprintf("Loop, Ping_Pong"), 20, i32(vampire.position.y * ZOOM) + 16 * ZOOM, 40, rl.WHITE)
rl.DrawText(fmt.ctprintf("CB Value: %v", callback_value), 20, i32(vampire.position.y * ZOOM) + 16 * ZOOM + 50, 40, rl.WHITE)
rl.DrawText(fmt.ctprintf("Loop"), 20, i32(vampire.position.y * ZOOM) + 48 * ZOOM, 40, rl.WHITE)
rl.DrawText(fmt.ctprintf("CB Value: %v", callback2_value), 20, i32(vampire.position.y * ZOOM) + 48 * ZOOM + 50, 40, rl.WHITE)
rl.DrawText(fmt.ctprintf("Loop, Reverse"), 20, i32(vampire.position.y * ZOOM) + 80 * ZOOM, 40, rl.WHITE)
rl.DrawText(fmt.ctprintf("CB Value: %v", callback3_value), 20, i32(vampire.position.y * ZOOM) + 80 * ZOOM + 50, 40, rl.WHITE)
rl.EndDrawing()
}
}
@Falconerd
Copy link
Author

Falconerd commented Mar 12, 2025

Required sprite, from https://pixel-poem.itch.io/dungeon-assetpuck

enemies-vampire_death

Video preview (does this thing even work?, if not - head here: (link)):

2025-03-12_16-00-01.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment