Back to Hardware

Retro Racer

22 Dec 2018
Progress: Complete

This is page 7 of the Games Console project.

Oscilloscope screenshot of Retro Racer

If you're not familiar with the game, it might not be clear what type of motion we're trying to achieve here. But perhaps this little demo can help out. On your keyboard (sorry mobile readers) use WASD to move (W to accelerate, S to brake, and A and D keys to steer).

Notice that the walls are super slippery. Rubbing against one doesn't just slow you down, it pushes you away. Driving into one of the rounded corners, it will move you potentially an entire tile's width in a direction perpendicular to your velocity. Notice that if you stay pointing in one direction, and drive continously, you'll end up in a position where your direction is perpendicular to the wall you're driving into.

Many, many fan games struggled with this, but it's one of those things that's essential if you want the game to feel right. Even with a top-down zelda game it's a concern, else you're left with the dreaded condition known as "sticky walls".

The way we avoid this is through impulse collisions. In video games, a collision is when two graphics overlap. If they're directly adjacent to each other, that's not a collision as far as we're concerned.

When a collision is detected, we need to move the car away from the wall, and the easiest (and incorrect) way to do that is to move it to its previous position. To do an impulse collision, we first calculate the direction of the reaction force. In the case of retro racer, this will either be horizontal, vertical, or along one of the radius lines of the rounded corners.

Impulse collisions in retro racer

Then, we apply a small impulse in that direction, moving the car maybe half a pixel. Then we check if there is still a collision, and if needed, perform another impulse. Repeated small impulses is the only way to ensure that at the end of the routine, the car will just be in visual contact with the wall. Remember, at top speed, the car could be moving tens of pixels at a time, so a poorly-implemented collision routine could set it ten pixels away from the wall.

To learn more about this implementation, I suggest you take a look at the javascript code for the demo above, which I have syntax-highlighted here for your convenience. You'll notice that in places the resolution is deliberately crippled. Floating point everything would give a very smooth result, but it was more useful to get a preview of how the 8-bit calculations would behave.

<canvas width=256 height=256 id=cnv style="background-color:black"></canvas>

<script>

ctx=document.getElementById('cnv').getContext('2d');
ctx.strokeStyle="#0F0"
ctx.fillStyle="#0F0"

theta=0
x = 70
y = 70
speed_x = 0
speed_y = 0

//1,2,3,4 = corners
//5 = horizontal
//6 = vertical

map=[
[2,1,2,5,5,1],
[6,6,6,2,1,6],
[6,6,6,6,3,4],
[6,3,4,3,5,1],
[3,5,5,5,5,4]
]


offsetX=10
offsetY=30

function main(){
  ctx.clearRect(0,0,256,256)
    
  theta= (256 + theta + 2*(keys[39]-keys[37])) %256
  
  
  if (keys[83]) {
    speed_x+=cos(theta)/8
    speed_y+=sin(theta)/8
  }
  
  if (keys[65]) {
    friction()
  }
  
  x = (256+x+speed_x/256) %256
  y = (256+y+speed_y/256) %256

  var scale=40;
  
  for (var j=map.length;j--;)
   for (var i=map[j].length;i--;) {
     if (x>=i*scale && y>=j*scale && x<(i+1)*scale && y<(j+1)*scale) 
      switch (map[j][i]) {
       case 1: collideCorner(    i*scale,(j+1)*scale); break;
       case 2: collideCorner((i+1)*scale,(j+1)*scale); break;
       case 3: collideCorner((i+1)*scale,    j*scale); break;
       case 4: collideCorner(    i*scale,    j*scale); break;
       case 5: collideHoriz(i*scale,j*scale,scale);break;
       case 6: collideVert (i*scale,j*scale,scale);break;
      }
     if (map[j][i]<5)  drawCurve(offsetX+i*scale,offsetY+j*scale,scale, map[j][i])
     if (map[j][i]==5) drawHoriz(offsetX+i*scale,offsetY+j*scale,scale)
     if (map[j][i]==6) drawVert(offsetX+i*scale,offsetY+j*scale,scale)
   }
  
  drawCar();
  
  ctx.fillText("speed_x:"+speed_x+" speed_y:"+speed_y,10,10)
  requestAnimationFrame(main);
}


function collideCorner(x1,y1){
  var dist, i=10, r=32
  while ((dist=(x-x1)*(x-x1)+(y-y1)*(y-y1))>r*r  &&i--) {
    x-=((x-x1)/r )
    y-=((y-y1)/r )
    friction()
  }
  i=10,r=8
  while ((dist=(x-x1)*(x-x1)+(y-y1)*(y-y1))<r*r  &&i--) {
    x+=((x-x1)/r )
    y+=((y-y1)/r )
    friction()
  }

}
function collideVert(x1,y1,s){
  var i=10;
  while (x<x1+8 &&i--) {
    x++;
    friction()
  }
  i=10;
  while (x>x1+s-8 &&i--) {
    x--;
    friction()
  }
}
function collideHoriz(x1,y1,s){
  var i=10;
  while (y<y1+8 &&i--) {
    y++;
    friction()
  }
  i=10;
  while (y>y1+s-8 &&i--) {
    y--;
    friction()
  }
}
function friction(){
  speed_x-=(speed_x>>4)
  speed_y-=(speed_y>>4)
}


