Nate's Blog

Abandon all hope, ye who enter here

Moon Blaster

When I was about 11 or 12 years old, I wrote a simple game in Commodore 128 BASIC. It’s not very fancy, but it is a complete game, with sound, graphics, a title screen, and even a backstory. The object of the game is to fire missiles at a moon moving across the top of the screen. The player can move the missile launcher to one of three bases using the joystick, and (of course) launch missiles with the fire button. By any standard, it’s not a great game, but I was pretty proud of it at the time.

The C128 included Commodore BASIC 7.0, which was much better than the older Commodore BASIC 2.0 that was on the C64. BASIC 7.0 had lots of additional commands to control the graphics and sound chips in the machine, read the joysticks, save and load binary files between RAM and disk, etc., and the computer came with a really nice system guide [PDF] that described all of them. 6502 machine language was out of my reach at the time—I didn’t have an assembler, or even any reference material. The C128 included a machine language monitor with a rudimentary assembler, but I had no idea what it was for. Nevertheless, I decided to try to make a game using the enhanced C128 BASIC, and Moon Blaster was the result.

Moon Blaster screenshot
Machine Language Port

My recent foray into C64 game reverse-engineering gave me an idea: re-write Moon Blaster in assembly language for the C64. I’ve never written a game in machine language for the C64 before, and this game is pretty simple—great for a first project. I had a couple main goals: 1) stay as true as possible to the original game, and 2) build both disk and cartridge versions. I’m pleased to report that I was successful. Here’s a short video of the new version of the game (headphone warning, it might be a bit loud):

Moon Blaster ML gameplay

If you’d like to play the game, you can download it here. The download contains cartridge and disk images for the new version, as well as the assembly source code for it. It also includes a disk image of the original BASIC game, if you’d like to compare the two. You’ll need a C64/C128 emulator (VICE is a good one), or a real C64/C128. (If you’re using real hardware, I’ll assume you know how to get the images to it. If not, let me know.) The easiest way to start the game in VICE is to attach the cartridge image, either by using the menu or by pressing Alt+C. You’ll need to set up the joystick as well. VICE works with a game controller, or can emulate a joystick using the keyboard. It’s pretty straightforward to set up, but if you have trouble, let me know.

It’s Not a Bug

An amusing story (at least to me) about Moon Blaster is the way I inadvertently introduced the concept of a critical hit. The moving objects on the screen—the moon, missile launcher, and missiles—were implemented as sprites. C128 BASIC included commands for sprite collision detection, which I used to detect when the player got a hit. When two sprites touch, the program jumps to a specified line, which in this case plays the hit sound, updates the score, etc.

Unfortunately, BASIC is so slow that, in the original game, if the player hit the moon close to dead center, the missile collided twice before it was moved back to the launcher. This had the effect of doubling the hit sound and animation, and scoring twice instead of once. I recall trying to fix it, and being stumped. Ultimately, in the grand tradition of “It’s not a bug; it’s a feature,” I decided just to add it to the instructions as an “intentional” bonus. However, the machine language version runs much faster, so this didn’t happen, and to stay true to the original, I actually had to write code specifically to replicate this bug feature.

Technical Stuff

The rest of this post contains technical details intended to be of interest to other programmers. If you’re not a programmer, you’re welcome to stick around, but if you want to bail at this point, I won’t mind 😀. I won’t go over all of the game code, but there were a few things I was pretty happy with, and I thought they were worth talking about. The assembler I used for this project is ca65, which is part of the cc65 compiler suite. In case it’s helpful, here’s a link to a 6502 instruction set reference.

Linear Feedback Shift Register

The original game initially used BASIC drawing commands to draw the star field. This was painfully slow, so I ended up changing it to load the bitmap memory from a disk file, which I had saved after running the drawing routines. This was still pretty slow, but was a noticeable improvement. When doing the assembly port, I had the raw power of a 1 MHz processor at my command, so I decided the game should draw the stars at startup every time. Doing this in machine code is way faster than loading a screen image from disk. Plus, with the cartridge version, there wouldn’t even be a disk.

However, I wanted the stars to be the same every time, too, so I needed a pseudorandom number generator. The SID sound chip in the C64 can be used to generate random numbers, but there’s not a good way to seed it. So using that approach, the stars would change every game. Instead, I implemented a 16-bit Linear Feedback Shift Register (LFSR) routine, and sampled bits from it to get pseudorandom coordinates for drawing the stars. I used the constants from the Wikipedia article, since that seemed as good a set as any. I don’t claim it’s the most optimal LFSR ever written for the 6502, but I think it’s pretty slick:

.EXPORTZP LFSR_STATE = $fd

