Created
March 12, 2025 05:05
-
-
Save Falconerd/aa83898788e27d3f75200f0d5766b57e to your computer and use it in GitHub Desktop.
A simple, yet effective way of handling sprite animations with (immediate) callbacks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Required sprite, from https://pixel-poem.itch.io/dungeon-assetpuck
Video preview (does this thing even work?, if not - head here: (link)):
2025-03-12_16-00-01.mp4