function drawHoriz(x,y,s) {
  ctx.beginPath();
  ctx.moveTo(x,y+4);
  ctx.lineTo(x+s,y+4);
  ctx.moveTo(x,y+s-4);
  ctx.lineTo(x+s,y+s-4);
  ctx.stroke();
}
function drawVert(x,y,s) {
  ctx.beginPath();
  ctx.moveTo(x+s-4,y);
  ctx.lineTo(x+s-4,y+s);
  ctx.moveTo(x+4,y+s);
  ctx.lineTo(x+4,y);
  ctx.stroke();
}

function drawCurve(x,y,r, quadrant){
  
  if (quadrant==1) y+=r;
  if (quadrant==2) x+=r,y+=r;
  if (quadrant==3) x+=r;
  
  
  ctx.beginPath();
  ctx.arc(x,y,r-4,0.5*Math.PI*(-quadrant),0.5*Math.PI*(1-quadrant));
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(x,y,4,0.5*Math.PI*(-quadrant),0.5*Math.PI*(1-quadrant));
  ctx.stroke();
}

function drawCar()  {
  costheta = cos(theta)/20
  sintheta = sin(theta)/20
  
  halfcos=costheta/2
  halfsin=sintheta/2
  
  ctx.beginPath();
  
  ctx.lineTo(Math.round(x+offsetX +halfcos+halfsin), Math.round(y+offsetY +halfsin-halfcos));
  ctx.lineTo(Math.round(x+offsetX -costheta+halfsin), Math.round(y+offsetY -sintheta-halfcos));
  
  ctx.lineTo(Math.round(x+offsetX -costheta-halfsin), Math.round(y+offsetY -sintheta+halfcos));
  ctx.lineTo(Math.round(x+offsetX +halfcos-halfsin), Math.round(y+offsetY +halfsin+halfcos));
  ctx.lineTo(Math.round(x+offsetX +halfcos+halfsin), Math.round(y+offsetY +halfsin-halfcos));
  
  ctx.stroke();
}


keys=[]; keys[37]=keys[39]=keys[83]=0
document.onkeydown=function(e){keys[e.keyCode]=true}
document.onkeyup=function(e){keys[e.keyCode]=false}

function sin(a) {return Math.round(Math.sin(Math.PI*2*a/256)*127)}
function cos(a) {return Math.round(Math.cos(Math.PI*2*a/256)*127)}

main();
</script>

The assembly source code I produced from this is below. A few sacrifices were made, mostly reducing the resolution of the collisions to make the whole thing a little less tedious. For reading two controllers, as we only have one true SPI port, I decided to just bit-bang it. I developed this with just one car, so to add a second I was quite lazy. All of one car's properties are shoved into ram, properties of the other car are shoved into the registers, and the code is run again.

Examine, inspect, scrutinize until satisfied, then promptly proceed to the page about astrolander.


### RetroRacer.asm ###



.equ A_theta    = D_START
.equ A_x_H      = D_START +1
.equ A_x_L      = D_START +2
.equ A_y_H      = D_START +3
.equ A_y_L      = D_START +4
.equ A_speedx_H = D_START +5
.equ A_speedx_L = D_START +6
.equ A_speedy_H = D_START +7
.equ A_speedy_L = D_START +8

.equ B_theta    = D_START +9
.equ B_x_H      = D_START +10
.equ B_x_L      = D_START +11
.equ B_y_H      = D_START +12
.equ B_y_L      = D_START +13
.equ B_speedx_H = D_START +14
.equ B_speedx_L = D_START +15
.equ B_speedy_H = D_START +16
.equ B_speedy_L = D_START +17


.equ MAPDATA = D_START + 20


    .def x_H = r4
    .def x_L = r5
    .def y_H = r6
    .def y_L = r7
    .def speedx_H = r8
    .def speedx_L = r9
    .def speedy_H = r10
    .def speedy_L = r11
    .def theta = r12

    ldi r16,0       ; Disable SPI - we're bitbanging the controllers instead.
    out SPCR,r16

    ;init frame counter
    ldi r16,0
    out TCNT0, r16
    ldi r16, (1<<CS02)|(1<<CS01)|(1<<CS00)
    out TCCR0, r16
    ldi r16, 128
    out OCR0, r16
    
    
    ; Init music
    ldi r16, HIGH(Music*2)
    sts MUSIC_START_H, r16
    ldi r16,  LOW(Music*2)
    sts MUSIC_START_L, r16
    rcall resetMusic
    
    
    ; Load map data into sram
    ldi ZH, HIGH(Map*2)
    ldi ZL,  LOW(Map*2)
    ldi YH, HIGH(MAPDATA)
    ldi YL,  LOW(MAPDATA)
    ldi r16, (5*6)
