Skip to main content
Learn how to convert PNG images to Amstrad CPC sprites and create smooth animations for your games.

Overview

DevCPC automatically converts PNG images to Z80 assembly sprite data that can be used directly in your games. The conversion handles:
  • Color palette mapping to CPC colors
  • Multiple sprite images from different PNG files
  • Automatic width/height encoding
  • Palette information for each sprite

Quick Start

1. Create Sprite PNG

Create sprites in your image editor:
  • Mode 0: Any width (even pixels), any height, max 16 colors
  • Mode 1: Width divisible by 4, any height, max 4 colors
  • Mode 2: Width divisible by 8, any height, max 2 colors

2. Configure Project

# devcpc.conf
MODE=0                           # CPC screen mode
SPRITES_PATH="assets/sprites"   # PNG directory
SPRITES_OUT_FILE="asm/sprites.asm"  # Output file

3. Place PNG Files

mkdir -p assets/sprites
cp player.png assets/sprites/
cp enemy.png assets/sprites/

4. Build

devcpc build
Sprites are automatically converted before ASM compilation.

CPC Screen Modes

Mode 0: 160x200, 16 Colors

Best for: Colorful sprites, detailed graphics, most games
MODE=0
Sprite requirements:
  • Width: Must be even (2, 4, 6, 8, 10, 12, 14, 16, …)
  • Height: Any value (typically 8, 16, 24, 32)
  • Colors: Maximum 16 from CPC palette
  • Memory: 1 byte = 2 pixels
Example sizes:
  • 16x16 sprite = 8 bytes wide × 16 lines = 128 bytes
  • 32x32 sprite = 16 bytes wide × 32 lines = 512 bytes

Mode 1: 320x200, 4 Colors

Best for: Sharp graphics, text, retro aesthetic
MODE=1
Sprite requirements:
  • Width: Divisible by 4 (4, 8, 12, 16, 20, …)
  • Height: Any value
  • Colors: Maximum 4 from CPC palette
  • Memory: 1 byte = 4 pixels
Example sizes:
  • 16x16 sprite = 4 bytes wide × 16 lines = 64 bytes
  • 32x32 sprite = 8 bytes wide × 32 lines = 256 bytes

Mode 2: 640x200, 2 Colors

Best for: High-res graphics, monochrome art
MODE=2
Sprite requirements:
  • Width: Divisible by 8 (8, 16, 24, 32, …)
  • Height: Any value
  • Colors: Maximum 2 from CPC palette
  • Memory: 1 byte = 8 pixels

PNG to ASM Conversion

Basic Conversion

# devcpc.conf
MODE=0
SPRITES_PATH="assets/sprites"
SPRITES_OUT_FILE="asm/sprites.asm"
Build output:
═══════════════════════════════════════
  Convertir Sprites PNG a ASM
═══════════════════════════════════════

ℹ Modo: 0 (160x200, 16 colores)
ℹ Entrada: assets/sprites
ℹ Salida: asm/sprites.asm

OK: asm/sprites.asm
PNGs encontrados: 3  | Convertidos: 3  | Errores: 0

Resumen:
PNG           Label      Size(px)  Bytes/line  Colors  Status
player.png    player     16x16     8           4       OK
enemy.png     enemy      16x24     8           6       OK
bullet.png    bullet     4x8       2           2       OK

✓ Sprites convertidos exitosamente

Generated ASM File

; MODE 0
; Auto-generated by DevCPC png2asm

player
;------ BEGIN IMAGE --------
  db 8   ; width in bytes
  db 16  ; height in pixels
  db 0, 0, 0, 0, 0, 0, 0, 0
  db 0, 0, 85, 85, 85, 0, 0, 0
  db 0, 85, 255, 255, 255, 85, 0, 0
  db 85, 255, 255, 255, 255, 255, 85, 0
  ; ... more data ...
;------ END IMAGE --------
  ; Palette (PEN -> INK):
  ; INK 0,0
  ; INK 1,24
  ; INK 2,6
  ; INK 3,1

