Skip to content
Techniques & Technology

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.

commodore-64sinclair-zx-spectrumcommodore-amiganintendo-entertainment-system graphicsanimationfundamentals 1978–present

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

TermMeaning
Frame rateHow often the screen refreshes (50/60 Hz)
Animation rateHow 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+n is a single byte — one write swaps a 64-byte sprite atomically.
  • Stage-and-DMA: on NES, build the next OAM frame in a $0200-$02FF shadow 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

TechniqueSavingTrade-off
Share framesReuse idle frame in walkMay look stiff
Reduce frames4 instead of 8Less smooth
Smaller sprites16×16 vs 24×21Less detail
Palette swapRecolour same framesCharacters look similar

See also