loadMapLoop:
    lpm r17,Z+
    st Y+,r17
    dec r16
    brne loadMapLoop

    
    
    sts A_x_L, r16
    sts A_speedx_H, r16
    sts A_speedx_L, r16

    sts A_y_L, r16
    sts A_speedy_H, r16
    sts A_speedy_L, r16
    

    sts B_x_L, r16
    sts B_speedx_H, r16
    sts B_speedx_L, r16

    sts B_y_L, r16
    sts B_speedy_H, r16
    sts B_speedy_L, r16
    
    ldi r16,128
    sts A_x_H, r16
    sts B_x_H, r16
    sts A_theta, r16
    sts B_theta, r16
        
    ldi r16, 170
    sts A_y_H, r16
    ldi r16, 185
    sts B_y_H, r16
    
    
Main:
    rcall readTwoControllers

    ; Apply user input to both cars
    ; Change direction
    ldi r16, 2 ; turn speed
    lds theta, A_theta
    sbrc r2, ctrlLeft
    add theta,r16
    sbrc r2, ctrlRight
    sub theta,r16
    sts A_theta, theta

    lds theta, B_theta
    sbrc r3, ctrlLeft
    add theta,r16
    sbrc r3, ctrlRight
    sub theta,r16
    sts B_theta, theta

    ; Apply accelerator
    sbrs r2, ctrlA
    rjmp checkAccelB

    lds r17, A_theta
    ldi r16, 16                 ; acceleration magnitude
    lds speedy_H, A_speedy_H
    lds speedy_L, A_speedy_L
    rcall sinS                  ; returns in r1:r0
    sbrc r0,7
    inc r1
    add speedy_L, r1
    ldi r16,0
    sbrc r1,7
    ldi r16,-1
    adc speedy_H, r16   
    sts A_speedy_H, speedy_H
    sts A_speedy_L, speedy_L

    lds r17, A_theta
    ldi r16, 16                 ; acceleration magnitude
    lds speedx_H, A_speedx_H
    lds speedx_L, A_speedx_L
    rcall cosS                  ; returns in r1:r0
    sbrc r0,7
    inc r1
    add speedx_L, r1
    ldi r16,0
    sbrc r1,7
    ldi r16,-1
    adc speedx_H, r16
    sts A_speedx_H, speedx_H
    sts A_speedx_L, speedx_L


checkAccelB:
    sbrs r3, ctrlA
    rjmp checkBrakeA
    
    lds r17, B_theta
    ldi r16, 16                 ; acceleration magnitude
    lds speedy_H, B_speedy_H
    lds speedy_L, B_speedy_L
    rcall sinS                  ; returns in r1:r0
    sbrc r0,7
    inc r1
    add speedy_L, r1
    ldi r16,0
    sbrc r1,7
    ldi r16,-1
    adc speedy_H, r16   
    sts B_speedy_H, speedy_H
    sts B_speedy_L, speedy_L

    lds r17, B_theta
    ldi r16, 16                 ; acceleration magnitude
    lds speedx_H, B_speedx_H
    lds speedx_L, B_speedx_L
    rcall cosS                  ; returns in r1:r0
    sbrc r0,7
    inc r1
    add speedx_L, r1
    ldi r16,0
    sbrc r1,7
    ldi r16,-1
    adc speedx_H, r16
    sts B_speedx_H, speedx_H
    sts B_speedx_L, speedx_L


    ; Apply brake
checkBrakeA:
    sbrs r2, ctrlB
    rjmp checkBrakeB
    
    rcall loadAllA
    rcall friction
    rcall storeAllA

checkBrakeB:
    sbrs r3, ctrlB
    rjmp newPosition
    
    rcall loadAllB
    rcall friction
    rcall storeAllB

    ; End of user input sequence.

newPosition:
    ; Increment position by speed amount
    rcall loadAllA
    add x_L, speedx_L
    adc x_H, speedx_H
    add y_L, speedy_L
    adc y_H, speedy_H
    rcall storeAllA

    rcall loadAllB
    add x_L, speedx_L
    adc x_H, speedx_H
    add y_L, speedy_L
    adc y_H, speedy_H
    rcall storeAllB

    ; Draw the map and check for collisions.
    ldi YH, HIGH(MAPDATA)
    ldi YL,  LOW(MAPDATA)
    ldi r27, (5*6)
    ldi r16,0
    ldi r17,0
