• Skip to main content
  • Skip to header right navigation
  • Skip to site footer
Retro Game Coders

Retro Game Coders

Retro computer/console game + dev community

  • About
    • Retro Computer Collection
    • Contact
  • Blog
  • Retro Resources
    • Retro Gaming Timeline
    • Online Retro IDE
    • Retro Pixel Art Editor
    • Dungeon Loom Map Editor
    • 6502 Programmer’s Reference
    • Emulators
      • Acorn Electron
      • Amstrad CPC Emulator
      • Online BBC Micro Emulator
      • Commodore PET Emulator
      • Browser C64 Emulator
      • DOSBox/DOS PC emulator
      • Tandy CoCo/Dragon
    • Best Retro YouTube Channels
    • New Retro Books
    • Raspberry Pi Amiga Emulation
    • MiSTer FPGA Tutorial
    • BMC64 C64 Pi
  • Community

Home » Retro Game Coders Blog » Programming

How C64 Sprites Work

c64 sprites

How to use sprites in our C64 games, in BASIC, XC-BASIC3, and UGBASIC

The C64 had many things going for it that made it the best-selling home micro of the 1980s, lasting into the 1990s, but I think the inclusion of hardware sprites was crucial in it being seen as a games machine, and garnering support from commercial game developers.

It’s a big enough deal that I thought I would sit down and write about how they work under the hood, and how we as modern retro game developers in the current year and beyond can use them in our games, especially now I have added XC-BASIC and UGBASIC to my online IDE!


No matter which language or toolchain you use, all C64 sprites are handled behind the scenes by the VIC-II graphics chip. BASIC, XC-BASIC3 and ugbasic are all just different ways of configuring the same hardware.

Join Retro Game Coders Community
Join the Retro Game Coders Community

C64 Sprite Definitions

Creating sprites can be done easily using an online tool such as petscii.krissz.hu (yep, it does sprites as well as PETSCII) or a desktop tool such as Spritepad.

But how is the data structured, and how is that translated into what you see on screen?

A C64 hardware sprite has a fixed format:

  • 24 pixels wide
  • 21 pixels tall
  • Stored as exactly 64 bytes in RAM

The VIC-II does not know about variables, arrays, DATA statements, or images. It only knows how to fetch 64 bytes from the specified area in memory and render them into pixels on screen every video frame.

To do that, it relies on two things:

  1. Sprite pointers
  2. The currently active VIC memory bank

Understanding those two ideas explains almost every sprite bug you will ever hit on the C64.


You can now follow the tutorials and edit the code right in your web browser with the Online Retro IDE

– No downloads, configuration, etc necessary, and it is free!

Sprite pointers Versus Sprites in Memory

I have to admit, memory addressing is a key area I often get confused. Math is not my strong suit, but as you will see the C64 makes the math even more ‘fun’ than usual.

The VIC-II has eight sprite pointer registers located at 2040-2047

One byte per sprite.

Each pointer does not store an address. Instead, it stores a block number. A block is always 64 bytes long.

The rule is:

sprite address = pointer value × 64

So if you write:

POKE 2040,192

The VIC-II will fetch sprite 0 data starting at:

192 × 64 = 12288 ($3000)

From that address it reads 64 bytes, once per frame, forever.

There is no safety checking. If anything else writes to that memory, the sprite instantly changes shape.

This is why sprite corruption often looks random. The VIC-II is faithfully displaying whatever trash data happens to be there.


VIC Memory and Banking

The C64 has 64 KB of RAM, but the VIC-II can only “see” 16 KB of it at any one time. Which 16 KB it sees is controlled by a hardware register in the CIA chip.

These 16 KB regions are called VIC banks:

BankAddress range
0$0000 to $3FFF
1$4000 to $7FFF
2$8000 to $BFFF
3$C000 to $FFFF

By default, the machine starts in bank 0. That means all sprite pointers are relative to $0000–$3FFF.

Within that range, some areas are dangerous:

  • $0400 to $07E7 is screen memory
  • Parts of low memory are used by BASIC and the OS

If you place sprite data in or around screen memory, it is likely to be overwritten as soon as you add text.

This is exactly what happened in my early versions of the XC-BASIC3 example below. The first sprite landed just below screen RAM and survived. The second sprite landed exactly at $0400 and was immediately overwritten by screen output!

