Sprites in Assembly III - Collision detection
This will be a fairly short tutorial on how to implement Sprite collision detection using Commander X16 video hardware - VERA chip. There are many ways to implement collision detection and mostly it comes down to compromises and most games in the 8 bit era used proximity calculations or bounding boxes collisions. Very rarely developers could afford to do pixel perfect collision detection. With the appearance of more and more advanced hardware with specialized video chips that natively supported hardware sprites we also got first hardware collision detection. Commodore 64 was one of the first and even though it was by no means perfect it was still a big step forward.
Commander X16 is no exception in this regard. It has very powerful hardware sprite support but collision detection is not perfect but if used in a smart way it can still be extremely useful and can reduce the amount of code we need to write to make games play fair.
Let’s get some things straight first:
- VERA can’t detect collisions between sprites and tiles
- We can only detect collisions using Interrupt handler
- We have to plan Sprite grouping carefully to take advantage of Collision Masks
Luckily we already discussed the Interrupt handler in a previous article about Sprite Animation in Assembly (here) so we will just need to add collision handling code, everything else can stay the same.
Turning On Sprite Collision detection
We were using VSYNC interrupt before. That is the interrupt that is also triggered by VERA about 60 times per second at the end of screen update. The VSYNC interrupt is turned on by default.
PRINT PEEK($9F26)
Returns 65 or 0100 0001 in binary on my current version of Emulator. As we see the VSYNC flag is set in Bit 0. Let’s look at the rest of the register $9F26 in VERA which is called IEN (Interrupt Enable). For the future I will also include an ISR register which looks suspiciously similar because indeed they are very much related and work in tandem.
Register | Address | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|---|---|
6 | $9F26 | IEN | IRQ Bit 8 | AFLOW | SPRCOL | LINE | VSYNC | |||
7 | $9F27 | ISR | Sprite Collisions | AFLOW | SPRCOL | LINE | VSYNC |
Sure enough, Bit 0 is VSYNC. The one that is used for Sprite Collisions is Bit 2 - SPRCOL.So this is really the only setting (beside sprites themselves) that we need to set up in order to use VERA’s hardware collision detection, like this:
ora #%00000100
sta $9F26 ; set SPRCOL ON
Collision Mask
This is a very simple but potentially powerful model of controlling which collisions we want to detect. I didn’t analyze the VERA emulator source code but based on my testing it runs bitwise AND function when sprites collide to decide if the collision will trigger an interrupt or not. For example, if Sprite 1 has a Collision mask of 0011 the collision with another Sprite with collision masks 0001, 0010 or 0011 will trigger a collision but it will not trigger the collision with Sprite that has masks 0100, 1000 or 1100. Collision Mask 0000 does not trigger any collisions even between two sprites both with mask 0000.
This allows us, for example, to separate sprites used for HUD or some foreground parallax items or even explosions that are in the background and by not triggering collision interrupts and greatly reduce the amount of code that needs to handle them.
I expect coders will come with some really clever ways to use this functionality.
Graphics
I prepared simple spaceship graphics for this demo and used some of the VERA functionality to make it more interesting. Graphics is the same but the difference between “player” and enemy is different palette plus enemy sprite is drawn using Vertical Flip flag. To indicate when the sprites are in collision I switch the palette of the player to visually show it.
Implementation
We are using some of the knowledge learned during previous tutorials for following functions:
LoadAssets
Loading sprite data from CPU memory to VRAM - only once because the same graphics is used by both Player and Enemy.
ConfigureSprites
This function activates Sprite 1 for player and Sprite 2 for Enemy and sets the default starting locations. Collision Mask for Player is set to 0011 and for Enemy to 0001 so they should trigger collision interrupt.
InitScreen
This function turns on Sprite Collision detection by setting SPRCOL flag to 1. It also enables Sprites overall by setting Sprite Enable on in the Video register ($9F29). It then sets the Horizontal and Vertical scale to 2x to essentially set screen resolution to 320x240 (40x30 tiles) and finally clears the screen and sets color to black background.
Main Program
Main program is probably the simplest part of this whole demo. It just inserts our custom Interrupt handler and then reads the Joystick input in an endless loop. Of course in the actual game we could perform all kinds of asynchronous tasks here.
This is in fact a beginning of game loop where we could manage all kind of game states and changes and we see how simple it is when we can let hardware do big part of work for us.
Interrupt handler
This is the area where the magic happens and we will analyze it in more detail.
First thing to remember is that we have two types of interrupts set up VSYNC and SPRCOL so we have to make sure that we first identify which interrupt we are actually processing. We do that by checking the register ISR ($9F27) we listed in the beginning.
The one (or more) that triggered it sets the bit in the appropriate position which for SPRCOL is bit 2 and for VSYNC is bit 0. One of the ways to select and process the correct interrupt is like this:
lda $9F27
and #SPRCOL
beq checkVSYNC
lda $9F27
and #VSYNC
bne ProcessVSYNC
jmp (OLD_IRQ_HANDLER)
Not sure if it is possible that more than one interrupt is triggered at a time but above example would process both if two of the flags were set. Of course SPRCOL is defined as 4 (bit 2 set) and VSYNC as 1 (bit 0 set).
We have to be mindful of the processing we intend to execute inside the interrupt handler. The following image shows when the interrupts are triggered:
Now imagine if the sprites colliding are all the way at the bottom screen. That means that we have very little time before the VSYNC will be triggered and the Interrupt handler will be called again. Even more likely scenario is that we have many sprites active on the screen and many collisions happening in very quick succession. And we don’t actually know how long we have. We could potentially disable interrupt during that period but that could cause some undesirable consequences like uneven movements of objects or missed collisions.
Another way to approach it is to split the code in such a way to minimize the amount of processing in the interrupt handler and do some processing in the main program. In this little demo I only set collision Flag during the Collision interrupt call and do other processing during more lengthy and predictable VSYNC interrupt. We are using following two variables as flags:
Collided: .byte 0 ; Flag is set when ships are already in collision
Now the SPRCOL handler only sets the Collision to 1 by
lda #SPRCOL
sta $9F27 ; Clear SPRCOL
And at the end we clear the SPRCOL flag by writing 1 to it.
Next step is to process the flags during VSYNC handler. We need to implement following algorithm:
Set Collided to <>0
Clear Collision Flag
Change the appearance of Player Sprite
Else (Collision=0)
If no collision from before (Collided=0)
Do nothing, go to moving objects
Else (Collided=1)
Clear Collided Flag
Change the appearance of Player Sprite back to normal
Interestingly the Assembly code is almost smaller than pseudocode above:
lda Collision
beq NoCollision
inc Collided
stz Collision
VERA_SET_ADDR $1FC0F, 1
lda #%10100111 ; Sprite 1 Palette offset 7
sta VERA_DATA0
jmp MoveAssets
lda Collided
beq MoveAssets
stz Collided ; Clear Collided flag
VERA_SET_ADDR $1FC0F, 1
lda #%10100011 ; Sprite 1 Palette offset 3
sta VERA_DATA0
Only thing remaining is to move objects and exit, like this:
lda Joy
and #JOY_RIGHT
beq :+
inc PosX
and #JOY_LEFT
beq :+
dec PosX
; Update player
VERA_SET_ADDR $1FC0A, 1
lda PosX
sta VERA_DATA0
stz VERA_DATA0
lda PosY
sta VERA_DATA0
VERA_SET_ADDR $1FC12, 1
lda EnemyX
sta VERA_DATA0
stz VERA_DATA0
lda EnemyY
sta VERA_DATA0
sta $9F27 ; Clear VSYNC Flag
jmp (OLD_IRQ_HANDLER)
As you see, we do not spend any time on controlling the movement of sprites. Both Enemy and Player simply wrap around 8 bit values.
I hope you find this tutorial useful. Full source code is available on GitHub here.
If you don’t have a cc65 setup yet, binary is available for download here.
Thank you for yet another great tutorial.
ReplyDeleteYou missed a link in the line "Luckily we already discussed the Interrupt handler in a previous article about Sprite Animation in Assembly (here)" just above the Turning On Sprite Collision detection section.
Thanks Jimmy. I fixed the missing link.
DeleteNow this is epic
ReplyDeleteHa ha, not sure about Epic, but thanks, I appreciate it.
Delete