mapLoop:
    ld r18, Y+  ; Load tile

    rcall drawTile
    
    mov r19,r16         ; Tiles are 40 pixels on a side. 
    mov r20,r17         ; Store the opposing coordinate in r19,r20
    subi r19,-40
    subi r20,-40

    rcall loadAllA
    rcall doCollisions
    rcall storeAllA

    rcall loadAllB
    rcall doCollisions
    rcall storeAllB

    subi r16, -40       ; Move to next tile
    cpi r16, (6*40)     ; if at the end of the row,
    brne mapLoopEnd
    ldi r16,0           ; reset X and increase Y
    subi r17, -40

mapLoopEnd:
    dec r27             ; keep going until the end of the map
    brne mapLoop

    lds theta, A_theta
    lds x_H, A_x_H
    lds x_L, A_x_L
    lds y_H, A_y_H
    lds y_L, A_y_L
    rcall drawCar

    lds theta, B_theta
    lds x_H, B_x_H
    lds x_L, B_x_L
    lds y_H, B_y_H
    lds y_L, B_y_L
    rcall drawCar
    
    
    call processAudio
    
    ldi r16,0
    out PORTA,r16
    out PORTC, r16
waitforframe:
    in r16, TIFR
    sbrs r16, OCF0
    rjmp waitforframe
    ldi r16, (1<<OCF0)
    out TIFR, r16
    ldi r16, 0
    out TCNT0, r16
    rjmp Main



doCollisions:
    ;i=r16
    ;j=r17
    ;ni=r19
    ;nj=r20
    
    
    cp x_H,r16
    brcs doCollisionEnd ; branch if x_H<r16 
    cp y_H,r17
    brcs doCollisionEnd ; branch if y_H<r17
    
    cp x_H,r19
    brcc doCollisionEnd
    cp y_H,r20
    brcc doCollisionEnd
    
    ;rcall drawTile ; highlight the tile we're in
    ;rcall drawTile
    
    cpi r18,1
    brne collisionCheck2
    mov r21,r16
    mov r22,r20
    rjmp doCurvedWall
collisionCheck2:
    cpi r18,2
    brne collisionCheck3
    mov r21,r19
    mov r22,r20 
    rjmp doCurvedWall
collisionCheck3:
    cpi r18,3
    brne collisionCheck4
    mov r21,r19
    mov r22,r17
    rjmp doCurvedWall
collisionCheck4:
    cpi r18,4
    brne collisionCheckOther
    mov r21,r16
    mov r22,r17
    rjmp doCurvedWall
collisionCheckOther:
    cpi r18,5
    breq doHorizontalWall
    cpi r18,6
    breq doVerticalWall
    
doCollisionEnd:

    ret

doHorizontalWall:
    mov r22,r17
    subi r22,-8
    cp y_H,r22
    brcc collideHorizBottom
    inc y_H
    rcall friction
    rjmp doHorizontalWall
    
collideHorizBottom:
    mov r22, r17
    subi r22, -32
    cp r22,y_H
    brsh doHorizontalEnd
    dec y_H
    rcall friction
    rjmp collideHorizBottom
doHorizontalEnd:
    ret

doVerticalWall:
    mov r22,r16
    subi r22,-8
    cp x_H,r22
    brcc collideVerticalRight
    inc x_H
    rcall friction
    rjmp doVerticalWall

collideVerticalRight:
    mov r22,r16
    subi r22,-32
    cp r22,x_H
    brsh doVerticalEnd
    dec x_H
    rcall friction
    rjmp collideVerticalRight
    
doVerticalEnd:
    ret

; Function doCurvedWall
; r21, r22 is the centre of curvature
doCurvedWall:
    rcall calcDistance
    ldi r24,  LOW(32*32)
    ldi r25, HIGH(32*32)    ; radius is 32
    cp  r0, r24
    cpc r1, r25
    brcs checkInnerWall
    
    mov r24,x_H
    sub r24, r21            ; divide by 32 (radius)
    asr r24
    asr r24
    asr r24
    asr r24
    asr r24
    brcc dontRound1
    inc r24
dontRound1:
    sub x_H,r24
    
    mov r24,y_H
    sub r24,r22
    asr r24
    asr r24
    asr r24
    asr r24
    asr r24
    brcc dontRound2
    inc r24
dontRound2:
    sub y_H,r24
    
    rcall friction
    rjmp doCurvedWall
    
checkInnerWall:
    rcall calcDistance
    ldi r24, LOW(8*8)
    ldi r25,HIGH(8*8)
    cp r0, r24
    cpc r1, r25
    brcc doCurvedWallEnd
        
    mov r24,x_H
    sub r24, r21            ; divide by 8 (radius)
    asr r24
    asr r24
    asr r24
    brcc dontRound3
    inc r24
