Friday, August 4, 2017

Spring theory

A cool feature introduced in Sonic 2 is that if a vertical spring has bit 0 of its subtype set, characters bounce away from it in a 3D twirling animation, rather than the usual bouncing sprite.


This works by enabling "flip mode" on the player object, which temporarily wrests away control over the mapping frame from the animation function, instead forcing the player to perform a full twirl before returning to the walking animation. If the spring in question is a red spring, the player will perform two twirls to account for the longer air time. Springs placed through debug mode will always make the player twirl, except in Marble Garden Zone for some reason.

The bouncing sprite however, is a perfectly normal animation, and a serious contender for the most boring one ever:
byte_12BDB: dc.b  $2F, $8E, $FD,   0
All it does is show the bouncing sprite for 49 frames*, after which it switches over to the walking animation. Incidentally, that's about how long you ascend for when you bounce off a yellow spring. Note that this doesn't account for the longer air time you get from red springs, or having your flight cut short due to a low-hanging ceiling.

It gets worse. If the ceiling is low enough that the player lands back on the spring before the bouncing animation ends, the animation will abruptly end while the player is still rising. This happens because the spring object writes to byte $20 of the player's SST, but not byte $21. Since $20 keeps the same value it already had, no animation change is detected, allowing the original animation to continue.

Once the animation is over and the player is back in their walking sprites, the animation change is once again detected, causing the bouncing animation to resume next time the player lands on the spring.


* The animation was probably meant to be 48 frames long, but the $FD control code doesn't start the new animation immediately, and so the previous sprite gets displayed for an extra frame.

Thursday, August 3, 2017

The global animation system, part 2

In order to keep track of an object's animation across several frames, animation functions do a lot of extra bookkeeping within the object's SST, beyond the animation value in byte $20. Objects should not mess with these values, or they run the risk of throwing the function's internal state off the rails and setting the whole game on fire.

  • $21 is the previous animation, and holds the value $20 had during the previous execution of the animation function. This is used to detect external changes to the current animation.
  • $22 is the mapping frame, as we already know. It's safe to change this one, but it will be changed back the next time the animation frame is incremented.
  • $23 is the animation frame. This is the current step in the animation. It starts at 0 and is incremented when the animation frame timer exceeds the wait value in the first byte of the animation's definition.
  • $24 is the animation frame timer. This is how long the animation function has waited for during the current animation frame. It starts at 0 and is incremented once per frame.

In the previous entry, I glossed over how a mapping frame of $FF instructs the Animate_Sprite function to start over the animation from the beginning. In fact, the last few values within the range of a byte are all reserved as control codes for the animation functions, limiting the number of frames a set of sprite mappings can have.

The Animate_Sprite function in particular starts off by checking if the sign bit on the value is set, and if so, interprets the value as a control code rather than a mapping frame. As a result, even though there only exist a handful of valid control codes, most objects are limited to a maximum of 128 mapping frames.

Player objects, on the other hand, actually have their own animation functions, which apart from adding a few bells and whistles, only interpret a few select values as control codes, increasing the maximum number mapping frames to 252.

Here's a rundown of all the control codes and their effects:

  • $FF: Restarts the animation. This is accomplished by setting byte $23, the animation frame, back to 0.
  • $FE: Rewinds the animation by the given number of frames. This is accomplished by subtracting a number from byte $23. The next byte indicates the number of frames to subtract. 
  • $FD: Starts a new animation. This is accomplished by writing the new animation to byte $20. The next byte indicates which animation to start.

The remaining control codes are exclusive to non-player objects:


Note that if control code $FD is used to start an animation when the same animation is already playing, nothing special will happen, and the function will happily continue reading bytes as additional frames in the current animation. Because byte $20 still has the same value as byte $21, no animation change is detected.

This situation is one of the few where messing with the animation function's bookkeeping becomes mandatory: in order to externally force an animation change, a sentinel value such as $FF may be written to the object's previous animation byte at $21. $FF is not a valid animation, so it is impossible for $20 and $21 to be equal, ensuring an animation change is detected.

