Sprite Collision Routine
Collision detection between sprites is something games do constantly, so it's obviously in our best interest to be as efficient as possible. This routine tests for collisions by seeking their opposite—that is, checking whether two sprites are outside the other's four boundaries. This enables the code to break out the instant it knows a collision is impossible.
To begin with, let's assume we've reserved space in zero page RAM for the x and y coordinates of the two sprites we're checking, and also for each one's height and width. There are numerous other ways to track these values—we might maintain an object pool, or we might even assume all sprites are sixteen pixels wide or something like that—but for simplicity's sake, in this example we'll use plain old variables.
.segment "ZEROPAGE"
; sprite 1 x position, y position, width, and height
x1: .res 1
y1: .res 1
w1: .res 1
h1: .res 1
; sprite 2 x position, y position, width, and height
x2: .res 1
y2: .res 1
w2: .res 1
h2: .res 1 Now for the collision detection code. I'll give the entire subroutine here, then break down how it works chunk by chunk. Remember, each step of the way, we're checking if the two sprites aren't colliding.
.proc collision_check
LDA x1
CLC
ADC w1
CMP x2
BCC @noCollide
LDA x2
CLC
ADC w2
CMP x1
BCC @noCollide
LDA y1
CLC
ADC h1
CMP y2
BCC @noCollide
LDA y2
CLC
ADC h2
CMP y1
BCC @noCollide
SEC
RTS
@noCollide:
CLC
RTS
.endproc As the appearance of the code might suggest, it's made up of six blocks. Let's break it down.
LDA x1
CLC
ADC w1
CMP x2
BCC @noCollide We start by loading the x position of sprite 1 into A and adding the width of sprite 1, thus giving us the position of sprite 1's right edge.
Now that A stores sprite 1's right edge, we compare it to sprite 2's x position—that is, the position of sprite 2's left edge. Finally we perform a Branch if Carry Clear, which breaks us out if the Carry Flag is 0.
Why? Because CMP works by subtracting x2 from A, and setting the Carry Flag only if x2 is less than A. For our purposes, that would mean the left edge of sprite 2 is to the left of the right edge of sprite 1—so either sprite 2 is way to the left of sprite 1, or they're colliding. Either way we should carry on with our test. But if x2 is greater than what's currently in A (the right edge of sprite 1) then sprite 2 is somewhere to the right of sprite 1, so they can't be colliding, and we can skip the rest of the checks and get out of here. This same logic will be used for the other three checks.
LDA x2
CLC
ADC w2
CMP x1
BCC @noCollide To check the opposite x boundaries, we load the left side of sprite 2 into A and add its width, giving us the right side of sprite 2. Then we compare that to the left side of sprite 1. Again, if sprite 1 is to the right of sprite 2, we break out of the test. If not, we've now proven that there is a collision on the x-axis—neither sprite is to the right of the other one. Now we have to prove that they're colliding on the y-axis as well.
LDA y1
CLC
ADC h1
CMP y2
BCC @noCollide
LDA y2
CLC
ADC h2
CMP y1
BCC @noCollide These two chunks are nicely analogous to the ones we just examined, only checking y coordinates instead of x. First we see if sprite 2 is below sprite 1 (i.e. its y coordinate is greater than sprite 1's y coordinate plus height), then we see if sprite 1 is below sprite 2. If either of those is true, they can't be colliding, so we break out.
SEC
RTS These two lines come after the final BCC @noCollide and just before the @noCollide: label itself. It other words, here's what runs if we never broke out of the test and the two sprites actually are colliding. All that happens here is SEC, Set Carry Flag, and a Return From Subroutine. In other words, we're using the Carry Flag as a way to alert whatever code called this subroutine that a collision did occur. The calling code will need to check the Carry Flag immediately upon return, before this information gets corrupted by other operations.
@noCollide:
CLC
RTS And finally, the label we branch to if at any point our routine determines that a collision is impossible. It makes sure the Carry Flag is 0 with a Clear Carry Flag and returns. Note that this is the flipside of the SEC command a few lines earlier; again, whatever code called this subroutine will need to immediately check the Carry Flag.