Wednesday, September 27, 2017

Responsive object collisions, part 3

Picking back up from where we left off, when the collision type is 11, the code branches to Touch_Special, which looks something like this in the current disassembly:
Touch_Special:
    ...
    move.w  a0,d1                           ; Get RAM address of what object hit this
    subi.w  #Object_RAM,d1
    beq.s   loc_10406                       ; If the main character hit it, branch
    addq.b  #1,collision_property(a1)       ; Otherwise, it seems everything else does double

loc_10406:
    addq.b  #1,collision_property(a1)       ; So hitting a boss with your Tails sidekick does double damage?
    rts

This is a bit obfuscated, so it's no wonder the disassembly comments get it wrong. Here's what's going on: at this point, a0 holds the address of the player that's processing the collision response list. The above code takes that address and subtracts the address of the start of object RAM, and takes the branch if the result is zero, which can only happen if a0 is pointing at the start of object RAM.

It just so happens that the player 1 object is always loaded to the start of object RAM, so this is just a confusing way to check which player touched the object. If it was player 1, then the branch is taken, and the collision_property attribute is incremented once; otherwise, the branch is not taken, and it is incremented twice. Assuming that it was initially set to zero, this means the object's collision_property attribute is set to 1 when player 1 touches it, 2 when player touches it, and 3 when both players touch it.

So what does this mean? Next time the object runs, its collision_property attribute will be a bitfield composed of flags that indicate which players are touching it right now. This collision type is what allows objects such as bumpers to affect players separately from one another.

Finally, when the collision type is 01, none of the previous branches are taken, and so control flows into the code which directly follows...
    ; If 01...
    move.b  collision_flags(a1),d0                  ; Get collision_flags
    andi.b  #$3F,d0                                 ; Get only collision size
    cmpi.b  #6,d0                                   ; Is touch response $46 ?
    beq.s   Touch_Monitor                           ; If yes, branch
...where immediately there's a hardwired check that looks for collision size 6. If this check passes, the code branches to Touch_Monitor, which handles, uh, monitor collision. This would take us far afield, so let's skip it for now.
    move.b  (Player_1+invulnerability_timer).w,d0   ; Get the main character's invulnerability_timer
    tst.w   (Competition_mode).w                    ; Is the competition mode?
    beq.s   loc_1000A                               ; If not, branch
    move.b  invulnerability_timer(a0),d0            ; Get invulnerability_timer from whoever branched to TouchResponse

loc_1000A:
    cmpi.b  #90,d0                                  ; Is there more than 90 frames on the timer remaining?
    bhs.w   locret_10018                            ; If so, branch
    move.b  #4,routine(a1)                          ; Set target object's routine to 4 (must be reserved for collision response)

locret_10018:
    rts

Next, there's code that looks at the invulnerability_timer attribute of player 1 (in a 2P game, the attribute of the player currently processing the collision response list) and if it's lower than 90, sets the routine of the touched object to 4.

Meanwhile, the invulnerability_timer attribute is set to 120 when the player lands after taking damage, and decreases once per frame until it reaches zero, causing the player to blink for two seconds before becoming vulnerable again. The check at loc_1000A prevents the player from messing with the routine of touched objects during the first half-second of the invulnerability period.

This collision type is used by the bouncing ring object, preventing a player from picking rings back up immediately after dropping them. Note that in a 2P game, the other player could potentially collect all the rings the moment they spawn.


The placement of the check at loc_1000A annoys me greatly. Reminder that to get this far, we had to go through all the rigamole of performing bound checks on the player and the ring's hitboxes, for a response we already know in advance isn't actually going to happen. Wouldn't it have made more sense to do the check on the ring object's side of things and only call Add_SpriteToCollisionResponseList once the 30 frames are up?

It's particularly annoying because the start of the invulnerability period is exactly when the player is likely to be touching a lot of rings, which means more of them will pass all the bound checks only to then fail the timer check. Luckily, it's not as bad as it could possibly be: at the point where they're guaranteed to be touching all the rings, the player is still falling back from taking damage, and the collision response list isn't processed when the player object is in routine 4.

No comments:

Post a Comment