Wednesday, August 2, 2017

The global animation system, part 1

Along with standardized functions for drawing and deleting sprites, Sonic 3 also includes functions that automate sprite animation. Most objects just call the Animate_Sprite function, which takes in an animation script via the a1 register.
loc_2D0F8:
    lea     (Ani_Starpost).l,a1
    jsr     (Animate_Sprite).l
    jmp     (Sprite_OnScreen_Test).l
Animation scripts, much like sprite mappings, are lists of word-sized pointers to the actual animation definitions. These are indexed through byte $20 in an object's SST. Like sprite mappings, byte $20 is multiplied by two before being used as an index to the pointer list, allowing for a maximum of 256 animations per script.

The animation definition format itself is deceptively simple. The first byte is the number of frames to wait between each mapping frame, and the remaining bytes are the actual mapping frames to display.
byte_12B8D: dc.b    7, $A4, $A5, $A6, $FF
As an example, take byte_12B8D, which is animation $C in Sonic's animation script. When $C is written to byte $20 in Sonic's SST, his mapping frame in byte $22 changes to $A4, the first frame in the animation. Then, the animation does nothing for 7 frames, after which it changes $22 to $A5, the next frame in the animation. It waits again, changes $22 to $A6, and waits one last time. The $FF byte at the end then instructs the animation to start over from the beginning.

The result is that mapping frame $A4 will be displayed for 8 frames, then $A5, and then $A6, in a continuous loop.


This format is good for animations which play back at a constant speed, but is inconvenient when some frames need to be displayed longer than others. For instance, consider byte_12B20, which is Sonic's standing animation:
byte_12B20: dc.b    5, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA
            dc.b  $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA
            dc.b  $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA, $BA
            dc.b  $BA, $BA, $BA, $BB, $BC, $BC, $BD, $BD, $BE, $BE, $BD, $BD, $BE, $BE, $BD, $BD
            dc.b  $BE, $BE, $BD, $BD, $BE, $BE, $BD, $BD, $BE, $BE, $BD, $BD, $BE, $BE, $BD, $BD
            dc.b  $BE, $BE, $BD, $BD, $BE, $BE, $AD, $AD, $AD, $AD, $AD, $AD, $AE, $AE, $AE, $AE
            dc.b  $AE, $AE, $AF, $D9, $D9, $D9, $D9, $D9, $D9, $AF, $AF, $FE, $35
Half of this animation alone points to mapping frame $BA, which is the main standing sprite. The foot-tapping sprites at $BD and $BE, which are displayed alternately for 12 frames each, take up most of the remaining space, but the overall speed must cater to the lowest common denominator: the blinking sprite at $BB, which is only displayed for 6 frames.

Tuesday, August 1, 2017

The same, but different

Alright, one final detail about breakable walls. Did you ever notice that the Knuckles walls from Carnival Night Zone end up reappearing much later in the game, in Sandopolis Zone?


What's interesting about this bit of graphics reuse is that a) it's only between these two, seemingly arbitrary levels, and b) the graphics actually aren't reused at all! Although the sprite mappings are shared between the two levels, the art is not, and the two sprites actually look a bit different from one another.


The reason for the art discrepancy becomes evident when you realize these sprites are not using the player palette or the enemy palette like other common objects: they're using the main level colors in line 2! No wonder they look slightly different, considering the two levels have such distinct color schemes.

Monday, July 31, 2017

*Blocks your path*

Back in my first post about breakable walls, reader Telnaior was asking about Knuckles-only walls. There's actually no consistent way to mark a wall Knuckles-only; each level handles it differently in the object's init code, though it's always based on the object's subtype. Some levels, such as Mushroom Hill Zone, make it so every wall is Knuckles-only.
loc_21818:
    move.w  ($FFFFB018).w,$30(a0)
    move.w  ($FFFFB062).w,$32(a0)
    moveq   #0,d1
    move.b  7(a0),d1
    addi.w  #$B,d1
    ...