dontRound3:
    add x_H,r24
    
    mov r24,y_H
    sub r24,r22
    asr r24
    asr r24
    asr r24
    brcc dontRound4
    inc r24
dontRound4:
    add y_H,r24
    
    rcall friction
    rjmp checkInnerWall
    
doCurvedWallEnd:
    ret

; function calcDistance
; Works out the squared distance between x_H,y_H and r21,r22
calcDistance:

    mov r26,x_H
    sub r26,r21     ; Difference between x coordinates
    muls r26,r26    ; square it
    movw r24,r0     ; save for later
    mov r26,y_H     
    sub r26,r22     ; difference in y coordinates
    muls r26,r26    ; square it
    add r0,r24      ; add two values together
    adc r1,r25      ; (return in r1:r0)
    
    ret


; Function friction
; subtracts one sixteenth of current speed from speed
friction:
    push r16
    push r17
    mov r16, speedx_L
    mov r17, speedx_H
    asr r17             ; arithmetic shift (speed is signed)
    ror r16
    asr r17
    ror r16
    asr r17
    ror r16
    asr r17
    ror r16
    sub speedx_L, r16
    sbc speedx_H, r17
    mov r16, speedy_L
    mov r17, speedy_H
    asr r17
    ror r16
    asr r17
    ror r16
    asr r17
    ror r16
    asr r17
    ror r16
    sub speedy_L, r16
    sbc speedy_H, r17
    pop r17
    pop r16
    ret

; Function drawTile(x=r16, y=r17, tile=r18)
; draws tile data from prog mem
drawTile:
    push r16
    push r17
    
    ldi ZH, HIGH(Tileset*2-162) ; Each tile is 162 bytes apart in prog mem.
    ldi ZL,  LOW(Tileset*2-162) ; (tile number starts at 1, so subtract a tile)
    ldi r19, 162            ; multiply tile number (r18) by 162
    mul r19, r18
    add ZL, r0              ; add to Z pointer
    adc ZH, r1
    subi r16, -10           ; draw to screen with an offset
    subi r17, -30   

    
drawTileLoop:
    lpm r19, Z+             ; load X coordinate
    cpi r19, -1
    breq drawTileEnd        ; -1 signals end of tile data

    add r19, r16            ; combine with tile location
    out PORTA, r19          ; output
    lpm r19, Z+             ; load Y coordinate
    add r19, r17            ; combine with tile location
    out PORTC, r19          ; output
    
    rcall delayPixel
    
    rjmp drawTileLoop
drawTileEnd:
    pop r17
    pop r16
    ret



drawCar:
    ; Although we're plotting pixels, let's stay in 16bit as long
    ; as possible to reduce the rounding errors.

    ;out PORTA, x_H
    ;out PORTC, y_H
    ;ret
    
    ldi r16,8
    mov r17, theta
    rcall sinS
    movw r30, r0        ; r31:r30 is now sintheta
    ldi r16,8
    mov r17, theta
    rcall cosS
    movw r28, r0        ; r29:r28 is now costheta
    

    
    movw r26, r30       ; r27:r26 is halfsintheta
    asr r27
    ror r26
    movw r24, r28       ; r25:r24 is halfcostheta
    asr r25
    ror r24

    ldi r16,10  
    adc x_H, r16        ; shift coords by drawing offset
    ldi r16, 30         ; 10 and 30 pixels.
    add y_H, r16

    ; 1st coordinate is loaded into x=r16, y=r17

    mov r20, x_L    ; x +halfcos  +halfsin
    mov r16, x_H    ; use r20 as temporary low byte
    add r20, r24
    adc r16, r25
    add r20, r26
    adc r16, r27
    sbrc r20,7      ; round to 8 bit
    inc r16

    mov r20, y_L
    mov r17, y_H    ; y +halfsin  -halfcos
    add r20, r26
    adc r17, r27
    sub r20, r24
    sbc r17, r25
    sbrc r20,7      ; round to 8 bit
    inc r17

    push r16        ; save starting coordinates for later
    push r17

    ; 2nd coordinate is loaded into x=r18, y=r19
    
    mov r20, x_L
    mov r18, x_H    ; x -costheta +halfsin
    sub r20, r28
    sbc r18, r29
    add r20, r26
    adc r18, r27
    sbrc r20,7
    inc r18

    mov r20, y_L
    mov r19, y_H    ; y -sintheta -halfcos
    sub r20, r30
    sbc r19, r31
    sub r20, r24
    sbc r19, r25
    sbrc r20,7
    inc r19

    rcall drawLine  ; Draw the first line of the rectangle!

    ; 3rd, 4th, 5th coordinates go into r18, r19 again.

    mov r20, x_L
    mov r18, x_H    ; x -costheta -halfsin
    sub r20, r28
    sbc r18, r29
    sub r20, r26
    sbc r18, r27
    sbrc r20,7
    inc r18

    mov r20, y_L
    mov r19, y_H    ; y -sintheta +halfcos
    sub r20, r30
    sbc r19, r31
    add r20, r24
    adc r19, r25
    sbrc r20,7
    inc r19

    rcall drawLine

    mov r20, x_L
    mov r18, x_H    ; x +halfcos  -halfsin
    add r20, r24
    adc r18, r25
    sub r20, r26
    sbc r18, r27
    sbrc r20,7
    inc r18

    mov r20, y_L
    mov r19, y_H    ; y +halfsin  +halfcos
    add r20, r26
    adc r19, r27
    add r20, r24
    adc r19, r25
    sbrc r20,7
    inc r19

    rcall drawLine

    pop r19         ; recover starting position 
    pop r18
    rcall drawLine  ; and complete the rectangle.

    ret