enemy
;------ BEGIN IMAGE --------
  db 8   ; width in bytes  
  db 24  ; height in pixels
  ; ... sprite data ...
;------ END IMAGE --------
  ; INK 0,0
  ; INK 1,6
  ; INK 2,3

Configuration Options

Tolerance

Controls how closely PNG colors must match CPC palette:
# Exact match required
SPRITES_TOLERANCE=0

# Recommended: allows small variations
SPRITES_TOLERANCE=8

# Auto: always finds closest color
SPRITES_TOLERANCE=-1

Transparency

Set which INK represents transparent pixels:
# Use INK 0 (black) for transparency
SPRITES_TRANSPARENT_INK=0

# Use INK 26 (white) for transparency  
SPRITES_TRANSPARENT_INK=26

# No transparency (default)
SPRITES_TRANSPARENT_INK=""

Complete Configuration

MODE=0
SPRITES_PATH="assets/sprites"
SPRITES_OUT_FILE="asm/sprites.asm"
SPRITES_TOLERANCE=8
SPRITES_TRANSPARENT_INK=0

Using Sprites in 8BP

Setup Sprite

10 REM Setup sprite 0 with player image
20 |SETUPSP,0,1,4
30 REM sprite_id, image_id, num_frames
From assembly:
; Setup sprite 0
ld l, 0          ; sprite ID
ld h, 1          ; image ID  
ld d, 4          ; frames
call |SETUPSP

Position and Draw

10 x=80: y=100
20 |LOCATESP,0,y,x
30 |PRINTSP,0
From assembly:
; Position sprite
ld l, 0          ; sprite ID
ld h, 100        ; Y position
ld e, 80         ; X position
call |LOCATESP

; Draw sprite
ld l, 0
call |PRINTSP

Animation

10 REM Animate sprite with 4 frames
20 frame=0
30 |SETUPSP,0,1,4
40 FOR i=1 TO 100
50   |RECSP,0: REM Erase
60   frame=(frame+1) MOD 4
61   |SETUPSP,0,1+frame,1
70   |PRINTSP,0: REM Draw
80   FOR j=1 TO 10: NEXT j: REM Delay
90 NEXT i

Using Sprites in Pure ASM

CPCRSLIB-Style Sprite Routine

; Draw sprite to screen
; Inputs:
;   HL = sprite data address
;   DE = screen address
putsprite:
    ; Read dimensions
    ld a, (hl)       ; width in bytes
    inc hl
    ld c, a
    ld a, (hl)       ; height in pixels
    inc hl
    ld b, a
    
    ; Save width for line jumps
    ld a, c
    ld (ps_width+1), a
    
    ; Calculate line skip
    neg
    ld (ps_skip+1), a
    
ps_line_loop:
    push bc
ps_width:
    ld c, 0          ; width (modified)
ps_byte_loop:
    ld a, (hl)       ; read sprite byte
    ld (de), a       ; write to screen
    inc hl
    inc de
    dec c
    jr nz, ps_byte_loop
    
ps_skip:
    ld a, 0          ; line skip (modified)
    add a, e
    ld e, a
    jr nc, ps_no_carry
    inc d
ps_no_carry:
    
    pop bc
    djnz ps_line_loop
    ret

Using the Sprite

; Calculate screen position
ld hl, player    ; sprite data
ld de, &C000     ; screen base

; Add X offset
ld a, 40         ; X position in bytes
add a, e
ld e, a

; Add Y offset (simplified)
ld a, 100        ; Y position
ld b, a
add_y_loop:
    ld a, e
    add a, 80    ; screen width in bytes
    ld e, a
    jr nc, add_y_ok
    inc d
add_y_ok:
    djnz add_y_loop

; Draw sprite
call putsprite

player:
    READ "sprites.asm"

Creating Animation Sequences

Frame-by-Frame Animation

