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