One of the first things a C64 Game Programmer wants to know is how to move sprites in C64 Assembly. This article explains Sprite Movement in horizontal and vertical direction.
To move a sprite in C64 Assembly you have to change the X and Y Positions of the sprite that you want to move. Each of the 8 available Sprites has a Register for X and Y Position (located from memory address $D000 to $D00F) as well as an additional flag in Register $D010 for horizontal Positions above 255.
This article will give you some examples and explanations about how this works in detail.
Move Sprites
Now we will take a look at the steps that are necessary to move a sprite. This example will move the sprite to fixed positions. You will find the full source code at the end of this article.
Initializing the Program
As always we begin with our starting address and the call of our assembly language code. For more explanation on this you can read the C64 Hello World article (opens in new tab).
1 2 3 |
*=$0801 ; Starting Address BASIC + 1 => SYS 2049 !byte $0C,$08,$40,$00,$9E,$20,$32,$30,$36,$32,$00,$00,$00 ; BASIC CODE: 1024 SYS 2062 |
Next thing we do is defining some constants for convenience. We also clear the screen in order to operate from a blank space.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
; constants COLOR_BLACK = $00 COLOR_GREEN = $05 COLOR_LIGHTGREEN = $0D SPRITE_0_X_POSITION = $D000 SPRITE_0_Y_POSITION = $D001 BORDER_COLOR = $D020 BACKGROUND_COLOR = $D021 SPRITE_0_COLOR = $D027 SPRITE_0_POINTER = $0400 + $03F8 ; Last 8 Bytes of Screen RAM SPRITE_0_DATA = $0340 ; Block 13, 13*64=>832 => $0340 SPRITE_UPPER_X = $D010 ; clear screen jsr $E544 |
Short Primer on Sprite Memory
The SPRITE_0_POINTER is the address in memory, where the address of the sprite data is saved. There is one byte for each sprite and the pointers are always located at the end of the screen memory. The default screen memory starts at $0400 and the first sprite pointer has an offset from $03F8 (1016) to that.
Each sprite has a size of 64k. There are 256 available blocks where the data can be stored. In this example we use Block 13 which resolves in memory address $0340 as start address for the sprite data.
Initializing First Sprite
We use the first sprite (which is Sprite 0) for our example. The tasks at hand are enabling the first sprite (while disabling all the others), setting the Sprite color and loading the Sprite data.
Sprite Settings
We load the number 13 into Register A and write it to the address of SPRITE_POINTER_0. This is how we chose the Block 13 for the location of the sprite data.
Then we load the color code for green into Register A and write it into the address where VIC-II looks for the border color. The same thing is done with the color code for light green and the memory address for the background color.
In memory address $D015 is decided, which sprites are enabled (shown on screen) and which are not. Bit 0 of this Byte stands for Sprite 0, etc. So Bit 7 stands for Sprite 7, which is the eight Sprite. We set $D015 to 1, so all sprites are disabled except the first one.
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
The last thing we do is setting the sprite color to black. We load the color code for black into Register A and wrtie it to the address where VIC-II looks for the color of Sprite 0.
Multicolor Sprites and Sprite Layering are not part of this article
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
;; init lda #$0D ; using block 13 for sprite 0 sta SPRITE_0_POINTER ; set block 13 as target address for Data of Sprite0 lda #COLOR_GREEN ; load green color code into A sta BORDER_COLOR ; make border green lda #COLOR_LIGHTGREEN ; load lightgreen color code into A sta BACKGROUND_COLOR ; make background lightgreen lda #$01 ; enable... sta $D015 ; ...Sprite 0 => %0000 0001 (all sprites off except Sprite 0) lda #COLOR_BLACK ; load black color code into A sta SPRITE_0_COLOR ; make Sprite0 completely black |
Sprite Appearance
Now we have to load the Sprite data. First, we clear all Bytes in the Sprite 0 memory. Then, we load our Sprite data into the memory in order to see something on the screen.
Essentially two loops are created. The start of each loop is the address of SPRITE_0_DATA and it iterates 63 times until every memory position of the sprite is cleared and later set.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
ldx #$00 ; init x lda #$00 ; init a clean: sta SPRITE_0_DATA,x ; write 0 into sprite data at x inx ; increment x cpx #$3F ; is x <= 63? bne clean ; if yes, goto clean ; Build the Sprite ldx #$00 ; init x build: lda data, x ; load data at x sta SPRITE_0_DATA,x ; write into sprite data at x inx ; increment x cpx #$3F ; is x <= 63? bne build ; if yes, goto build |
You may wonder where data comes from. With the ACME assembler we can declare a label called data at the end of the program and then put the data for the Sprite there. In this case the Sprite is simply a filled block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
; sprite data data: !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 |
For more Information about Sprite Layouts and Positions in Memory you can visit the C64-Wiki (opens in new Tab).
Don’t move too fast
There is one more thing to prepare. If we would just write into the x and y Registers of the Sprites than everything would happen so fast that we would not really see the sprite movement. So we write a subroutine that checks if the rasterline has reached a certain point and only returns if this is the case. By this we make the C64 wait for roughly one frame until it computes the next operations in line.
1 2 3 4 5 6 7 |
; Wait Subroutine ;; This will make the c64 wait for roughly one frame wait: lda #$FF ; load 255 into A cmp $D012 ; look if current rasterline equals 255 bne wait ; if no, goto wait rts ; if yes, return from subroutine |
Move the Sprite
Finally we are ready to move the Sprite around. We first set the starting location by writing it into the Sprite x and y Position memory. Remember that the Border is always in front of the sprites (unless you do some tricks) so we have to set the location inside the screen in order to see our sprite at the beginning of the program. Otherwise the sprite would be hidden by the border. We randomly choose the Positions (x,y) = (100,70).
1 2 3 4 5 |
; set start location of sprite 0 ldx #$64 ; initial x position = 100 ldy #$46 ; initial y position = 70 stx SPRITE_0_X_POSITION ; move sprite 0 to x position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position |
To move the Sprite down, we simply increase the y-Position. If the Position is 70 and we increase to 71, the sprite will be shown one pixel beneath its origin location. For the purpose of this example, we build a loop and move the sprite down until the y-Position 200 is reached.
1 2 3 4 5 6 |
move_down: iny ; increment y position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position jsr wait ; call wait subroutine cpy #$C8 ; are we at y=200? bne move_down ; if no, go to move_down |
Moving the sprite back up is the same thing in reverse. Instead of increasing, we decrease the y-Position in order to move the sprite up. We create a loop that moves the sprite back to its origin y-Position at 70.
1 2 3 4 5 6 |
move_up: dey ; decrement y position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position jsr wait ; call wait subroutine cpy #$46 ; are we at y=70? bne move_up ; if no, go to move_up |
Horizontal Fun
Moving in horizontal direction has one more thing that you have to look for. If we set the X-Byte of the sprite to $FF we are at screen position x=255. The Screen is 320 pixels wide. So there is another Register, $D010 where the upper bit (Bit 8) for each sprite is stored. Bit 0 is the upper Bit of Sprite 0, and so on. If Bit 0 is 1 and the Sprite X Register contains the value 4, the real X value is 256 (Bit 8=1) + 4 (Value of X-Register) = 260.
This means that when we increment the x-Position to move the Sprite right, we have to check if we go above 255 and if so, set the upper bit of Sprite 0 (while leaving the upper Bits of the other Sprites unchanged!).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
move_right: inx ; increment x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #255 ; are we at x=255? bne move_right ; if no, goto move_right lda #$01 ; BITMASK %0000 0001 ora SPRITE_UPPER_X ; set bit 0 to 1 (leave others unchanged) sta SPRITE_UPPER_X ; write result to memory move_right_end: inx ; increment x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$2D ; are we at 255+45 = 300? bne move_right_end ; if no, goto move_right_end |
To move back left we do the opposite again. We decrement the x-Position to move the Sprite left. If we are at x=0, we now switch Bit 8 of Sprite 0 back to 0 and then decrement the x-Position until we are back at our origin of 100.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
move_left: dex ; decrement x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$00 ; are we at x=0? bne move_left ; if no, goto move_left lda #$FE ; BITMASK %1111 1110 and SPRITE_UPPER_X ; set bit 0 to 0 (leave others unchanged) sta SPRITE_UPPER_X ; write result to memory move_left_end: dex ; decrement x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$64 ; are we at x=100? bne move_left_end ; if no, go to move_left_end |
If you wonder why we never reset x to 255 or to 0 keep in mind that if x is 255 and we increment, it overflows back to 0. If we decrement from 0, its value is then 255.
Result Snapshot
Let’s take a look at the result:
Full Source Code
Here is the full source code of our first example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
; *************************** ; * Example: Move a sprite * ; *************************** *=$0801 ; Starting Address BASIC + 1 => SYS 2049 !byte $0C,$08,$40,$00,$9E,$20,$32,$30,$36,$32,$00,$00,$00 ; BASIC CODE: 1024 SYS 2062 ; Define some Constants for convenience COLOR_BLACK = $00 COLOR_GREEN = $05 COLOR_LIGHTGREEN = $0D SPRITE_0_X_POSITION = $D000 SPRITE_0_Y_POSITION = $D001 BORDER_COLOR = $D020 BACKGROUND_COLOR = $D021 SPRITE_0_COLOR = $D027 SPRITE_0_POINTER = $0400 + $03F8 ; Last 8 Bytes of Screen RAM SPRITE_0_DATA = $0340 ; Block 13, 13*64=>832 => $0340 SPRITE_UPPER_X = $D010 ; Start Program jsr $E544 ; call the Function that clears the screen LDA #$0D ; using block 13 for sprite0 STA SPRITE_0_POINTER ; set block 13 as target address for Data of Sprite0 ;; Initialization lda #COLOR_GREEN ; load green color code into A sta BORDER_COLOR ; make border green lda #COLOR_LIGHTGREEN ; load lightgreen color code into A sta BACKGROUND_COLOR ; make background lightgreen lda #$01 ; enable... sta $D015 ; ...Sprite 0 => %0000 0001 (all sprites off except Sprite 0) lda #COLOR_BLACK ; load black color code into A sta SPRITE_0_COLOR ; make Sprite0 completely black ; Reset Sprite Data ldx #$00 ; init x lda #$00 ; init a clean: sta SPRITE_0_DATA,x ; write 0 into sprite data at x inx ; increment x cpx #$3F ; is x <= 63? bne clean ; if yes, goto clean ; Build the Sprite ldx #$00 ; init x build: lda data, x ; load data at x sta SPRITE_0_DATA,x ; write into sprite data at x inx ; increment x cpx #$3F ; is x <= 63? bne build ; if yes, goto build ; Set Start Location of Sprite 0 ldx #$64 ; initial x position = 100 ldy #$46 ; initial y position = 70 stx SPRITE_0_X_POSITION ; move sprite 0 to x position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position ; Do some Movement move_down: iny ; increment y position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position jsr wait ; call wait subroutine cpy #$C8 ; are we at y=200? bne move_down ; if no, go to move_down move_up: dey ; decrement y position sty SPRITE_0_Y_POSITION ; move sprite 0 to y position jsr wait ; call wait subroutine cpy #$46 ; are we at y=70? bne move_up ; if no, go to move_up move_right: inx ; increment x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #255 ; are we at x=255? bne move_right ; if no, goto move_right lda #$01 ; BITMASK %0000 0001 ora SPRITE_UPPER_X ; set bit 0 to 1 (leave others unchanged) sta SPRITE_UPPER_X ; write result to memory move_right_end: inx ; increment x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$2D ; are we at 255+45 = 300? bne move_right_end ; if no, goto move_right_end move_left: dex ; decrement x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$00 ; are we at x=0? bne move_left ; if no, goto move_left lda #$FE ; BITMASK %1111 1110 and SPRITE_UPPER_X ; set bit 0 to 0 (leave others unchanged) sta SPRITE_UPPER_X ; write result to memory move_left_end: dex ; decrement x position stx SPRITE_0_X_POSITION ; move sprite 0 to x position jsr wait ; call wait subroutine cpx #$64 ; are we at x=100? bne move_left_end ; if no, go to move_left_end ; Wait Subroutine ;; This will make the c64 wait for roughly one frame wait: lda #$FF ; load 255 into A cmp $D012 ; look if current rasterline equals 255 bne wait ; if no, goto wait rts ; if yes, return from subroutine ; The Data for our Sprite data: !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 !byte 255, 255, 255 |
Conclusion
This was a very brief explanation of how you can move sprites on a C64. There are some things to prepare upfront and you should watch out for the extra Bit when moving horizontally. Now you can try the program for yourself. You may even extend it. Read my article on Joystick Input and then make sprites move only if intended by Joystick Movement.