Moving the sprite data to $3000 fixed the problem:

  • It is inside the active VIC bank
  • It is not used by screen RAM
  • It is not used by BASIC variables

This rule applies no matter what language you use. In a battle between you and the hardware, the VIC-II always wins.


💬 Questions or comments? Head over to the community to discuss!

Multicolour Sprites

In single-color mode, each sprite pixel is 1 bit. One byte represents 8 pixels. Easy.

In multicolor mode, each pixel is 2 bits. That changes everything:

  • Each byte now represents 4 pixels
  • Pixels are twice as wide on screen
  • Each sprite row is exactly 3 bytes
  • 21 rows × 3 bytes = 63 bytes
  • Plus 1 unused padding byte = 64 bytes total

Each 2-bit pixel value means:

  • 00 transparent
  • 01 sprite’s own specific colour
  • 10 colour from multicolour register $D025
  • 11 colour from multicolour register $D026

Yeah, the C64 was a lot more limited when it came to coloured graphics than some competitors without employing some sneaky tricks. It makes me respect the pixel artists and demo coders who push these limits even more.

If multicolour mode is not enabled in the VIC-II register $D01C, the same data is interpreted as single-colour bits, producing vertical stripes and corrupted shapes.

VIC-II Memory Map
(Bank 0, default)

$0000 ───────────────────────────────────────────
Zero page, system vectors, OS work areas
(used by KERNAL, BASIC, IRQs, etc.)

$0200 ───────────────────────────────────────────
BASIC workspace, variables, program data
(XC-BASIC program image also lives around here)

$03C0 ───────────────────────────────────────────
Often used in simple demos for ONE sprite
(safe only if nothing else grows into it)

$0400 ─────────────────────────────────────────── ← SCREEN RAM START
Screen character matrix (40 × 25 = 1000 bytes)
PRINT, TEXTAT, cursor movement all write here

$07E8 ─────────────────────────────────────────── ← SCREEN RAM END
Colour RAM lives elsewhere ($D800+)

$0800 ───────────────────────────────────────────
Free / mixed use depending on program
(easy to collide with BASIC or XC-BASIC data)

$1000 ───────────────────────────────────────────
More free RAM, still risky without planning

$2000 ───────────────────────────────────────────
Common location for graphics or character sets
(still inside VIC bank 0)

$3000 ─────────────────────────────────────────── ← SAFE SPRITE DATA
Recommended sprite storage
• inside VIC bank
• not screen RAM
• not used by BASIC

Sprite 0 @ $3000 (pointer = 192)
Sprite 1 @ $3040 (pointer = 193)
Sprite 2 @ $3080 (pointer = 194)
etc.

$3FFF ─────────────────────────────────────────── ← VIC BANK 0 END

Sprites in Regular C64 BASIC

In C64 BASIC, everything is explicit:

  • Sprite data is manually POKEd into memory
  • Sprite pointers are manually set
  • Sprite positions are manually updated
  • Memory placement is entirely your responsibility

Because BASIC has no compiler, the DATA statements are read at runtime and written directly into RAM. That makes it easier to see what is happening, but also easier to overwrite things accidentally.

The program works because it carefully places sprite data at pointer 192, which is $3000 (12288 decimal) and nearby addresses and continues to set the other pointers.

C64 BASIC Code

Load an animate an inexpertly drawn tribute to the classic space invader sprite

Here is code to display and move my tribute to the original Taito Space Invader on screen.

Check out a full playable demo online in your web browser.

0 REM ==================
1 REM SINGLE SPRITE DEMO
2 REM ==================
3 REM
4 REM CLEAR SCREEN/BORDER (BLACK), TEXT WHITE
5 POKE 53280,0: POKE 53281,0: POKE 646, 1: PRINT CHR$(147) 
6 REM SPRITE COLOR = GREEN
10 POKE 53287, 5
11 POKE 53248+21, 1   : REM enable sprite 0
12 POKE 2040,192      : REM sprite 0 data slot
19 REM LOAD DATA
20 FOR N = 0 TO 62: READ B: POKE 12288+N,B: NEXT
30 FOR X = 0 TO 255: Y=100 + INT(16 * SIN(X / 3.14))
40 POKE 53248,X: REM INVADER X COORDINATE
50 POKE 53249,Y: REM INVADER Y COORDINATE
60 NEXT X
70 GOTO 30
1000 REM SPRITE 0
1001 DATA 0,0,0
1002 DATA 6,0,96
1003 DATA 6,0,96
1004 DATA 1,129,128
1005 DATA 1,129,128
1006 DATA 7,255,224
1007 DATA 7,255,224
1008 DATA 30,126,120
1009 DATA 30,126,120
1010 DATA 127,255,254
1011 DATA 127,255,254
1012 DATA 103,255,230
1013 DATA 103,255,230
1014 DATA 102,0,102
1015 DATA 102,0,102
1016 DATA 1,231,128
1017 DATA 1,231,128
1018 DATA 0,0,0
1019 DATA 0,0,0
1020 DATA 0,0,0
1021 DATA 0,0,0