When a breakable wall is marked Knuckles-only, control is deferred to loc_21818, at first identical to loc_21568, which we've studied before. The interesting bits start at loc_21862, where we check whether the wall should break.
loc_21862:
    lea      (Player_1).w,a1
    move.w  $30(a0),d1
    move.w  d6,d0
    andi.w  #1,d0
    beq.s   loc_218B0
    cmpi.b  #2,$38(a1)
    bne.s   loc_218B0
    bclr    #5,$2A(a0)
    bsr.s   sub_218CE
    btst    #6,$2A(a0)
    beq.s   loc_2185C
    lea     (Player_2).w,a1
    cmpi.b  #2,$38(a1)
    bne.s   loc_2185C
    ...
Notice that everything but the Knuckles character ID check is gone, and that an identical check is now also performed on player 2's SST. Perhaps at one point, Knuckles-only walls were considered for Competition mode.

When the wall does break, an interesting piece of code runs over at loc_218EE. It does another character ID check for Knuckles (paranoid much?), and then checks if the double jump flag at $2F(a1) is set to 1.
loc_218EE:
    move.w  $18(a1),$1C(a1)
    bclr    #5,$2A(a1)
    cmpi.b  #2,$38(a1)
    bne.s   loc_21928
    cmpi.b  #1,$2F(a1)
    bne.s   loc_21928
    move.b  #2,$2F(a1)
    move.b  #$21,$20(a1)
    bclr    #0,$2A(a1)
    tst.w   $18(a1)
    bpl.s   loc_21928
    bset    #0,$2A(a1)

loc_21928:
    ...
If both of those check out, then it means the wall was broken by a gliding Knuckles, and the object forces Knuckles out of the gliding state by setting the double jump flag to 2 and Knuckles' animation to $21, which is the falling-from-gliding animation. This is interesting because regular breakable walls don't have this bit of code, so Knuckles will happily glide right through a bunch of them.


It also turns out the rocky walls from Angel Island Zone and Lava Reef Zone are actually a different object, which does contain this code, so the fact that it's missing from the standard breakable wall object is possibly an oversight.

Friday, July 28, 2017

The onus in "bonus" is on "us"

This has absolutely nothing to do with breakable walls, I was just reminded of it when I was flipping between the Sonic 1 and Sonic 3 pictures in my previous post.


This is the sprite mapping layout for the HUD in Sonic 1. If you look at the S in "RINGS", you can see how each letter is aligned to the right, leaving a single pixel-wide gap on the left for spacing purposes.


To make the font italic in Sonic 2, the bottom half of each letter got dragged one pixel to the left. As a result, the S now spans the entire width of an 8x8 tile, but it looks okay because there's still a pixel of spacing overall.


When they removed the italics for Sonic 3 though, rather than starting from the Sonic 1 graphics, it seems they instead "straightened out" the Sonic 2 font: observe the bottom right corner of the S. As a side effect, all the text is now aligned to the left. Why does this matter? Because it screwed up the spacing on the "BONUS" mappings.


The "BONU" bit of the text happens to span the entirety of the 32 pixel-wide sprite, and counts on the leading space in the S to make the spacing work out. The S, as it turns out, is reused from the HUD graphics. When they aligned all the text to the left, they broke this spacing, and they never moved the "BONU" mapping over to compensate.

Got that? Okay, now go watch this considerably more interesting video.

Thursday, July 27, 2017

Back whence you came, or possibly not

When you crash through the rocky walls in Angel Island Zone and Lava Reef Zone, they break off into nicely separated rock sprites which then fly off to whichever direction you punched them.


This makes a lot of sense, which is why I was quite surprised when I saw the Knuckles wall in Hydrocity Zone 2 do this:


I thought, surely, this must be a bug. The pieces are flying right back at you! That's when I realized that the simple 2x4 brick walls have had this behavior all the way back since Sonic 1.


It seems only the walls that use this classic mapping layout inherit the backwards-flying behavior. The thick columns in Marble Garden Zone also break off into square pieces, but they fly away from you. The ice walls in Icecap Zone break up into clean rock sprites like Angel Island and Lava Reef, but the pieces just fall off to either side.

