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:

lda $9F26
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:

CheckSPRCOL:
    lda $9F27
    and #SPRCOL
    beq checkVSYNC

    ; Process SPRCOL

checkVSYNC:
    lda $9F27
    and #VSYNC
    bne ProcessVSYNC
    jmp irq_exit

    ; Process VSYNC

irq_exit:
    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:

Collision:  .byte 0 ; Flag set when collision is detected
Collided:   .byte 0 ; Flag is set when ships are already in collision


Now the SPRCOL handler only sets the Collision to 1 by

inc Collision    ; Set Collision flag
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:

If new collision was detected (Collision=1)
        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:

ProcessVSYNC:
    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

NoCollision:
    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:

MoveAssets:
    lda Joy
    and #JOY_RIGHT
    beq :+
    inc PosX

:   lda Joy
    and #JOY_LEFT
    beq :+
    dec PosX

:   inc EnemyY

    ; Update player
    VERA_SET_ADDR $1FC0A, 1
    lda PosX
    sta VERA_DATA0
    stz VERA_DATA0
    lda PosY
    sta VERA_DATA0

    ; Update Enemy
    VERA_SET_ADDR $1FC12, 1
    lda EnemyX
    sta VERA_DATA0
    stz VERA_DATA0
    lda EnemyY
    sta VERA_DATA0

    lda #VSYNC
    sta $9F27 ; Clear VSYNC Flag

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


Back to Sprites in Assembly I - Setup

Back to Sprites in Assembly II - IRQ Animation

Comments

  1. Thank you for yet another great tutorial.
    You 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.

    ReplyDelete
  2. Replies
    1. Ha ha, not sure about Epic, but thanks, I appreciate it.

      Delete

Post a Comment

Popular posts from this blog

Commander X16 Premium Keyboard