I came across a C64 BASIC game that ran pretty slow, so I challenged myself to see how much I could speed it up.
Today’s C64 BASIC optimisations are around making a pong-style bat and ball game from Tom‘s online tutorial play a bit better.
You can see the before and after below, with only minor alterations on the original to get it to run outside of Tom’s interactive tutorial on regular C64 and emulators:
Before and after optimising the BASIC code
Original Code
1000 REM STATE INITIALIZATION
1010 X=0
1020 Y=0
1030 DX=1
1040 DY=1
1050 P=10
1100 PRINT CHR$(147)
2000 REM BALL MOVEMENT
2010 POKE ((Y*40)+X) CHR$(32)
2020 X=X+DX
203...
I came across a C64 BASIC game that ran pretty slow, so I challenged myself to see how much I could speed it up.
Today’s C64 BASIC optimisations are around making a pong-style bat and ball game from Tom‘s online tutorial play a bit better.
You can see the before and after below, with only minor alterations on the original to get it to run outside of Tom’s interactive tutorial on regular C64 and emulators:
Before and after optimising the BASIC code
Original Code
1000 REM STATE INITIALIZATION
1010 X=0
1020 Y=0
1030 DX=1
1040 DY=1
1050 P=10
1100 PRINT CHR$(147)
2000 REM BALL MOVEMENT
2010 POKE ((Y*40)+X) CHR$(32)
2020 X=X+DX
2030 Y=Y+DY
2031 IF (X=0) AND (Y<P) THEN GOTO 3000
2032 IF (X=0) AND (Y>(P+4)) THEN GOTO 3000
2040 IF X=40 THEN DX=-1
2050 IF X=40 THEN X=38
2060 IF X<1 THEN DX=1
2070 IF X<1 THEN X=2
2080 IF Y=25 THEN DY=-1
2090 IF Y=25 THEN Y=23
2100 IF Y<0 THEN DY=1
2110 IF Y<0 THEN Y=2
2200 POKE ((Y*40)+X) CHR$(209)
2500 REM MOVING A PADDLE
2510 K$=""
2520 K=0
2530 GET$ K$
2540 IF K$<>"" THEN K=ASC(K$)
2550 IF K=145 THEN P=P-1
2560 IF K=17 THEN P=P+1
2561 IF P<0 THEN P=0
2562 IF P>20 THEN P=20
2570 IF P>0 THEN POKE ((P-1)*40) CHR$(32)
2571 POKE ((P+0)*40) CHR$(182)
2572 POKE ((P+1)*40) CHR$(182)
2573 POKE ((P+2)*40) CHR$(182)
2574 POKE ((P+3)*40) CHR$(182)
2575 POKE ((P+4)*40) CHR$(182)
2576 IF P<20 THEN POKE ((P+5)*40) CHR$(32)
2580 GOTO 2000
3000 REM GAME OVER
3010 PRINT CHR$(147);
3020 S=0
3030 S=S+1
3040 PRINT " GAME OVER"
3050 IF S<25 THEN GOTO 3030
3060 PRINT CHR$(147)
If you look closely you can see there are a couple of issues with the original code as it was developed to run within Tom’s embedded BASIC interpreter, not on a C64.
First, the C64 starts addressing screen memory at 1024, so to make this work any of the screen pokes need 1024 added.
Secondly, in C64 BASIC we can not poke using CHR$, so that would need to be changed also.
Regardless, the code was of course written with clarity in mind rather than prioritising speed. This means we have some opportunities to make it run better, even under interpreted BASIC.
My Version
Remember anywhere I use {} these are for my online developer environment or a desktop IDE that understands these tokens, otherwise you need to replace them with the appropriate key on the C64 keyboard, and {13} is the Return key.
1000 REM INIT
1005 P$=" {DOWN}{LEFT}"+CHR$(208)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(250)+"{DOWN}{LEFT} "
1010 X=10: Y=10: DX=1: DY=1: P=10: POKE 211, 0
1100 PRINT CHR$(147)
2000 REM GAME LOOP
2010 X=X+DX: Y=Y+DY
2031 IF X=0 THEN IF Y<P OR Y>(P+4) THEN GOTO 3000
2040 IF X=40 OR X=0 THEN DX=DX*-1: X=X+DX
2080 IF Y=25 OR Y<0 THEN DY=DY*-1: Y=Y+DY
2200 OB=B: POKE OB, 32: B=1024+(Y*40)+X: POKE B,87
2500 REM PADDLE
2510 GET K$:
2520 IF K$="Q" THEN OP=P: P=P-1
2530 IF K$="A" THEN OP=P: P=P+1
2561 IF P<0 OR P=18 THEN P=OP
2571 POKE 214, P: SYS 58732: PRINT P$: GOTO 2000
3000 REM GAME OVER
3010 PRINT CHR$(147);" GAME OVER{13} PLAY AGAIN?"
3020 GET K$: IF K$<>"Y" THEN 3020
3030 RUN
What I did in brief:
- Roughly halved the line count.
- Removed nearly all unnecessary
GOTOstatements. - Reduced frame delays by removing as much processing overhead as possible.
How the Optimised Version Works and Why It’s Faster
If you compare the two listings, the program logic is almost identical, but the rewritten version executes more smoothly, takes less memory, and is easier to follow.
In the main, my goal was to reduce the amount of work the BASIC interpreter must do per frame, by cutting redundant calculations, and by replacing the slow multiple character-by-character operations for the paddle with the built-in PRINT command.
Easy line-count reductions:
Check the variable initialisation:
1010 X=0
1020 Y=0
1030 DX=1
1040 DY=1
1050 P=10
Each line forces the interpreter to look up, parse, and execute a statement. Combining them onto one line like this:
X=10: Y=10: DX=1: DY=1: P=10: POKE 211,0
saves several interpreter passes. On a Commodore 64, even small reductions like this can add up, especially inside loops or setup sections that run often. It also makes it clear at a glance which variables form the game’s initial state.
Using Strings for Complex Graphics
Originally, the paddle was drawn by six separate POKE statements, each involving math and conversions:
POKE ((P+0)*40) CHR$(182)
POKE ((P+1)*40) CHR$(182)
POKE ((P+2)*40) CHR$(182)
...
This means the interpreter has to calculate and update each memory location individually. The improved version builds a string once that contains both the paddle graphics and cursor control codes:
P$=" {DOWN}{LEFT}"+CHR$(208)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(246)+"{DOWN}{LEFT}"+CHR$(250)+"{DOWN}{LEFT} "
Then it simply sets and prints that string at a specific cursor row position using a built-in ROM routine:
POKE 214,P: SYS 58732: PRINT P$
The system call (SYS 58732) positions the cursor using the screen editor routine, which is written in machine code and runs far faster than any BASIC equivalent. By letting the computer handle cursor motion and character drawing, we save both code and CPU time.
Streamlining the Game Loop
The old loop contained many lines like:
2020 X=X+DX
2030 Y=Y+DY
2040 IF X=40 THEN DX=-1
2050 IF X=40 THEN X=38
...
I combined and simplified them to give the same result:
2010 X=X+DX: Y=Y+DY
2040 IF X=40 OR X=0 THEN DX=DX*-1: X=X+DX
2080 IF Y=25 OR Y<0 THEN DY=DY*-1: Y=Y+DY
The logic is the same, in that it reverses ball direction when hitting a boundary, but we use short-circuit conditions (OR) and simple arithmetic (DX=DX*-1) instead of separate checks and assignments.
Multiplying by -1 to reverse direction is both more compact and faster. The reduced branching means the interpreter does fewer scans for line numbers, which can be one of the slowest parts of BASIC execution.
Quicker Collision Detection
The paddle collision now checks the entire range of possibilities in one expression:
IF X=0 THEN IF Y<P OR Y>(P+4) THEN GOTO 3000
This replaces multiple individual comparisons and jumps. It’s more efficient and also easier to read. One glance shows that the ball only passes the paddle safely if it’s within the range P to P+4.
You will also notice the weird double IF. A quirk of C64 BASIC is IF THEN IF executes a bit faster than IF AND, possibly because after the first check it can drop out right away, avoiding the next check.
Smarter Screen Drawing
In the earlier version, the code directly erased and redrew the ball every frame using calculations in multiple places. The revision tracks both the old and new screen locations:
OB=B: POKE OB,32
B=1024+(Y*40)+X
POKE B,87
First it erases the old position (OB), then calculates and draws the new one (B). This method avoids repeating the multiplication and addition that converts coordinates to screen memory each time and prevents ghosting because it always erases exactly the previous position.
Faster Input and Bounds Control
Rather than checking the ASCII value of each keypress (ASC(K$)), the optimised listing compares directly with strings:
IF K$="Q" THEN OP=P: P=P-1
IF K$="A" THEN OP=P: P=P+1
Direct string comparison is simpler and slightly faster. Storing OP (old paddle position) allows an immediate correction if the player tries to move off-screen:
IF P<0 OR P=18 THEN P=OP
This single condition replaces a chain of limit checks, still keeping the paddle constrained to the visible area.
Friendlier Game Over Condition
The old “Game Over” routine dropped to BASIC. The new version clears the screen and immediately prompts for replay:
PRINT CHR$(147);" GAME OVER{13} PLAY AGAIN?"
GET K$: IF K$<>"Y" THEN 3020
RUN
Optimisation Process
The key ideas applied here are broadly useful for any C64 BASIC program:
- Reduce line numbers and branches. Each new line requires the interpreter to search the program listing so fewer lines mean less searching.
- Cache repeated calculations with variables. Store results like screen addresses and reuse them instead of recalculating.
- Use built-in ROM features where possible. Printing strings or calling ROM routines (
SYScommands) can outperform BASIC loops dramatically. - Combine statements on one line. The colon (
:) lets you group short statements, improving flow and performance, especially when part of anIFcondition. - Keep logic compact. Logical operators and arithmetic sign changes can often replace multiple
IFandGOTOstatements. - Think about readability as well as speed. Well-structured code is easier to read therefore easier to optimise and debug later.
Taking it Further
We are not done, though, because a great improvement can be made by going from interpreted BASIC to a compiled version! Remember XC-BASIC? In my next post I will show how we can take the working C64 BASIC version and with minimal changes get it to compile with XC, and what a speed difference that makes …