C64 Programming - Invader Fractal #2

:: C64, 6502, retro

In the last post we discovered how to decode 15-bit symmetric invaders into a chacrater set. This time around we will make them fractal!

The invaders from the last post occupy exactly one character each. The aim is to now select an invader at random, and then draw it one size bigger, using other random invaders as its “pixels” like this

Then, the process can be repeated again, drawing an even bigger invader which is formed by drawing even bigger “pixels” which are composed of the size 2 invaders, like this!

A plan of action

Writing in ASM is very different to high level languages. You can’t simply jump in and start writing code with only a very loose idea, at least some stuff has to be planned out up ahead if you want to get anywhere without wasting time. (Well, I guess with more experience this get easier … )

In order to draw the first level of invader, we are going to need the following high-level elements

  • A way of selecting a random invader and calculating where in character set memory it sits
  • An algorithm that takes each bit of each row of the invader in turn, and draws another invader into video memory if the bit is set
  • Another alogrithm that does a very similar thing to the previous one, on a bigger scale

I have a small not-very-random number generator routine originally called rng. Its implementation is out of the scope of this post, but you can call it as follows

1
jsr rng		

jsr is an opcode that jumps to the named routine, after first pushing the current location on the stack, then when it encounters an rts it returns execution to the calling code. The rng routine will leave a not particularly random number inside the accumulator ready for use.

Choosing an invader

The program I wrote populates a character set at memory location $2000 with 254 sort of unique invaders, using the technique described in the previous post and the rng routine. I defined characters 0 and 1 in the set manually, where 0 is blank and 1 has every pixel set. A character in memory is layed out as 8*8 contiguous bytes, each one represeting the pixels that are switched on for each row. Therefore, the location in memory of character n is $2000 + (n * 8). The 6502 does not have any multiplication instructions - although you can of course write your own by using a combination of adding and bit shifting, which would be fast. However, for this task we will just write a loop that adds 8 n times.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
draw_invader: subroutine
    ; store the address at $3E and 3F
    ; starting at $2000 where the character memory begins
    lda #$0
    sta $3E
    lda #$20
    sta $3F
    ;; pick random invader number
    jsr rng
    sta $3D
    ;; add 8 for each count
.calc_address
    clc
    lda #8
    adc $3E
    sta $3E
    bcc *+4
    inc $3F
    dec $3D
    bne .calc_address

This is fairly straight forward - the address is 16 bits, and we store it in location $3E and $3F in the zero-page (an arbitary choice) so that it can be used with the indirect addressing mode later, as discussed in the previous article. A random number from 0–255 is generated and stored in $3D, and then we enter a loop that adds 8 to the address, decreases $3D and loops until $3D is zero. One new thing here is the operand following the bcc instruction, *+4. The * tells the assembler to use the current location, which you can then offset by some number. Effectively this skips the inc $3F instruction, which saves having to use a label to do it. Oviously, this technique is error prone and hard to read, but it makes sense for stuff you have to do all the time, like this addition of an 8 bit number to a 16 bit one. The “subroutine” keyword tells the assembler to uniquely name any labels following it that start with a ".", this is so you don’t have to keep dreaming up globally unique label names for your different routines.

Drawing an Invader

Now we have the correct address, we can work out how to draw the invader. The C64 screen is 40*25 characters. A character is 8*8 pixels, although the invaders are only actually using 5*5 of them, which will be useful later since it means we can fit one massive invader on the screen, just!

1
2
3
4
    lda #$00
    sta $44
    lda #$04
    sta $45

The video memory starts at $0400, and clearly we are going to need to keep track of where we are in order to draw the invader, so another 16 bit address ($0400) is placed in the zero-page at locations $44 and $45 (more random locations).

Now the algorithm can begin:

  • Read the current byte of the invader
  • For each bit of the byte, determine if it is set
  • If it is, pick a random invader character and store it at the current video memory location
  • Increase the video location by one
  • At the end of the byte, move down to the next row in video memory, at column 0
  • Loop 5 times
 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
    ldx #5                      ; 5 lines
.draw_line
    ldy #0                      ;clear y, not using it here
    lda ($3E),y                 ;load current invader byte
