❌

Reading view

There are new articles available, click to refresh the page.

Commodore 64 Assembly, part 7 - sprites and segments

By: mms

Time to dig into the meat - time to draw things on the screen! This time I'm learning how to draw sprites.

Movable Object Block (aka "sprites") are exactly this - blocks we can easily move around the screen. Normally, if wanted to move a screen we would need to store was underneath, calculate where the sprite overlaps with background, draw what was visible and then redraw background after the object moves. Commodore does all of that for us, with the help of MOS VIC chip. Basically, we need to store the sprite in memory, tell the chip where to look for, define where on screen it should render it and boom - we have a sprite. It's so simple that it took me 3 evenings to kind of get a hang of it.

First of all: we can see up to 8 sprites at the same time (ignoring magical tricks for now). Is it a lot? Not enough? I've got no idea. But we've got 8.

A sprite is a set of bytes defining how it's pixels look like. We've got a space of 24x21 pixels, so not that much (but enough to overwhelm me). Commodore supports two types of sprites: monochrome or multicolor. For monochrome, we've got every pixel to work on - assuming that we want to enable or disable it. Disabled pixeds are transparent. Multicolor sprites borrow half the pixels for color definition, making each individual pixel represented as double height. We can therefore can have crispy, monochrome or less crispy multicolor. Note, that it's not true color - multicolor here means exactly 3: one which is specific to a given sprite, and 2 which are shared between all sprites.

For now I used the amazing online sprite editor spritemate and prepared two. One highres, of which I am proud:

        .byte $00,$00,$00,$00,$c0,$00,$01,$f0
        .byte $00,$01,$fc,$00,$00,$fe,$00,$03
        .byte $ff,$80,$07,$ff,$c0,$03,$ff,$80
        .byte $01,$ff,$00,$07,$ff,$80,$0f,$ff
        .byte $c0,$1f,$ff,$e0,$0f,$ff,$c0,$07
        .byte $ff,$80,$1f,$ff,$e0,$3f,$ff,$f0
        .byte $7f,$ff,$f8,$3f,$ff,$f0,$1f,$ff
        .byte $e0,$00,$00,$00,$00,$00,$00,$0a

The other one, in color, of which I am very proud:

        .byte $00,$00,$00,$00,$80,$00,$02,$a0
        .byte $00,$02,$a8,$00,$00,$aa,$00,$02
        .byte $a9,$80,$02,$a6,$80,$02,$9a,$80
        .byte $00,$6a,$00,$02,$a9,$80,$02,$a5
        .byte $80,$0a,$db,$a0,$0a,$eb,$a0,$01
        .byte $eb,$40,$0a,$a9,$60,$2a,$a5,$a8
        .byte $2b,$d7,$e8,$2a,$ff,$a8,$09,$6a
        .byte $a0,$00,$00,$00,$00,$00,$00,$88

I'll use the amazing Commodore 64 memory map page to find where to put data, as the Vic chip has predefined addresses for certain functions. For example, it will look at $07f8 to find pointer for sprite #0, at $D027 to get the color and at $d015 to determine if it should display this sprite. With $d000 and $d001 we can specify the X and Y location. This is great, as once we populate a given memory location with data, it will be there and VIC chip will do it's thing. Once we put the proper pointer in location address, it will simply be there. Once we put location, the sprite will stay in place until we change the location - and then the chip will correctly re-draw the background.

Note, that we will use three type of values:

  • immediate values - for things like X/Y coordinates.
  • binary data - for some features
  • pointers - for sprite locations

The immidate values are easy. We get a number, put in memory and it works.

Binary data is easy to understand: each bit represents an ON/OFF value. So, if we want to enable a feature on sprite 4 we need to put 000010000; for 2 and 4 we will use 00010100.

Now, the pointers. First of all: sprites are most often placed in a reserved memory space located at $2000. This is normally unused space, and it became normal to put it there. So, we need to put our sprites and assemblers are here to help. For our cl64, we can use .segment which will reserve space:

        .segment "GFXDATA"

        ;; high-res
        .byte $00,$00,$00,$00,$c0,$00,$01,$f0
        .byte $00,$01,$fc,$00,$00,$fe,$00,$03
        .byte $ff,$80,$07,$ff,$c0,$03,$ff,$80
        .byte $01,$ff,$00,$07,$ff,$80,$0f,$ff
        .byte $c0,$1f,$ff,$e0,$0f,$ff,$c0,$07
        .byte $ff,$80,$1f,$ff,$e0,$3f,$ff,$f0
        .byte $7f,$ff,$f8,$3f,$ff,$f0,$1f,$ff
        .byte $e0,$00,$00,$00,$00,$00,$00,$0a
        
        ;; color
        .byte $00,$00,$00,$00,$80,$00,$02,$a0
        .byte $00,$02,$a8,$00,$00,$aa,$00,$02
        .byte $a9,$80,$02,$a6,$80,$02,$9a,$80
        .byte $00,$6a,$00,$02,$a9,$80,$02,$a5
        .byte $80,$0a,$db,$a0,$0a,$eb,$a0,$01
        .byte $eb,$40,$0a,$a9,$60,$2a,$a5,$a8
        .byte $2b,$d7,$e8,$2a,$ff,$a8,$09,$6a
        .byte $a0,$00,$00,$00,$00,$00,$00,$88
