Monday, January 28, 2019

Green spheres in special stages, part 4

Alright, let's finally start writing our green sphere logic. Because we're implementing them over yellow spheres, there's actually something extra we need to take into account: yellow spheres are the only objects Tails will interact with in a Sonic and Tails game, so we have to skip over that code by adding a check for our green spheres flag at loc_92C4:
    cmpi.b  #5,$44(a0)
    bne.s   loc_9304
    tst.b   (Special_stage_green_spheres).w
    bne.s   loc_9304
    tst.b   (Special_stage_clear_routine).w
    bne.s   loc_9304
Next, we'll go to sub_972E and look for the yellow sphere handling code. It can be found at loc_97EE, so that's where we need to add the object to the collision response list, which will look very similar to the blue sphere handling code:
loc_97EE:
    cmpi.b  #5,d2
    bne.s   loc_9822
    tst.b   (Special_stage_green_spheres).w
    beq.s   loc_97F4
    bsr.w   Find_SStageCollisionResponseSlot
    bne.s   loc_97BE
    move.b  #3,(a2)
    move.l  a1,4(a2)
    bra.s   loc_97BE

loc_97F4:
    tst.b   (Special_stage_jump_lock).w
We write the routine and layout pointer to the collision response slot as usual, then jump to loc_97BE to play the blue sphere sound. This time however, we set the routine to 3, which we'll then define in the off_9DFC array as follows:
off_9DFC:   dc.l Touch_SSSprites_Ring
            dc.l Touch_SSSprites_BlueSphere
            dc.l Touch_SSSprites_GreenSphere
We also need to add a dummy green sphere definition to the MapPtr_A10A array, which will serve as our halfway state between touching the green sphere and placing a blue sphere in its stead. Turns out, there's another blue sphere clone in slot $C which is completely unused, so we can just tweak its VDP pattern to point at palette line 4 and use that:
    dc.l Map_SStageChaosEmerald ; $B
    dc.l $E5A70000              ;
    dc.l Map_SStageSphere       ; $C
    dc.l $E6800000              ;
    dc.l Map_SStageSuperEmerald ; $D
    dc.l $E5A70000              ;
Okay, we're now ready to write our implementation of Touch_SSSprites_GreenSphere, which is basically going to be a lighter version of Touch_SSSprites_BlueSphere. Here's what it needs to do:
  • The first time through, we change the object at the layout position to $C, and set up a timer of nine frames.
  • On subsequent loops, we run out the timer, and then wait until any of the top three bits in the fractional part of the player's position are set, signaling that we are no longer right on top of the sphere.
  • After we're done waiting, we write 2 (a blue sphere) to the layout and clear out the collision response slot.

So let's go ahead and turn that into code:
Touch_SSSprites_GreenSphere:
    movea.l 4(a0),a1
    cmpi.b  #$C,(a1)
    beq.s   .checkChangeColor
    move.b  #$C,(a1)
    move.b  #9,1(a0)
    rts

.checkChangeColor:
    subq.b  #1,1(a0)
    bpl.s   .return
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   .return
    move.b  #2,(a1)
    clr.l   (a0)
    clr.l   4(a0)

.return:
   rts
That should about do it. If we build and test the ROM right now though, we'll soon realize there is a problem. Although our green spheres correctly only turn blue once we've walked past them, we end up collecting those immediately after, leaving behind a trail of red spheres as we go.


So what is the reason for the discrepancy between red and blue spheres? Why do we collect the blue sphere as we're walking away, but not activate the red sphere in the same circumstances? The answer can be found at sub_972E:
sub_972E:
    lea     (Plane_buffer).w,a1
    move.w  (Special_stage_X_pos).w,d0
    addi.w  #$80,d0
    lsr.w   #8,d0
    andi.w  #$1F,d0
    move.w  (Special_stage_Y_pos).w,d1
    addi.w  #$80,d1
    lsr.w   #8,d1
    andi.w  #$1F,d1
    lsl.w   #5,d1
    or.b    d0,d1
    lea     (a1,d1.w),a1
    move.b  (a1),d2
    beq.w   locret_98AE
Note how before we truncate the player's X and Y coordinates, we add onto them a fractional value of $80, or 0.5. This effectively rounds the player's position towards the closest integer, which means that if we're walking from position A to position B, then we will touch the object at position B as soon as we're closer to B then we are to A.

That's not really the relevant part, though. If we continue reading, we'll soon run into some very familiar code:
    cmpi.b  #1,d2
    bne.s   loc_97AA
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    bne.s   locret_97A8
When the object at the player's current position is 1, a red sphere, we are once again we are checking the top three bits in the fractional part of the player's position. However, this time we're not waiting until any of them are set. We're waiting until all of them are cleared! Red spheres only activate if we're right on top of them!

This explains the mismatched behavior between blue spheres and red spheres. For red spheres, it is sufficient to wait until we are no longer at the center of the intersection before placing them in the layout. Before placing a blue sphere however, we must ensure that the player has since moved to another layout cell.

To do this, we will first write the player's position to the collision response slot at loc_97EE:
    move.b  #3,(a2)
    move.w  d1,2(a2)
    move.l  a1,4(a2)
Now we can repeat the same calculation in Touch_SSSprites_GreenSphere, and then compare it against the stored value. As soon as these are different, it is safe to place the blue sphere in the layout. We preserve the original check so that the sphere still changes from green to blue with the same timing that a blue sphere does to red, except all we write to the layout at that point is $A, a dummy blue sphere:
Touch_SSSprites_GreenSphere:
    movea.l 4(a0),a1
    cmpi.b  #$C,(a1)
    beq.s   .checkChangeColor
    cmpi.b  #$A,(a1)
    beq.s   .checkPlayerMoved
    move.b  #$C,(a1)
    move.b  #9,1(a0)
    rts

.checkChangeColor:
    subq.b  #1,1(a0)
    bpl.s   .checkPlayerMoved
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   .return
    move.b  #$A,(a1)

.checkPlayerMoved:
    move.w  (Special_stage_X_pos).w,d0
    addi.w  #$80,d0
    lsr.w   #8,d0
    andi.w  #$1F,d0
    move.w  (Special_stage_Y_pos).w,d1
    addi.w  #$80,d1
    lsr.w   #8,d1
    andi.w  #$1F,d1
    lsl.w   #5,d1
    or.b    d0,d1
    cmp.w   2(a0),d1
    beq.s   .return
    move.b  #2,(a1)
    clr.l   (a0)
    clr.l   4(a0)

.return:
   rts
Note how we change the first return branch to point at the new check, which forces the sphere to become collectible as soon as we move to another layout cell, even if the sphere was still running out its timer. This avoids a bug at very high speeds, where if we collect a green sphere and then immediately run into a bumper, we fail to pick up the resulting blue sphere as we backpedal through it.


And that's it. We've successfully added green spheres to the special stage, while yellow spheres continue to work in all the layouts we didn't touch. Many thanks to flamewing for approving my pull requests to the skdisasm repo over these past few weeks; this series would've been unpalatable without properly labeled variables and subroutines.

No comments:

Post a Comment