; Load All / Store All functions.
; Moves data to/from memory for either car.
loadAllA:
    lds theta, A_theta
    lds x_H, A_x_H
    lds x_L, A_x_L
    lds speedx_H, A_speedx_H
    lds speedx_L, A_speedx_L
    lds y_H, A_y_H
    lds y_L, A_y_L
    lds speedy_H, A_speedy_H
    lds speedy_L, A_speedy_L
    ret
storeAllA:
    sts A_theta, theta
    sts A_x_H, x_H
    sts A_x_L, x_L
    sts A_speedx_H, speedx_H
    sts A_speedx_L, speedx_L
    sts A_y_H, y_H
    sts A_y_L, y_L
    sts A_speedy_H, speedy_H
    sts A_speedy_L, speedy_L
    ret 

loadAllB:
    lds theta, B_theta
    lds x_H, B_x_H
    lds x_L, B_x_L
    lds speedx_H, B_speedx_H
    lds speedx_L, B_speedx_L
    lds y_H, B_y_H
    lds y_L, B_y_L
    lds speedy_H, B_speedy_H
    lds speedy_L, B_speedy_L
    ret
storeAllB:
    sts B_theta, theta
    sts B_x_H, x_H
    sts B_x_L, x_L
    sts B_speedx_H, speedx_H
    sts B_speedx_L, speedx_L
    sts B_y_H, y_H
    sts B_y_L, y_L
    sts B_speedy_H, speedy_H
    sts B_speedy_L, speedy_L
    ret 



readTwoControllers:
    push r16
    push r17
    push r18
    push r19
    ; PIN 1 is CLOCK
    ; PIN 2 is LATCH
    ; PIN 3 is DATA INPUT for controller 1
    ; PIN 4 is DATA INPUT for controller 2
    
    ldi r16,0
    ldi r18,0
    ldi r19,0
    out PORTB, r16  ; Set latch low 
    rcall readControllerDelay
    
    
    ldi r17,8       ; eight clock pulses

readTwoControllersLoop:
    
    in r16,PINB
    
    lsl r18
    sbrs r16, 3     ; read controller 1 data
    sbr r18,1
    
    lsl r19
    sbrs r16, 4     ; read controller 2 data
    sbr r19,1
    
    ldi r16, 0b00000010 ; Clock high
    out PORTB,r16
    rcall readControllerDelay
    ldi r16, 0b00000000 ; Clock low
    out PORTB,r16
    rcall readControllerDelay
    
    dec r17
    brne readTwoControllersLoop
    
    ldi r16, 0b00000100 ; Latch high for next time
    out PORTB,r16
    
    mov r2,r18
    mov r3,r19
    
    pop r19
    pop r18
    pop r17
    pop r16
    ret
    
readControllerDelay:
    ldi r16,255
readControllerDelayLoop:
    dec r16
    brne readControllerDelayLoop
    ret



/*  Function drawLine (x0,y0,x1,y1)
 *  My implementation of Bresenham line algorithm
 *  based on pseudocode/description from wikipedia
 */
drawLine:

    ;Function arguments
.def x0=r16
.def y0=r17
.def dx=r18
.def dy=r19
    
    ;Local variables
    push r20
    push r21
    push r22
    push r23
    push r24
    push r25
.def slowx=r20
.def slowy=r21
.def fastx=r22
.def fasty=r23
.def err = r24
.def t   = r25

    ; First we split the magnitude and direction
    ; Need to be careful/explicit because mag could be >127

    cp dx,x0
    breq lineXeq
    brcs lineXneg       ; Carry flag set if it overflowed
    
    ldi slowx,1         ; slowx is the direction
    sub dx,x0           ; dx is now the magnitude
    rjmp lineY