Create multiple PNG files:
assets/sprites/
├── player_walk_01.png
├── player_walk_02.png
├── player_walk_03.png
└── player_walk_04.png
After conversion:
player_walk_01:
    db 8, 16
    ; frame 1 data
    
player_walk_02:
    db 8, 16
    ; frame 2 data
    
player_walk_03:
    db 8, 16
    ; frame 3 data
    
player_walk_04:
    db 8, 16
    ; frame 4 data

Animation in BASIC

10 REM Walking animation
20 DEFINT a-z
30 x=80: y=100: frame=0
40 DIM frames(3)
50 frames(0)=1: frames(1)=2: frames(2)=3: frames(3)=4
60 REM Main loop
70 |RECSP,0
80 frame=(frame+1) MOD 4
90 |SETUPSP,0,frames(frame),1
100 |LOCATESP,0,y,x
110 |PRINTSP,0
120 x=x+2: IF x>150 THEN x=10
130 FOR i=1 TO 10: NEXT i
140 GOTO 70

Animation Table in ASM

; Animation data structure
anim_walk:
    db 4              ; number of frames
    dw walk_01        ; frame 1 address
    dw walk_02        ; frame 2
    dw walk_03        ; frame 3  
    dw walk_04        ; frame 4

; Animation player
play_animation:
    ld hl, anim_walk
    ld b, (hl)        ; frame count
    inc hl
    
anim_loop:
    push bc
    push hl
    
    ; Get frame address
    ld e, (hl)
    inc hl
    ld d, (hl)
    inc hl
    
    ; Calculate screen position
    ld hl, &C000 + (100*80) + 40
    
    ; Draw frame
    ex de, hl
    call putsprite
    
    ; Delay
    call wait_frames
    
    pop hl
    pop bc
    djnz anim_loop
    
    ret

wait_frames:
    ld b, 5
wf_loop:
    halt
    djnz wf_loop
    ret

Sprite Sheet Conversion

For multiple frames in one PNG:

Create Sprite Sheet

player_sheet.png: 64x16 pixels (4 frames of 16x16)
[Frame 1][Frame 2][Frame 3][Frame 4]

Manual Extraction

After conversion, sprite sheet becomes one image. You need to:
  1. Option A: Create separate PNG files (recommended)
  2. Option B: Extract frames in code:
; Sprite sheet: 64 bytes wide (4 frames of 16 bytes)
player_sheet:
    db 32, 16        ; full width, height
    ; ... data ...

; Extract frame by offsetting into data
get_frame:
    ; Input: A = frame number (0-3)
    ; Output: HL = frame data
    ld hl, player_sheet + 2  ; skip dimensions
    
    ld b, a
    ld a, b
    or a
    ret z            ; frame 0
    
    ld de, 8         ; frame width in bytes
get_frame_loop:
    add hl, de
    djnz get_frame_loop
    ret

Masked Sprites (Transparency)

Creating Mask

For true transparency, create a mask sprite:
player.png       - Color sprite
player_mask.png  - Black/white mask (black = transparent)

Drawing Masked Sprite

; Draw masked sprite
; HL = sprite data
; DE = mask data  
; BC = screen address
draw_masked:
    push bc          ; save screen address
    
    ; Draw mask with AND
    ex de, hl        ; HL = mask
    pop de           ; DE = screen
    push de
    call draw_and
    
    ; Draw sprite with OR
    pop de           ; DE = screen
    call draw_or
    
    ret

draw_and:
    ; AND mask with screen
    ld a, (hl)
    inc hl
    ld b, a          ; width
    ld a, (hl)
    inc hl
    ld c, a          ; height
    
and_loop:
    push bc
    ld b, 0          ; width in B
and_byte_loop:
    ld a, (hl)
    and (de)         ; AND with screen
    ld (de), a
    inc hl
    inc de
    djnz and_byte_loop
    
    ; Next line
    ; (add line skip logic)
    
    pop bc
    dec c
    jr nz, and_loop
    ret

draw_or:
    ; Similar but with OR
    ; ...

