Skip to content

Sprite Animation

Cycle through animation frames with configurable timing. Walk cycles, explosions, idle animations.

spritesanimationrenderinggraphics

Overview

Sprite animation cycles through a sequence of frames at a fixed rate. A frame counter ticks each game frame; when it reaches the delay threshold, advance to the next animation frame. Wrap back to the first frame at the end.

The same logic works for walk cycles, explosions, flickering torches — any repeating animation.

Algorithm

; Animation state
anim_frame: byte = 0       ; Current frame index (0, 1, 2...)
anim_counter: byte = 0     ; Ticks until next frame

; Animation data
ANIM_DELAY = 8             ; Frames between animation steps
ANIM_LENGTH = 4            ; Number of frames in animation

update_animation:
    anim_counter = anim_counter + 1
    if anim_counter < ANIM_DELAY:
        return             ; Not time yet

    ; Time to advance
    anim_counter = 0
    anim_frame = anim_frame + 1
    if anim_frame >= ANIM_LENGTH:
        anim_frame = 0     ; Wrap to start

    return

Pseudocode

; Animation frames (sprite indices or pointers)
walk_frames: byte[4] = { 0, 1, 2, 1 }    ; Walk cycle
WALK_DELAY = 6
WALK_LENGTH = 4

; Player animation state
player_anim_frame: byte = 0
player_anim_counter: byte = 0

; Call every game frame
update_player_animation:
    player_anim_counter = player_anim_counter + 1
    if player_anim_counter < WALK_DELAY:
        return

    player_anim_counter = 0
    player_anim_frame = player_anim_frame + 1
    if player_anim_frame >= WALK_LENGTH:
        player_anim_frame = 0

    ; Get actual sprite index from frame table
    sprite_index = walk_frames[player_anim_frame]
    set_player_sprite(sprite_index)
    return

Implementation Notes

6502:

ANIM_DELAY  = 6
ANIM_LENGTH = 4

walk_frames:
    .byte 0, 1, 2, 1      ; Frame indices

update_animation:
    inc anim_counter
    lda anim_counter
    cmp #ANIM_DELAY
    bcc .done             ; Not time yet

    lda #0
    sta anim_counter

    inc anim_frame
    lda anim_frame
    cmp #ANIM_LENGTH
    bcc .set_sprite
    lda #0
    sta anim_frame

.set_sprite:
    tax
    lda walk_frames,x     ; Get sprite index
    sta SPRITE_POINTER    ; Platform-specific
.done:
    rts

Z80:

update_animation:
    ld a,(anim_counter)
    inc a
    ld (anim_counter),a
    cp ANIM_DELAY
    ret c                 ; Not time yet

    xor a
    ld (anim_counter),a

    ld a,(anim_frame)
    inc a
    cp ANIM_LENGTH
    jr c,.set_frame
    xor a
.set_frame:
    ld (anim_frame),a

    ld hl,walk_frames
    add a,l
    ld l,a
    ld a,(hl)             ; Get sprite index
    ; ... set sprite
    ret

Trade-offs

AspectCost
CPU~20-30 cycles per frame
Memory2 bytes state + frame table
FlexibilityEasy to change speed and sequence

When to use: Character walk cycles, enemy animations, environmental effects.

When to avoid: Single-frame sprites, or when animation is tied to movement (use distance-based instead).

Variations

Speed from movement: Only advance animation when moving

if player_moving:
    update_player_animation()
else:
    player_anim_frame = 0    ; Reset to idle frame

One-shot animations: Explosions that don’t loop

update_explosion:
    if anim_frame >= EXPLOSION_LENGTH:
        return             ; Stay on last frame (or destroy)
    ; ... normal update without wrap

Variable speed: Store delay per animation

animations:
    ; delay, length, frame_data...
    .byte 6, 4, 0, 1, 2, 1    ; Walk: 6 frame delay, 4 frames
    .byte 3, 6, 0, 1, 2, 3, 4, 5  ; Run: faster, more frames

Direction-aware: Different frames for left/right

if facing_left:
    base_frame = LEFT_WALK_START
else:
    base_frame = RIGHT_WALK_START

sprite = base_frame + walk_frames[anim_frame]