lineXneg:
    ldi slowx,-1
    mov t, x0           ; t as temp var
    sub t, dx
    mov dx, t
    rjmp lineY

lineXeq:
    clr slowx
    clr dx
    
lineY:                  ; Now do the same for y
    cp dy,y0
    breq lineYeq
    brcs lineYneg
    
    ldi slowy,1
    sub dy,y0
    rjmp lineCheckDistance

lineYneg:
    ldi slowy,-1
    mov t, y0           ; t as temp var
    sub t, dy
    mov dy, t
    rjmp lineCheckDistance

lineYeq:
    clr slowy
    clr dy

lineCheckDistance:      ; Now to find which direction is "faster"
    cp dx, dy
    brcs lineDxLessthanDy
    
    mov fastx, slowx    ; x is the fast direction
    clr fasty

    eor dx, dy          ; Tripple XOR, the classy way to
    eor dy, dx          ; swap two registers
    eor dx, dy

    rjmp lineInit   

lineDxLessthanDy:
    mov fasty, slowy    ; y is the fast direction, no need to swap
    clr fastx

lineInit:
    mov err, dy         ; set err to half the furthest distance
    lsr err
    
    out PORTA, x0       ; We are now ready to draw the line
    out PORTC, y0
    rcall delayPixel



    mov t, dy

lineNextPixel:
    sub err, dx         ; Update error term
    brcc lineStepParallel

    add err, dy         ; Make error term positive again
    add x0,slowx        ; Step diagonally
    add y0,slowy
    
    rjmp lineLoopBack

lineStepParallel:
    add x0,fastx        ; Step parallel
    add y0,fasty

lineLoopBack:
    out PORTA, x0       ; Plot point
    out PORTC, y0
    rcall delayPixel

    dec t
    brne lineNextPixel

    pop r25
    pop r24
    pop r23
    pop r22
    pop r21
    pop r20

    ret                 ; End of drawLine

delayPixel:
    push r16
    ldi r16,12
delayPixelLoop:
    dec r16
    brne delayPixelLoop
    pop r16
    ret

/* Function sinS(coordinate, dir)
 * Scales 8bit signed coordinate by sine of the direction
 * Simplified, no rounding, returns in r1:r0
 */
sinS:
    .def cor=r16
    .def dir=r17
    push ZH
    push ZL
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    sub ZL, dir
    sbci ZH, $00            ; sub-with-carry 0 from high byte
    lpm dir, Z
    fmuls cor,dir
    pop ZL
    pop ZH
    ret

/* Function cosS(coordinate, dir)
 * Scales 8bit signed coordinate by cosine of the direction
 */
cosS:
    push ZH
    push ZL
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)   ; reuse table, but shift by pi/2
    subi dir, 192                   ; which in our notation is 64 (-192)
    sub ZL, dir
    sbci ZH, $00
    lpm dir, Z
    fmuls cor,dir
    pop ZL
    pop ZH
    ret

; signed values, in reverse order
sineTable: 
.db $FD,$FA,$F7,$F4,$F0,$ED,$EA,$E7,$E4,$E1,$DE,$DB,$D8,$D5,$D2,$CF,$CD,$CA,$C7,$C4,$C1,$BF,$BC,$B9,$B7,$B4,$B2,$AF,$AD,$AB,$A8,$A6,$A4,$A2,$A0,$9E,$9C,$9A,$98,$96,$95,$93,$91,$90,$8F,$8D,$8C,$8B,$8A,$88,$87,$86,$86,$85,$84,$83,$83,$82,$82,$82,$81,$81,$81,$81,$81,$81,$81,$82,$82,$82,$83,$83,$84,$85,$86,$86,$87,$88,$8A,$8B,$8C,$8D,$8F,$90,$91,$93,$95,$96,$98,$9A,$9C,$9E,$A0,$A2,$A4,$A6,$A8,$AB,$AD,$AF,$B2,$B4,$B7,$B9,$BC,$BF,$C1,$C4,$C7,$CA,$CD,$CF,$D2,$D5,$D8,$DB,$DE,$E1,$E4,$E7,$EA,$ED,$F0,$F4,$F7,$FA,$FD,$00,$03,$06,$09,$0C,$10,$13,$16,$19,$1C,$1F,$22,$25,$28,$2B,$2E,$31,$33,$36,$39,$3C,$3F,$41,$44,$47,$49,$4C,$4E,$51,$53,$55,$58,$5A,$5C,$5E,$60,$62,$64,$66,$68,$6A,$6B,$6D,$6F,$70,$71,$73,$74,$75,$76,$78,$79,$7A,$7A,$7B,$7C,$7D,$7D,$7E,$7E,$7E,$7F,$7F,$7F,$7F,$7F,$7F,$7F,$7E,$7E,$7E,$7D,$7D,$7C,$7B,$7A,$7A,$79,$78,$76,$75,$74,$73,$71,$70,$6F,$6D,$6B,$6A,$68,$66,$64,$62,$60,$5E,$5C,$5A,$58,$55,$53,$51,$4E,$4C,$49,$47,$44,$41,$3F,$3C,$39,$36,$33,$31,$2E,$2B,$28,$25,$22,$1F,$1C,$19,$16,$13,$10,$0C,$09,$06,$03,$00