How things Change in XC-BASIC3

XC-BASIC3 is compiled, not interpreted. That changes where sprite data starts life.

When we write the following in the code below:

ballsprite:
DATA AS BYTE ...

Those bytes are stored inside the compiled program data, not yet in sprite memory, so our VIC-II cannot see them there. That is why we must do:

memcpy @ballsprite, destination, 64

The @ballsprite syntax provides the address of the data inside the program. memcpy then copies it into VIC-visible RAM where the sprite pointer can reach it.

XC-BASIC3 helps with sprite positioning and colour setup, but it does not manage memory layout for you. You still have to:

  • Choose a safe address
  • Stay inside the active VIC bank
  • Set the sprite pointers yourself
  • Enable multicolor mode in hardware

This is closer to low-level C64 programming than it first appears, which makes XC-BASIC3 a good bridge between BASIC and assembly.

XC-BASIC3 Bat and Ball Game

XC-BASIC3 Bat and Ball Game Sprites Demo

This is a bat and ball game in the style of the classic Pong. Use WASD for the keys. We have multicolour sprites for the ball and a bat, and if you miss the ball it will bounce off the edges of the playable screen area.

Check out the code and have a play in the online IDE.

REM constants for key scan codes and initializations
CONST W = 64770 ' scan code
CONST A = 64772 ' scan code
CONST S = 64800 ' scan code
CONST D = 64260 ' scan code
CONST ESC = 32640 ' scan code
DIM a$ AS STRING * 1
DIM x,y AS BYTE FAST
DIM ballx,bally AS BYTE FAST
DIM xdir,ydir AS INT FAST
DIM cx,cy AS BYTE

REM screen colours
BACKGROUND 0
BORDER 0
PRINT CHR$(147)

' Wall/HUD background
FOR cy = 0 TO 24
  FOR cx = 30 TO 39
    TEXTAT cx, cy, CHR$(230), 5
  NEXT cx
NEXT cy

REM store sprite shapes in safe VIC-visible RAM
CONST SHAPES_START = $3000        : REM 12288
memcpy @ballsprite, SHAPES_START, 64
memcpy @batsprite,  SHAPES_START+64, 64

REM compiler multicolor flags
SPRITE MULTICOLOR 1, 1

REM multicolor settings from sprite editor
POKE $D025, 12        : REM multicolor #1
POKE $D026, 11        : REM multicolor #2

REM set sprite colors and placement
SPRITE 0 COLOR 15 MULTI ON BACKGROUND
SPRITE 1 COLOR 15 MULTI ON BACKGROUND

REM hardware multicolor enable (sprites 0 and 1)
POKE $D01C, %00000011

REM allow keys to repeat when held down
POKE 650,128

REM title text
TEXTAT 5,10, "bat and ball demo", 1
TEXTAT 5,12, " keys w, a, s, d", 1


REM set sprite pointers for $3000 and $3000+64
POKE 2040, SHAPES_START/64        : REM = 192
POKE 2041, (SHAPES_START+64)/64   : REM = 193

REM initial positions and directions
x=100
y=110
ballx=70
bally=70
xdir=-1
ydir=-1
SPRITE 1 ON AT x,y
SPRITE 0 ON AT 120,120
SPRITE CLEAR HIT

REM Keyboard control and game loop
DO WHILE NOT KEY(ESC)

  REM check keys and move bat
  IF KEY(A) THEN x=x-2
  IF KEY(D) THEN x=x+2
  IF KEY(W) THEN y=y-2
  IF KEY(S) THEN y=y+2

  REM bounce ball off bat and walls
  IF (bally+10) > y AND ballx >= x-8 AND ballx <= x+8 AND ydir>0 THEN ydir=-ydir
  IF ballx=240 THEN xdir=-xdir
  IF ballx=0 THEN xdir=-xdir
  IF bally=240 THEN ydir=-ydir
  IF bally=40 THEN ydir=-ydir

  REM update ball position
  ballx=ballx+xdir
  bally=bally+ydir

  REM draw sprites at new positions after raster wait
  WAIT 53265, 128
  SPRITE 1 AT x,y
  SPRITE 0 AT ballx,bally