There's no moral to this story, I'm just venting.

BONUS CONTENT because I took a pretty good screenshot: the breakable wall object is still present within Sonic 2's code, and has been refactored to work with the dual player system. However, no code to handle player 2 was actually added, so Tails can't break it.

The object is completely unused in the final game, but it can be seen in the Nick Arcade prototype's Green Hill Zone.


In the Sonic 2 mobile remake, they brought the object back for Hidden Palace Zone. Tails can now break it, of course.

Wednesday, July 26, 2017

One thing's for sure… you do exist

If you've picked anything up from my blogging style is that I like to first explain a topic in relative detail, and then follow it up with a practical example that illustrates what I'm talking about. Case in point, Icecap Zone 1:


When you break through one of these large walls as Knuckles, more often than not you'll come to a complete stop, and Knuckles will switch over to his pushing animation. I say "more often than not", because if your position happens to line up so that you're right next to the wall but not actually touching it, the wall will break before it kills your speed.

All of this happens because the breaking wall object in this stage does none of the things I mentioned before. This was fine in standalone Sonic 3, since the only thing that could break those walls was a sliding ice block.

Actually, here's a fun exercise: if you use debug mode to place one of those sliding blocks and then push it towards the walls guarding Knuckles' routes, you'll find that they'll break just fine. There's actually only one kind of breakable wall in this stage, and they prevent you from breaking into Knuckles' routes by simply not placing any ice blocks nearby.

Tuesday, July 25, 2017

I was never here

The breakable wall object, like most other solid objects in the game, checks for collision with Sonic and pals by calling the SolidObjectFull function. This function takes in a bunch of parameters through the d1-d4 registers, as well as the solid object in the a0 register and a player object in the a1 register.
loc_21568:
    move.w  ($FFFFB018).w,$30(a0)
    move.w  ($FFFFB062).w,$32(a0)
    moveq   #0,d1
    move.b  7(a0),d1
    addi.w  #$B,d1
    moveq   #0,d2
    move.b  6(a0),d2
    move.w  d2,d3
    addq.w  #1,d3
    move.w  $10(a0),d4
    jsr     (SolidObjectFull).l
    tst.b   $2C(a0)
    bpl.s   loc_215A4
    ...
After executing this function however, if either player was touching the object, then they get pushed outside the object's bounding box, and their speed is killed. This is obviously bad in the case where the player breaks down the wall, which is why prior to calling the function, the breakable wall object records the horizontal velocity for both players.

If you look at the first two lines of loc_21568 above, the RAM values at $B018 and $B062 are stored in offsets $30 and $32 of the breakable wall's SST. $B000 is the RAM address of player 1's SST, normally identified by the Player_1 label. An object's X velocity is kept at offset $18, so $B000 + $18 = player 1's X velocity at $B018. The same principle applies to player 2, whose SST is directly after player 1's at $B04A and is normally identified by the Player_2 label.
loc_215A4:
    swap    d6
    andi.w  #3,d6
    bne.s   loc_215B2

loc_215AC:
    jmp     (Sprite_OnScreen_Test).l
; ---------------------------------------------------------------------------

loc_215B2:
    lea     (Player_1).w,a1
    move.w  $30(a0),d1
    move.w  d6,d0
    andi.w  #1,d0
    beq.s   loc_2162A
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
    ...
A bit later, the stored velocity is recalled into register d1, which I handwaved in my previous post. None of the following code actually runs unless one of two bits in register d6 are set, which get set by the SolidObjectFull function depending if the object was touched by player 1, player 2, or both.

Note that just checking bit 5 of $2A isn't enough: not only is it not set when the player is in mid-air, but it remains set as long the player is still touching the object, so if you stood with your back to a wall and spindashed away, it would break.
sub_2165A:
    move.w  d1,$18(a1)
    addq.w  #4,$10(a1)
    movea.l $34(a0),a4
    move.w  $10(a0),d0
    cmp.w   $10(a1),d0
    blo.s   loc_2167A
    subi.w  #8,$10(a1)
    movea.l $38(a0),a4
            