Map:

.db 2,1,2,5,5,1
.db 6,6,6,2,1,6
.db 6,6,6,6,3,4
.db 6,3,4,3,5,1
.db 3,5,5,5,5,4



Tileset:
.db 0,3,1,3,2,3,3,3,4,3,5,4,6,4,7,4,8,4,9,4,10,5,11,5,12,5,13,6,14,6,15,7,16,7,17,8,18,8,19,9,20,9,21,10,22,11,23,12,24,13,25,13,26,14,26,15,27,16,28,17,29,18,30,19,30,20,31,21,31,22,32,23,32,24,33,25,33,26,34,27,34,28,34,29,35,30,35,31,35,32,35,33,35,34,36,35,36,36,36,37,36,38,36,39,0,36,1,37,2,37,2,38,3,39,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.db 39,3,38,3,37,3,36,3,35,3,34,4,33,4,32,4,31,4,30,4,29,5,28,5,27,5,26,6,25,6,24,7,23,7,22,8,21,8,20,9,19,9,18,10,17,11,16,12,15,13,14,13,13,14,13,15,12,16,11,17,10,18,9,19,9,20,8,21,8,22,7,23,7,24,6,25,6,26,5,27,5,28,5,29,4,30,4,31,4,32,4,33,4,34,3,35,3,36,3,37,3,38,3,39,39,36,38,37,37,37,37,38,36,39,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.db 39,36,38,36,37,36,36,36,35,36,34,35,33,35,32,35,31,35,30,35,29,34,28,34,27,34,26,33,25,33,24,32,23,32,22,31,21,31,20,30,19,30,18,29,17,28,16,27,15,26,14,26,13,25,13,24,12,23,11,22,10,21,9,20,9,19,8,18,8,17,7,16,7,15,6,14,6,13,5,12,5,11,5,10,4,9,4,8,4,7,4,6,4,5,3,4,3,3,3,2,3,1,3,0,39,3,38,2,37,2,37,1,36,0,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.db 0,36,1,36,2,36,3,36,4,36,5,35,6,35,7,35,8,35,9,35,10,34,11,34,12,34,13,33,14,33,15,32,16,32,17,31,18,31,19,30,20,30,21,29,22,28,23,27,24,26,25,26,26,25,26,24,27,23,28,22,29,21,30,20,30,19,31,18,31,17,32,16,32,15,33,14,33,13,34,12,34,11,34,10,35,9,35,8,35,7,35,6,35,5,36,4,36,3,36,2,36,1,36,0,0,3,1,2,2,2,2,1,3,0,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
.db 0,3,1,3,2,3,3,3,4,3,5,3,6,3,7,3,8,3,9,3,10,3,11,3,12,3,13,3,14,3,15,3,16,3,17,3,18,3,19,3,20,3,21,3,22,3,23,3,24,3,25,3,26,3,27,3,28,3,29,3,30,3,31,3,32,3,33,3,34,3,35,3,36,3,37,3,38,3,39,3,0,36,1,36,2,36,3,36,4,36,5,36,6,36,7,36,8,36,9,36,10,36,11,36,12,36,13,36,14,36,15,36,16,36,17,36,18,36,19,36,20,36,21,36,22,36,23,36,24,36,25,36,26,36,27,36,28,36,29,36,30,36,31,36,32,36,33,36,34,36,35,36,36,36,37,36,38,36,39,36,-1,-1
.db 3,0,3,1,3,2,3,3,3,4,3,5,3,6,3,7,3,8,3,9,3,10,3,11,3,12,3,13,3,14,3,15,3,16,3,17,3,18,3,19,3,20,3,21,3,22,3,23,3,24,3,25,3,26,3,27,3,28,3,29,3,30,3,31,3,32,3,33,3,34,3,35,3,36,3,37,3,38,3,39,36,0,36,1,36,2,36,3,36,4,36,5,36,6,36,7,36,8,36,9,36,10,36,11,36,12,36,13,36,14,36,15,36,16,36,17,36,18,36,19,36,20,36,21,36,22,36,23,36,24,36,25,36,26,36,27,36,28,36,29,36,30,36,31,36,32,36,33,36,34,36,35,36,36,36,37,36,38,36,39,-1,-1

    
.include "audio.asm"
Music:
.include "Music/MegaMan.asm"

.include "Bootloader.asm"



The adventure continues on page 8.