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.

C64zx-spectrumAmigaNES 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

Change tile indices in OAM:

; OAM structure: Y, tile, attr, X
animate_player:
    lda anim_frame
    asl                 ; 2 tiles per frame (16x16 sprite)
    clc
    adc #PLAYER_TILE_BASE
    sta OAM_DATA+1      ; top tile
    adc #1
    sta OAM_DATA+5      ; bottom tile
    rts

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)

Some systems support horizontal flip:

  • C64: sprite X-expand, but no flip
  • NES: flip bit in OAM attribute
  • Amiga: no hardware flip for BOBs

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

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