.CODE

.PROC lfsr_16

    ; get XOR of bits 15 and 13
    lda LFSR_STATE+1
    lsr a
    lsr a
    eor LFSR_STATE+1

    ; get XOR of bit 12
    lsr a
    eor LFSR_STATE+1

    ; get XOR of bit 10
    lsr a
    lsr a
    eor LFSR_STATE+1

    ; put result of XORs into carry flag
    lsr a
    lsr a
    lsr a

    ; rotate carry flag into LFSR_STATE as the LSB
    rol LFSR_STATE
    rol LFSR_STATE+1

    rts
.ENDPROC ; lfsr_16
Raster Interrupt Split-Screen

The game is mostly graphical, but there are text elements to display the score and number of shots fired. C128 BASIC has a command (CHAR) to draw character on a bitmap screen. I could have taken the same approach with the machine language port, but that would involve banking in the character ROM and copying the bitmap data for each character. It would be slow (although I had plenty of cycles to do it since the game is so simple) and ugly code. Instead, I decided to go a different direction—changing between graphics and text mode in the middle of the frame.

The VIC-II video chip in the C64 can be configured to cause an interrupt when it reaches a specified scan line—a so-called raster interrupt. To achieve a split screen effect, one can enable the raster interrupt at the top of the screen, and then in the interrupt handler, turn on graphics mode, then set the raster interrupt somewhere in the middle of the screen, and turn on text mode. There are a few other details to take care of, but this works surprisingly well. To print the score, I can simply place the characters in the right location in screen memory. Here’s the code I used to enable the interrupt, and the interrupt handler that implements the split-screen:

.INCLUDE "vic2.inc"

IRQ_VECTOR = $0314
KERNAL_ISR = $ea31
CIA1_IRQ = $dc0d
CIA2_IRQ = $dd0d

.DATA

FRAME_SYNC: .res 1

.CODE

.PROC setup_irq
    sei

    ; disable CIA-1 interrupts
    lda #%01111111
    sta CIA1_IRQ

    ; clear high bit of raster counter
    and VIC2::CR1
    sta VIC2::CR1

    ; acknowledge pending interrupts
    lda CIA1_IRQ
    lda CIA2_IRQ

    ; set raster interrupt
    lda #0 ; interrupt on this raster line
    sta VIC2::RST

    ; clear frame sync variable
    sta FRAME_SYNC

    ; set interrupt vector
    lda #<split_screen_isr
    sta IRQ_VECTOR
    lda #>split_screen_isr
    sta IRQ_VECTOR+1

    ; enable raster interrupts
    lda #%00000001
    sta VIC2::IMA

    cli
    rts
.ENDPROC ; setup_irq

.PROC teardown_irq
    sei

    ; disable raster interrupts
    lda #%00000000
    sta VIC2::IMA

    ; acknowledge pending interrupts
    asl VIC2::IRQ ; acknowledge VIC-II raster interrupt by clearing low bit

    ; enable CIA-1 interrupts
    lda #%11111111
    sta CIA1_IRQ

    ; restore interrupt vector
    lda #<KERNAL_ISR
    sta IRQ_VECTOR
    lda #>KERNAL_ISR
    sta IRQ_VECTOR+1

    cli
    rts
.ENDPROC ; teardown_irq

.PROC split_screen_isr

    ; clear decimal flag
    cld

    ; look at current raster line to see if we should turn bitmap on or off
    lda VIC2::CR1
    ldx VIC2::RST
    cpx #100
    bcs bitmap_off

    ; turn bitmap mode on
bitmap_on:
    inc FRAME_SYNC
    ora #%00100000  ; bitmap on
    sta VIC2::CR1
    lda VIC2::PTR
    ora #%00001100  ; set bitmap memory to $2000-$3FFF
    and #%11111100
    sta VIC2::PTR
    lda #209        ; next interrupt raster line
    jmp return

    ; turn bitmap mode off
bitmap_off:
    and #%11011111  ; bitmap off
    sta VIC2::CR1
    lda VIC2::PTR
    ora #%00000100  ; set bitmap memory to $0000-$1FFF
    and #%11110100
    sta VIC2::PTR
    lda #0          ; next interrupt raster line

return:
    sta VIC2::RST
    asl VIC2::IRQ   ; acknowledge VIC-II raster interrupt by clearing low bit
    jmp KERNAL_ISR
.ENDPROC ; split_screen_isr
Exit to BASIC

C64 games rarely include an option to exit the game and return to BASIC. Even though 64K of RAM was pretty roomy for the time, most games needed a great deal of it, and so banked out the BASIC ROM and clobbered BASIC memory areas. It’s also possible this was used as a rudimentary form of copy protection. However, the original Moon Blaster included an option to quit; being a BASIC program itself, this was simple to do.