For this to work, we need to configure cl with a file we will call main.cfg:
FEATURES {
    STARTADDRESS: default = $0801;
}
SYMBOLS {
    __LOADADDR__: type = import;
}
MEMORY {
    ZP:       file = "", start = $0002,  size = $00FE,      define = yes;
    LOADADDR: file = %O, start = %S - 2, size = $0002;
    MAIN:     file = %O, start = %S,     size = $D000 - %S;
}
SEGMENTS {
    ZEROPAGE: load = ZP,       type = zp,  optional = yes;
    LOADADDR: load = LOADADDR, type = ro;
    EXEHDR:   load = MAIN,     type = ro,  optional = yes;
    CODE:     load = MAIN,     type = rw;
    RODATA:   load = MAIN,     type = ro,  optional = yes;
    DATA:     load = MAIN,     type = rw,  optional = yes;
    GFXDATA:  load = MAIN,     type = ro, optional = yes, start = $2000;
    BSS:      load = MAIN,     type = bss, optional = yes, define = yes;
}

The only difference between this one and the default one is the added "GFXDATA" segment. There's a lot here I have to understand, but ok. Now our sprites are located in a known memory location. The problem is that the VIC chip can address only 16 KB of memory. Which exactly 16 it will use is based on the currently used VIC bank

Those 16 KB of data is the only addressable space which contains: the screen RAM, character data, bitmap data and our sprite data. We will ignore the first three - all. But to tell the chip the address of the sprite, we need to give the offset from the current bank. The full calculation goes like this: location = base offset + (sprite offset * sprite size):

  • base offset - we will assume 0.
  • sprite offset - is what we need to get
  • sprite size - we know it, and it's $40
  • location - we know it, it's $2000.

Therefore, the offset for us it's: $2000 / ($40 * i) where i is which sprite we are talking about. They may be 0-indexed in our documentation, but here we will use 1. Our first offset is $80, the second $81 and son on.

Putting it all together (plus a few extra things):

        ;; clear screen
        jsr $e544

        ;; make background black
        LDA #$00                ; black color
        sta $d021               ; store background color
        
        ;; make border black
        LDA #$00                ; black color
        sta $d020               ; store border color
        
        ;; SPRITE 0
        lda #100                ; set x position
        sta $d000               ; store x to memory

        lda #100                ; set y position
        sta $d001               ; store y to memory

        LDA #$09                ; set brown color as sprite color
        STA $D027               ; store color as color for sprite 0

        lda #highres            ; load the offset of sprite 0
        sta $07f8

        ;; Sprite 1
        lda #200                ; set x position
        sta $d002               ; store x to memory

        lda #100                ; set y position
        sta $d003               ; store y to memory

        LDA #%0000010           ; set bits to treat sprite 1 as multi-color
        sta $D01C               ; store it

        lda #color              ; load the offset of sprite 1
        sta $07f9

        LDA #$09                ; set brown color as sprite color
        STA $D028               ; store color as color for sprite 1

        ;; shared colors
        LDA #$00                ; black color
        STA $D025               ; store color as shared color 1
        
        LDA #$01                ; white color
        STA $D026               ; store color as shared color 2
        
        ;;  display sprites
        lda #%00000011         ; set bit s to enable sprite 1
        sta $d015              ; store bits

        ;; make the sprites bigger
        LDA #%0000011           
        sta $D017               ; double height
        LDA #%0000011
        sta $D01D               ; double width

        
loop:        
        jmp loop                

        .segment "GFXDATA"

highres:= $80
        ;; high-res
        .byte $00,$00,$00,$00,$c0,$00,$01,$f0
        .byte $00,$01,$fc,$00,$00,$fe,$00,$03
        .byte $ff,$80,$07,$ff,$c0,$03,$ff,$80
        .byte $01,$ff,$00,$07,$ff,$80,$0f,$ff
        .byte $c0,$1f,$ff,$e0,$0f,$ff,$c0,$07
        .byte $ff,$80,$1f,$ff,$e0,$3f,$ff,$f0
        .byte $7f,$ff,$f8,$3f,$ff,$f0,$1f,$ff
        .byte $e0,$00,$00,$00,$00,$00,$00,$0a
        
color:= $81
        ;; color
        .byte $00,$00,$00,$00,$80,$00,$02,$a0
        .byte $00,$02,$a8,$00,$00,$aa,$00,$02
        .byte $a9,$80,$02,$a6,$80,$02,$9a,$80
        .byte $00,$6a,$00,$02,$a9,$80,$02,$a5
        .byte $80,$0a,$db,$a0,$0a,$eb,$a0,$01
        .byte $eb,$40,$0a,$a9,$60,$2a,$a5,$a8
        .byte $2b,$d7,$e8,$2a,$ff,$a8,$09,$6a
        .byte $a0,$00,$00,$00,$00,$00,$00,$88
two icons of a comic-style poo. The left one is all brown. The right one is also brown but  has black shading and white eyes and a smile. It may resemble the so-called emoji.
Yes, I am 12.

Which give me an inspiration for the first real program to write.

❌