.loop
    pha                         ;preserve it on the stack
    and #1                      ;check current bit for 1 or 0
    beq .skip
    jsr rng                     ;pick random invader
    sta ($40),y                 ;draw to screen offset by y
.skip
    pla                         ;restore from stack
    iny                         ;increase y offset
    cpy #5                      ;are we finished yet? 
    beq .finish_line            ;(invaders are 5x5 so no point looking at remaning bits)
    lsr                         ;if no, then bit shift one right
    clc                         ;to process next bit, and loop
    bcc .loop
.finish_line
    dex                         ;move to next line if x still > 0
    beq .finish 
    lda #39                     ;add 39 to the video address 
    adc $40                     ;which will move to the next row
    sta $40
    bcc *+4
    inc $41

    inc $3E                     ;add 1 to the invader address to
    bcc *+4                     ;move to its next byte
    inc $3F    
    clc    
    bcc .draw_line              ;loop

.finish

The indirect addressing is used nicely here, with the Y register effectively offseting the video location and acting as a counter at the same time, whilst X is used to count the rows. The rest of the stuff should be pretty self explanatory by now - the only new thing here is the slighly odd looking

1
2
clc
bcc .label

All this does is force a branch, so it is similar to jmp although my reading indicates it is better to use this style since it makes the code more re-locatable.

The Final Invader!

The final piece is to draw one massive invader composed of smaller invaders from the previous step across the whole screen.

  • Choose an invader at random
  • Process the bits as per the first algorithm
  • For a set bit, store the desired video memory location and call the other algorithm
  • Update the video memory to start at the next location
  • Loop

Essentially this routine will seed the other one - the code that sets the video memory location of $0400 is removed from the previous routine, and instead the new routine sets this up before calling it. Because the first routine modifies those numbers, I have chosen to store the actual location in another zero-page address so that is easier to reason about in the top level routine. This isn’t really necessary but it simplies the problem.

Since the algorithm is mostly the same, I will just highlight a few of the different parts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.loop
    pha                         ;preserve on stack
    and #1                      ;check current bit for 1 or 0
    beq .skip
    lda $44                     ;copy video memory location
    sta $40
    lda $45
    sta $41
    txa                         ; preserve registers
    pha
    tya
    pha
    jsr draw_invader            ; call other routine
    pla                         ; restore registers
    tay
    pla
    tax

Notice here we copy the data from the addresses $44 and $45 into $40 and $41 where the draw_invader routine reads from. Then, since both routines use the X and Y registers, we have to push the current contents onto the stack so that they can be recovered after the subroutine has executed. Then we simply generate a random invader number, call the other routine and then restore the status of the registers from the stack.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.skip
    iny                         ;increase y offset
    cpy #5                      ;are we finished yet? 
    beq .finish_line            ;(invaders are 5x5 so no point looking at remaning bits)
    lda #8                      ;update video memory location
    adc $44
    sta $44
    bcc *+4
    inc $45
    pla                         ;restore from stack
    lsr                         ;if no then shift one right
    clc                         ;to process next bit and loop

    bcc .loop

Once again, this is mostly the same as before, except now we update the video address by 8 bytes to move into the next “mega pixel” or “grid location”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.finish_line
    pla
    dex
    beq .finish
    lda #167        ;update video memory location to next row
    adc $44
    sta $44
    bcc *+4
    inc $45
    inc $42
    bcc *+4
    inc $43
    clc
    bcc .draw_line

Again, very similar to before, and now we also move down 5 rows and back to column zero. This is what the magic number 167 is doing.

And as if by magic!

Conclusion

You might be thinking, Ross! Why are you repeating yourself in the code!! And that would be a good question. Unlike higher level languages, quite often it doesn’t pay to re-use code in asm. It is always slower for a start, since you are introducing more subroutine calls, more stack usage and so forth. Of course I have no doubts that there are a million other ways to write this, in various degrees of cleverness, but all in all I am happy that I got it to work, and the result is quite pleasing! I have learnt quite a lot from this mini-project, and have bunch of ideas on what to do next :)