LOOP
END

REM sprite data exported from sprite editor
ballsprite:
DATA AS BYTE _
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,21,0, _
0,85,64, 1,105,80, 1,169,80, 5,169,84, _
5,165,244, 5,85,244, 5,87,244, 1,87,208, _
1,223,80, 0,125,64, 0,21,0, _
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0

batsprite:
DATA AS BYTE _
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, _
0,0,0, 0,0,0, _
5,85,80, _
26,170,164, _
105,85,91, _
101,85,87, _
85,85,87, _
101,221,223, _
87,119,127, _
63,255,252, _
15,255,240, _
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0   : REM final padding byte so total = 64

Enter UGBASIC

ugbasic lives at a higher, more abstracted level than XC-BASIC.

Instead of asking you where sprite memory lives, it works in terms of image files, atlases and sprites. Internally, it still:

  • Allocates sprite memory
  • Copies pixel data into RAM
  • Sets sprite pointers
  • Configures VIC registers

But it does much of this for you.

That makes ugbasic more abstracted and potentially faster to get results with, plus it makes it harder to accidentally corrupt memory. The trade-off is that the memory model is mostly hidden, so you do not see the direct relationship between sprite data, pointers and VIC banks unless you go looking for it.

Many retro developers like to micro (heh) manage data usage to squeeze every last available byte, so keep that in mind.

All three approaches ultimately drive the same eight hardware sprites, however. The difference is how much of the VIC-II you are asked to understand up front.

UGBASIC Christmas Demo

More festive fun with another Christmas themed sprite demo (again, click through to see it running in the online IDE). Can you guess how the final game I am developing using these sprites will work? …

BITMAP ENABLE(2)
DEFINE MSPRITE ASYNC
COLOR BORDER WHITE
CLS WHITE

xmas := LOAD IMAGE("xmas.png") 
snowpic := LOAD IMAGE("snow.png") 
elfpic := LOAD IMAGE("elf.png") EXACT

CONST limitx = SCREEN WIDTH - SPRITE WIDTH

RANDOMIZE

FOR idx = 1 TO 25
  x=RND(320-IMAGE WIDTH(snowpic))
  y=RND(75)+75
  PUT IMAGE snowpic AT x,y WITH TRANSPARENCY
NEXT

PUT IMAGE elfpic AT 200, 50 WITH TRANSPARENCY
PUT IMAGE xmas AT 80, 0 WITH TRANSPARENCY

sprites := LOAD ATLAS("xmas_sprites2.png") FRAME SIZE (24 , 24) EXACT
sprite1 = MSPRITE( IMAGE( sprites FRAME 1 ) )
sprite2 = MSPRITE( IMAGE( sprites FRAME 2 ) )
sprite3 = MSPRITE( IMAGE( sprites FRAME 4 ) )

DO

  SPRITE sprite1 ENABLE AT elfx+SCREEN BORDER X, 21+elfy+SCREEN BORDER Y    
  SPRITE sprite2 ENABLE AT SCREEN BORDER X+limitx-elfx, 60+elfy+SCREEN BORDER Y    
  SPRITE sprite3 ENABLE AT SCREEN BORDER X+elfx, 90+elfy+SCREEN BORDER Y    
  elfx=elfx+1
  IF elfx > limitx THEN    
  elfx = 0
  ENDIF

  MSPRITE UPDATE
LOOP
Category: ProgrammingTag: basic programming, Commodore 64 (C64)
Previous Post:ZX Spectrum Next on the InternetZX Spectrum Next on the Internet: Xberry Pi ESP01 and Pi Zero Upgrades
Next Post:Introduction to Programming the Commodore PETProgramming the Commodore PET

Retro Game Coders

Retro computer/console game + dev programming community by Chris Garrett

  • Bluesky
  • Threads
  • Facebook
  • Instagram
  • YouTube
  • Mastodon

Maker Hacks ・ D6Combat・chrisg.com

© Copyright 2026 Chris Garrett

Privacy ﹒ Terms of Service

Return to top