loc_2167A:  
    move.w  $18(a1),$1C(a1)
    bclr    #5,$2A(a1)
    move.l  #loc_21692,(a0)
    ...
Finally, if the wall did break, d1 is written back into the player's horizontal velocity at $18(a1), almost as if the wall had never been there in the first place. A slight tug is also given to the player's horizontal position at $10(a1), moving them four pixels towards the center of the wall. Bit 5 of $2A(a1) is also cleared in order to prevent the player from switching over to the pushing animation.

Monday, July 24, 2017

Special wall-breaking powers

The bit of code responsible for deciding whether or not a player should be able to destroy a breakable wall has quite a few peculiarities which make its behavior less than obvious, so I thought we could have a look at it and learn a couple of things along the way, along with help from our friend, the Sonic Retro wiki.

Here's the code:
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
    cmpi.b  #2,$38(a1)
    beq.s   loc_215F4
    btst    #4,$2B(a1)
    bne.s   loc_215E0
    btst    #5,$2A(a0)
    beq.s   loc_2162A

loc_215E0:
    cmpi.b  #2,$20(a1)
    bne.s   loc_2162A
    move.w  d1,d0
    bpl.s   loc_215EE
    neg.w   d0

loc_215EE:
    cmpi.w  #$480,d0
    blo.s   loc_2162A

loc_215F4:
    bclr    #5,$2A(a0)
    bsr.s   sub_2165A
    ...
Alright, let's start from the top.
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
Pretty straightforward. If you're Super Sonic or Super Knuckles (or the Hyper version of either character), then you can break the wall. Note that this leaves out Super Tails, as if he wasn't screwed over enough already.
    cmpi.b  #2,$38(a1)
    beq.s   loc_215F4
$38 is the character ID: Sonic is 0, Tails is 1, Knuckles is 2. So if you're playing as Knuckles, then you can break the wall. This effectively means the previous check is only relevant when playing as Sonic.
    btst    #4,$2B(a1)
    bne.s   loc_215E0
$2B is the secondary status bitfield, exclusive to the player objects. Sonic Retro helpfully chimes in:

BitHexDescription
4$10Fire shield flag.

So if you have a flame barrier, we skip over the next check. Which happens to be...
    btst    #5,$2A(a0)
    beq.s   loc_2162A
$2A is the primary status bitfield, universal to all objects. Note that we're checking the flags of the breakable wall in a0, not the player object loaded in a1. Retro says:

BitHexDescription
5$20Set if Sonic is pushing on this object.

This is a bit inaccurate. Bit 5 is only set if player 1 is pushing on the object while standing on something. Combined with the previous check, this means if you have a flame barrier, then you can break the wall even if in mid-air.
    cmpi.b  #2,$20(a1)
    bne.s   loc_2162A
$20 is the current animation ID, for objects that use the global animation system. In the case of the player objects, #2 corresponds to the spinning animation, used while either rolling or jumping. So if you aren't in the spinning animation, then you can't break the wall.
    move.w  d1,d0
    bpl.s   loc_215EE
    neg.w   d0

loc_215EE:
    cmpi.w  #$480,d0
    blo.s   loc_2162A
Finally, the player's horizontal velocity, which was previously stored in d1, is copied over to d0 and transformed into its absolute value. That value is then compared to $480, and if you're moving at a horizontal speed of less than 4.5 pixels, then you can't break the wall. This also applies to the flame barrier check, which is what allows Sonic to break through walls using his fireball dash move.

Notice anything fishy, though? Nowhere do we check whether the player has performed a double jump move or not, so if you have a flame barrier, just jumping at the wall with the requisite speed will work. In fact, there's no check for Sonic either, which means Tails can break down walls in the exact same way. This surely annoys players who try to avoid the first special ring in Angel Island 2 by jumping -- only to go through the wall due to their speed coming out of the tube.