Lunar Lander

Overview

This recreation of Lunar Lander was programmed in Easy68k assembly over the course of two weeks. It features two different win conditions, physics, and a seven-segment display.

Implementation

Rendering

Rendering was accomplished by reading in a BMP file and, using its file header, extracting the necessary data to calculate the width and height of the image. From there, each individual picture was read in and drawn to the screen. As this process is costly, only the areas of the screen that had movement on top of them needed to be rewritten. This drastically sped up the program.

determineColor
        * Determine color
        move.l (a0), d1         ; move next color from memory into pen
    
        * Byte shift
        ror.l #8,d1             ; make sure all the bytes are in the right spots
        
        * If color is transparent, skip it
        cmp.l  #TRANSPARENT_COLOR, d1
        beq    skipDraw
        
        * Set color and draw pixels
        move.l  #PEN_COLOR_TRAP_CODE, d0    ; set the proper trap code to set the pen color
        trap    #15                         ; set pen color through trap code
        
        * Set location to draw
        move.l d3, d1           ; x location
        move.l d4, d2           ; y location
        
        move.l #DRAW_PIXEL_TRAP_CODE, d0    ; set trap code back to drawing pixels               
        trap    #15                         ; draw pixel

skipDraw:
        * Loop incrementers
        addi.l  #1,d3                   ; increment width counter 
        cmp.l   d3,d5                   ; compare loop counter (d1) to width (d5)         
        bne     continueRow             ; continue loop
nextRow:
        move.l  d7, d3                  ; reset width counter for next loop
        subi.l  #1,d4                   ; decrement height counter
        move.l  THIS_OFFSET_Y(sp), d1
        cmp.l   THIS_OFFSET_Y(sp), d4   ; check if height has reached offset
        bne     clipX                   ; if it has not, continue
    
        * Return to caller
        move.l #4, d0
        sub.l d0, sp
        jsr loadAllRegs                 ; restore registers 
        add.l d0, sp    
        rts                             ; return from function

Physics

Physics was implemented using simple linear acceleration.

* Params: direction x, direction y
* Positive x -> right, positive y -> down
addVelocity:   
* Math
    * Add x direction to velocity
    move.l  DIR_X(sp), d0           ; move var from stack to register
    cmpi.l  #0, d0                  ; if x velocty is 0, don't use fuel
    beq     skipFuelX
    subi.l  #1, (fuel)              ; subtract 1 from fuel
skipFuelX:
    muls    #ACCELERATION_X, d0     ; multiply acceleration by direction to get correct direction
    add.l   d0, (velocityX)         ; add acceleration to velocity
    
    * Add y direction to velocity
    move.l  DIR_Y(sp), d0           ; move var from stack to register
    muls    #ACCELERATION_Y, d0     ; multiply acceleration by direction to get correct direction
    cmpi.l  #0, d0
    beq     skipFuelY
    subi.l  #1, (fuel)              ; subtract 1 from fuel
skipFuelY:
    add.l   d0, (velocityY)         ; add acceleration to velocity

The challenging aspect of implementing it, however, was in converting integers to floats. x86 Assembly, storing only bytes of information, does not have any inherent method to represent floats. Instead, fractional bits were used. Essentially, depending on how accurate I wanted my float to be, n fractional bits were reserved as the last n bits of the longs I used. Then, any math could be applied to those drawn out numbers, and the fractional bits can be shifted back when the number needed to be used as an integer.

    * Add velocity parameters
    move.l  #12, d1             ; set space for parameters
    sub.l   d1, sp              ; set up SP
    move.l  d5, (sp)            ; move values onto stack
    move.l  d4, 4(sp)
    jsr     addVelocity 
    
    * Get x result
    move.l  (velocityX), d5     ; move velocity into register
    add.l   d5, d6              ; add it to position
    
    * Get y result
    move.l  (velocityY), d5
    asr.l   #FRAC_BITS, d5
    asr.l   #FRAC_BITS, d5      ; shift d5 back
    add.l   d5, d7              ; add y velocity to position
    add.l   d1, sp

Win Conditions

Two win conditions and one lose condition were present in the game. The player could safely land on yellow segment for 1 point or on the green segment for 2 points. If the player had a velocity above a certain amount when they landed or landed in a different area, they would die and be sent to the start of the game. Win conditions were calculated based on the color of the pixel directly below the lunar lander. If the hex code of the color matched one of the winning colors, the player would gain a point and return to the main menu.

checkForWin:
    cmpi.l  #WIN, (gameWon)         ; if game was not won, it was lost
    bne     gameEndLose     
    
gameEndWin:
    * Check for extra points
    cmpi.l  #1, (extraPoints)
    beq     showExtraPtsScreen      ; if extra point win, go to extra point screen
    bra     showWinScreen           ; if regular win, go to regular win screen
gameEndLose:   
    jsr redrawBackground            
    jsr drawSpriteBroken            ; draw broken sprite if lose
    jsr swapBuffer

Seven Segment Display

A seven segment display has one main advantage over using sprites to represent numbers: it is a lot faster. Lines being drawn instead of entire images that needed to be read in before even beginning to be drawn shaves off a ton of time. For implementing this, I stored a table to show the coordinates of every line in a single digit based around the lower left corner. Figuring out exactly how I wanted to do that took a few attempts and more than a few sketches.

segmentTable:
    dc.b $3F    ;0
    dc.b $06    ;1
    dc.b $5B    ;2
    dc.b $CF    ;3
    dc.b $66    ;4
    dc.b $6D    ;5
    dc.b $7D    ;6
    dc.b $07    ;7
    dc.b $7F    ;8
    dc.b $67    ;9
    
ledPositionTable
    dc.l $00010003 ; a = (1,0) -> (3,0)
    dc.l $01040304 ; b = (4,1) -> (4,3)  
    dc.l $05040704 ; c = (4,5) -> (4,7) 
    dc.l $08010803 ; d = (1,8) -> (3,8)
    dc.l $05000700 ; e = (0,5) -> (0,7)
    dc.l $03000100 ; f = (0,1) -> (0,3)
    dc.l $04010403 ; g = (1,4) -> (3,4)
    

Once I figured out how I wanted to store each of the lines, drawing them was deceptively easy. To get each digit by itself, I used x86’s version of modulus. Then I looped through each number, and each segment of each number. After that, I used x86’s trap code for drawing a line given two coordinates.

Leave a comment