Optimizing Sprite Performance

1. Pre-shift Sprites

For smooth horizontal scrolling:
# Create 8 versions, each shifted 1 pixel
player_shift0.png
player_shift1.png
...
player_shift7.png
In code:
; Select pre-shifted version
ld a, (x_pixel)  ; fine X position (0-7)
ld hl, shift_table
add a, l
ld l, a
ld a, (hl)       ; get sprite address

2. Unrolled Drawing

For fixed-size sprites:
; Unrolled 16x16 sprite (Mode 0, 8 bytes wide)
draw_16x16:
    ; Line 1
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    ld a, (hl): ld (de), a: inc hl: inc de
    
    ; Calculate next line address
    ld a, e
    add a, 72        ; 80 - 8 = next line
    ld e, a
    jr nc, line2
    inc d
line2:
    ; Line 2
    ; ... repeat ...

3. Screen Address Lookup Table

; Pre-calculate line addresses
screen_lines:
    dw &C000         ; line 0
    dw &C050         ; line 1
    dw &C0A0         ; line 2
    ; ... 200 lines ...

; Fast Y positioning
get_screen_line:
    ; Input: A = Y position
    ; Output: HL = screen line address
    add a, a         ; * 2 (word table)
    ld l, a
    ld h, 0
    ld de, screen_lines
    add hl, de
    ld a, (hl)
    inc hl
    ld h, (hl)
    ld l, a
    ret

Color Considerations

CPC Palette

Always use these exact RGB values:
INK 0  = (0,0,0)         Black
INK 6  = (255,0,0)       Red
INK 24 = (255,255,0)     Yellow  
INK 26 = (255,255,255)   White

Dithering

For gradient effects:
Light to dark using 2 colors:
░░░░ (25% color A)
░▒░▒ (50% mix)
▒▒▒▒ (75% color A)
████ (100% color A)

Palette Cycling

Animate without redrawing:
10 REM Draw sprite once
20 |SETUPSP,0,1,1
30 |LOCATESP,0,100,80
40 |PRINTSP,0
50 REM Cycle colors
60 FOR i=1 TO 26
70   INK 1,i
80   FOR j=1 TO 10: NEXT j
90 NEXT i

Troubleshooting

”Width not divisible by X”

Problem: PNG width doesn’t match mode requirements Solution:
  • Mode 0: Make width even (2, 4, 6, 8, …)
  • Mode 1: Make width divisible by 4
  • Mode 2: Make width divisible by 8

”Too many colors”

Problem: PNG uses more colors than mode allows Solution:
  1. Reduce colors in image editor
  2. Convert to indexed color mode
  3. Use only CPC palette colors
  4. Change to Mode 0 (16 colors)

“Color not in CPC palette”

Problem: Color doesn’t match CPC palette Solution:
# Increase tolerance
SPRITES_TOLERANCE=16

# Or use auto mode
SPRITES_TOLERANCE=-1

Sprite Flickers

Problem: Sprite flickers when moving Solution:
10 REM Erase old position first
20 |RECSP,0
30 REM Move sprite
40 x=x+2
50 |LOCATESP,0,y,x
60 REM Draw new position
70 |PRINTSP,0

Best Practices

1. Consistent Sizes

Use standard sizes:
8x8, 16x16, 24x24, 32x32

2. Power-of-2 Frames

Animation frames: 2, 4, 8, 16
(easier to loop with AND mask)

3. Organize by Purpose

assets/sprites/
├── player/
│   ├── idle.png
│   ├── walk1.png
│   └── walk2.png
├── enemies/
│   ├── enemy1.png
│   └── enemy2.png
└── items/
    ├── coin.png
    └── powerup.png

4. Name with Frame Numbers

Good:
  player_walk_01.png
  player_walk_02.png
  player_walk_03.png

Bad:
  walk1.png
  walk_frame_2.png
  player-walk-3.png

Complete Example

See 8BP Game Example for a full project with animated sprites.

Build docs developers (and LLMs) love