Retro Racer

22 Dec 2018
Progress: Complete

This is page 7 of the Games Console project.

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.

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();
}

ctx.beginPath();
ctx.stroke();
ctx.beginPath();
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)
lpm r17,Z+
st Y+,r17
dec r16

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:

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

lds theta, B_theta
sbrc r3, ctrlLeft
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
ldi r16,0
sbrc r1,7
ldi r16,-1
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
ldi r16,0
sbrc r1,7
ldi r16,-1
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
ldi r16,0
sbrc r1,7
ldi r16,-1
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
ldi r16,0
sbrc r1,7
ldi r16,-1
sts B_speedx_H, speedx_H
sts B_speedx_L, speedx_L

; Apply brake
checkBrakeA:
sbrs r2, ctrlB
rjmp checkBrakeB

rcall friction
rcall storeAllA

checkBrakeB:
sbrs r3, ctrlB
rjmp newPosition

rcall friction
rcall storeAllB

; End of user input sequence.

newPosition:
; Increment position by speed amount
rcall storeAllA

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 doCollisions
rcall storeAllA

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:

mov r24,y_H
sub r24,r22
asr r24
asr r24
asr r24
brcc dontRound4
inc r24
dontRound4:

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
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
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.

; 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
sbrc r20,7      ; round to 8 bit
inc r16

mov r20, y_L
mov r17, y_H    ; y +halfsin  -halfcos
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
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
sbrc r20,7
inc r19

rcall drawLine

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

mov r20, y_L
mov r19, y_H    ; y +halfsin  +halfcos
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.
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

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

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

ldi r17,8       ; eight clock pulses

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
ldi r16, 0b00000000 ; Clock low
out PORTB,r16

dec r17

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

ldi r16,255
dec r16
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

rjmp lineLoopBack

lineStepParallel:

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:

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"