Sprite Animation
Bringing pixels to life
Frame-by-frame sprite animation creates the illusion of movement by cycling through carefully designed images at controlled intervals.
Overview
Animation is the rapid display of slightly different images. For sprites, this means cycling through a sequence of frames: a walk cycle might use 4-8 frames, shown in sequence as the character moves. The timing between frames and the smoothness of the sequence determine whether animation feels fluid or jerky.
Animation fundamentals
Frame rate vs animation rate
| Term | Meaning |
|---|---|
| Frame rate | How often the screen refreshes (50/60 Hz) |
| Animation rate | How often the sprite image changes |
Sprite animation is typically slower than screen refresh:
- 50 fps screen refresh
- Character animation at 10 fps (every 5 screen frames)
Animation timing
anim_timer: .byte 0
anim_frame: .byte 0
ANIM_SPEED = 6 ; frames between changes
update_animation:
inc anim_timer
lda anim_timer
cmp #ANIM_SPEED
bcc .no_change
lda #0
sta anim_timer
; Advance animation frame
inc anim_frame
lda anim_frame
cmp #NUM_FRAMES
bcc .no_wrap
lda #0
sta anim_frame
.no_wrap:
.no_change:
rts
Walk cycle basics
A typical walk cycle:
Frame 1: Standing (contact)
Frame 2: Passing (one leg forward)
Frame 3: Contact (other leg)
Frame 4: Passing (first leg forward)
Four frames is minimum; 6-8 frames feels smoother.
Platform implementations
Commodore 64
Sprites use 64 bytes each. Animation swaps sprite pointers:
; Sprite pointers at $07F8-$07FF
SPRITE_PTRS = $07f8
; Animation frames stored at $2000, $2040, $2080...
walk_frames:
.byte $80, $81, $82, $83 ; pointers / 64
animate_player:
ldx anim_frame
lda walk_frames,x
sta SPRITE_PTRS ; sprite 0 pointer
rts
ZX Spectrum
Software sprites require copying new frame data:
; Copy animation frame to sprite buffer
animate_sprite:
ld a, (anim_frame)
ld l, a
ld h, 0
add hl, hl ; ×2
add hl, hl ; ×4
add hl, hl ; ×8 (8 bytes per frame)
ld de, frame_data
add hl, de ; hl = frame address
ld de, sprite_buffer
ld bc, 8
ldir
ret
NES
NES sprites are 8×8 or 8×16 (controlled globally by PPUCTRL bit 5). A 16×16 character is built from multiple OAM entries — typically 4 sprites in 8×8 mode (TL/TR/BL/BR) or 2 sprites side-by-side in 8×16 mode. Animation rewrites the tile-index byte of each OAM entry; the OAM buffer is in CPU RAM (commonly $0200-$02FF) and pushed to PPU OAM via $4014 DMA each VBlank.
; OAM structure (4 bytes per sprite): Y, tile, attr, X
; A 16x16 character built from 4 hardware sprites in 8x8 mode.
; Each animation frame uses 4 sequential tiles in CHR, so frame N
; starts at tile (PLAYER_TILE_BASE + N*4).
animate_player:
lda anim_frame
asl
asl ; * 4 -> tile offset for this frame
clc
adc #PLAYER_TILE_BASE
sta OAM_DATA+1 ; sprite 0 tile (top-left)
clc
adc #1
sta OAM_DATA+5 ; sprite 1 tile (top-right)
clc
adc #1
sta OAM_DATA+9 ; sprite 2 tile (bottom-left)
clc
adc #1
sta OAM_DATA+13 ; sprite 3 tile (bottom-right)
rts
In 8×16 mode the layout collapses to two OAM entries (left and right halves); the hardware fetches each sprite's two tiles from a pair starting at the tile field's index. There the per-frame stride is 2 tiles × 2 sprites = 4, so the same * 4 arithmetic applies — but you only update OAM_DATA+1 and OAM_DATA+5, with +2 between them rather than +1.
Amiga
BOBs require redrawing with new image data. Sprites update data registers:
animate_sprite:
move.w anim_frame,d0
lsl.w #2,d0 ; ×4 for pointer table offset
lea anim_table,a0
move.l (a0,d0.w),a1 ; get frame address
; Copy to sprite data area or update copper list
State-based animation
Different actions need different animations:
; Animation states
ANIM_IDLE = 0
ANIM_WALK = 1
ANIM_JUMP = 2
ANIM_ATTACK = 3
anim_state: .byte ANIM_IDLE
get_frame_table:
lda anim_state
asl
tax
lda anim_tables,x
sta ptr
lda anim_tables+1,x
sta ptr+1
rts
anim_tables:
.word idle_frames
.word walk_frames
.word jump_frames
.word attack_frames
Directional animation
Characters facing left/right:
Mirrored sprites (hardware)
Hardware horizontal/vertical flip support varies:
- C64: no flip in hardware. Sprites have X- and Y-expand bits (
$D01D/$D017) but no mirror. Trick: store a pre-mirrored copy of the sprite alongside the original and swap the sprite data pointer ($07F8-$07FF) when the character turns. - NES: flip bits in OAM byte 2 — bit 6 = vertical flip, bit 7 = horizontal flip. Free at the hardware level; no extra tile data.
- Amiga (hardware sprites): no flip. The sprite DMA always reads forwards.
- Amiga (BOBs): the Blitter has no built-in flip; you either pre-flip the source bitmap, or use the Blitter's descending mode (BLTCON1 DESC bit) to copy backwards — that mirrors vertically, but horizontal mirror still needs pre-built mirrored data.
Separate frames
Store left and right versions:
; 4 walk frames × 2 directions = 8 frames
walk_right: .byte $80, $81, $82, $83
walk_left: .byte $84, $85, $86, $87
get_walk_frame:
lda facing_right
bne .right
lda walk_left,x
rts
.right:
lda walk_right,x
rts
Animation speed variation
Match animation speed to movement speed:
; Walking = slower animation
; Running = faster animation
update_walk_animation:
lda is_running
bne .running
lda #8 ; slow
jmp .set_speed
.running:
lda #4 ; fast
.set_speed:
sta anim_speed
rts
One-shot animations
For attacks, jumps, or deaths—play once, don't loop:
update_oneshot:
lda anim_timer
beq .done ; already finished
dec anim_timer
bne .no_advance
inc anim_frame
lda anim_frame
cmp #ONESHOT_FRAMES
bcc .not_done
; Animation complete
lda #0
sta anim_timer
sta anim_frame
jsr return_to_idle
rts
.not_done:
lda #ANIM_SPEED
sta anim_timer
.no_advance:
.done:
rts
Frame double-buffering
If a sprite's animation data lives in main RAM (C64 sprite slots, Amiga BOB sources, Spectrum software-sprite buffers), copying the next frame while it is on screen can tear or flash. Two patterns avoid this:
- Double-buffered sprite data: keep two slots, draw into the inactive slot, switch the pointer atomically (during VBlank, or by writing the pointer once). The C64 lends itself to this because the sprite data pointer at
$07F8+nis a single byte — one write swaps a 64-byte sprite atomically. - Stage-and-DMA: on NES, build the next OAM frame in a
$0200-$02FFshadow buffer during the active frame, then push the whole 256 bytes via OAM DMA (STA $4014) at the start of VBlank. The DMA is atomic from the PPU's perspective.
Memory optimisation
| Technique | Saving | Trade-off |
|---|---|---|
| Share frames | Reuse idle frame in walk | May look stiff |
| Reduce frames | 4 instead of 8 | Less smooth |
| Smaller sprites | 16×16 vs 24×21 | Less detail |
| Palette swap | Recolour same frames | Characters look similar |