I wanted the machine language port to have this same capability. It was in the original game, and I’m not worried about copy protection. The game doesn’t take much memory (less than 5K of code and data, not counting the 8K bitmap and 1K text screen memory areas), and I was able to place everything so as not to interfere with BASIC. To exit to BASIC, the disk-based game can simply jump to the BASIC warm-start routine $E38B. However, there are two problems with this.

The first problem is that just jumping to BASIC warm-start doesn’t work at all if you’re running the cartridge version, because BASIC isn’t initialized in that case. To address this, I call a few BASIC ROM routines from the cartridge entry point to initialize BASIC to the point that it can be warm-started. Here’s the code I used to do that:

.INCLUDE "main.inc"
.SEGMENT "START"

; entry point which jumps away to BASIC, so it works right from a cartidge
.PROC start

    ; initialize the KERNAL
    jsr $fda3 ; initialize I/O devices
    jsr $fd50 ; test RAM and initialize memory pointers
    jsr $fd15 ; initialize KERNAL vectors
    jsr $e518 ; initialize the screen and keyboard
    cli ; enable interrupts

    ; initialize BASIC
    jsr $e453 ; initialize BASIC vectors
    jsr $e3bf ; initialize BASIC RAM

    ; run the game
    jsr main

    ; jump to BASIC
    ldx #$80 ; no error code
    jmp $e38b ; BASIC warm start

.ENDPROC ; start

The second problem is that I wanted the user to be able to re-start the game simply by typing RUN. This kind of works with the disk version, but that runs the BASIC loader program and loads the whole game from disk again, even though it’s still in memory. With a cartridge, there would be no BASIC program to RUN at all. To deal with this, I included a simple BASIC program to restart the game in the constant data section. Before exiting, I replace whatever BASIC program is loaded (possibly none) with this program, which immediately starts the game again when run. To do this, not only the BASIC program area at $0801 needs to be populated, but also several pointers that BASIC uses, starting at $2B in zero page. (The details about these locations can be found in a C64 memory map.) Here’s the code I used to do that:

.RODATA

; BASIC program to re-start the game
;
; 10 SYS 32804
;
BASIC_LOADER_ADDR = $0801
BASIC_LOADER:
.byte $0d, $08, $0a, $00, $9e, $20, $33, $32, $38, $30, $34, $00, $00, $00
BASIC_LOADER_SIZE = * - BASIC_LOADER

BASIC_POINTERS_ADDR = $2b
BASIC_POINTERS:
.byte $01, $08, $0f, $08, $0f, $08, $0f, $08, $00, $80, $00, $00, $00, $80
BASIC_POINTERS_SIZE = * - BASIC_POINTERS

.SEGMENT "MAIN"

; entry point which does an RTS, so it works right from BASIC's SYS command
.PROC main

    ; initialize SID
    jsr init_sid

    ; initialize screen
    jsr init_screen

    ; show title screen
    jsr title_screen

    ; restore screen
    jsr restore_screen

    ; put the BASIC loader into BASIC program memory
    lda #<BASIC_LOADER
    sta $fb
    lda #>BASIC_LOADER
    sta $fc
    lda #<BASIC_LOADER_ADDR
    sta $fd
    lda #>BASIC_LOADER_ADDR
    sta $fe
    ldy #0
loop1:
    lda ($fb),y
    sta ($fd),y
    iny
    cpy #BASIC_LOADER_SIZE
    bne loop1

    ; update the BASIC pointers for the new BASIC program just copied
    lda #<BASIC_POINTERS
    sta $fb
    lda #>BASIC_POINTERS
    sta $fc
    lda #<BASIC_POINTERS_ADDR
    sta $fd
    lda #>BASIC_POINTERS_ADDR
    sta $fe
    ldy #0
loop2:
    lda ($fb),y
    sta ($fd),y
    iny
    cpy #BASIC_POINTERS_SIZE
    bne loop2

    rts

.ENDPROC ; main

With those two problems solved, the user can cleanly exit the game and then jump right back in, with no load time, regardless of whether the game is in cartridge or disk form. It’s not earth-shattering or anything, but I think it’s pretty cool.

Closing Thoughts

I had a lot of fun doing this project, and learned quite a few things. It was neat to revisit something I wrote so many years ago, and to breathe new life into it. The game won’t win any awards, but that’s okay. If you download and the play the game, or look at the code, and have any questions about how I did this or that, feel free to ask. If you have any suggestions on how I could have done things better, I’d love to hear about that too.

Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *