Code status: the maintained Z80 routine is CPU-executed; the other examples are illustrative. The Z80 check includes a table spanning a 256-byte boundary. The 6502 renderer adapter and language-neutral variants need their own caller and target checks.
Audit execution record · Scope and remaining checks
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 ; Elapsed updates since the last animation step
; 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
returnPseudocode
; 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)
returnImplementation 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:
rtsZ80:
Z80 animation state
; Pasmo syntax. ANIM_DELAY and ANIM_LENGTH are constants in1.animation_255.
; Caller allocates anim_counter, anim_frame and sprite_index as writable bytes.
; Initialise counters to0 and sprite_index to walk_frames[0] on state entry.
; walk_frames has ANIM_LENGTH bytes and remains readable for all calls.
; One call per chosen update. Changes AF/DE/HL; preserves BC/IX/IY.
; A=new sprite_index only when the delay expires; use the state byte to draw.
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,animation_set_frame
xor a
animation_set_frame:
ld (anim_frame),a
ld e,a
ld d,0
ld hl,walk_frames
add hl,de ; propagate carry when the table crosses a page
ld a,(hl) ; Get sprite index
ld (sprite_index),a ; renderer consumes this logical image number
retSupply ANIM_DELAY and ANIM_LENGTH in 1–255, a readable walk_frames table of that length, and writable bytes anim_frame, anim_counter and sprite_index. Start both counters at zero and put walk_frames[0] in sprite_index. Call once per chosen update; the renderer reads sprite_index. The routine changes AF/DE/HL and preserves BC/IX/IY. The whole address addition matters: a table may cross a 256-byte boundary.
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | A counter update on waiting calls; table lookup and renderer work on a change. Measure the actual target implementation. |
| Memory | 2 bytes state + frame table |
| Flexibility | Easy 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 frameOne-shot animations: Explosions that don’t loop
update_explosion:
if anim_frame >= EXPLOSION_LENGTH:
return ; Stay on last frame (or destroy)
; ... normal update without wrapVariable 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 framesDirection-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]State-driven animation
For characters with multiple animations (idle, walk, run, jump, attack), pair the animation pattern with a state machine — each state has its own frame table and delay:
; Descriptors have a delay and a reference to their own frame sequence.
animations[IDLE] = { delay:16, frames:[0,1] }
animations[WALK] = { delay:6, frames:[2,3,4,3] }
animations[RUN] = { delay:3, frames:[5,6,7,8,9,8] }
animations[JUMP] = { delay:1, frames:[10] }
change_animation(next_state):
player_anim_state = next_state
anim_frame = 0
anim_counter = 0
sprite_index = animations[next_state].frames[0]
update_animation:
descriptor = animations[player_anim_state]
delay = descriptor.delay
length = LENGTH(descriptor.frames)
; Apply the counter/step logic above using this descriptor.A compact machine representation can use a table of pointers to variable-length records. Multiplying the state by one fixed record size only works when every record has that size. Here the frame sequences have different lengths. Validate state, length and delay before choosing a record.
When the state changes (e.g. player presses jump), reset both counters and publish the first image immediately. Without this, switching from walk to run might briefly show the wrong frame index.
Explore the cross-platform sprite family for software drawing, hardware limits, restoration and overload policies.