don't click here

Interesting collision/physics observations in Sonic 1/2/3

Discussion in 'Engineering & Reverse Engineering' started by Lapper, Aug 17, 2024.

  1. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    A thread for highlighting intriguing collision/physics/gameplay mechanics from the classic genesis games that deserve special attention because they're either interesting or useful. I do this on Twitter in a very digestible way already but I think it'd also be appreciated here on Retro with greater detail and focus on the code.

    Pointing out things, explaining why certain things/quirks are the way they are, looking at the code behind them, and more.

    I also invite others to share as well of course!

    -----

    Standing on objects is strange.

    I want to point out and explore something that has always confused me about solid object collision.

    Sonic stands 1 pixel inside some objects, like Buttons and Rocks, but is perfectly flush on others, like Pushable Blocks. In fact, most objects actually look just fine... so you'd assume that these few outlier objects just have misaligned art or they have anomalies in their code - but it's not the artwork's fault, and the "incorrect" objects actually make more sense when you look at the code.

    All the classic Sonic games have a strange internal object collision quirk to blame. I'm gonna dig in and see why this happens and how they try to "fix" it

    SillyRock.png SillyButton.png

    There's an overarching issue with the main object collision code that almost all objects use. When an object that is acting solid knows Sonic is already standing on it, it attempts to reposition Sonic's Y Position to be aligned to it's surface - but when using an object's actual height, it places Sonic 1 pixel lower than it should, so his feet appear sunken into flat surfaces.

    Well.. if so then why do some objects look correct, like the Pushable block? What's the solution?
    EpicPushBlock.png
    Well bizarrely, rather than fix the core issue, the game manually accounts for this pixel for almost every object. It literally adds 1 to the height radius of an object while Sonic is standing on it.

    Every object acting solid passes in 2 different height values, a radius for when Sonic is in the air (I'll call this "air_radius"), and a radius for when Sonic is on the object ("stand_radius").
    Code (ASM):
    1.  
    2. ; ---------------------------------------------------------------------------
    3. ; Solid    object subroutine (includes spikes, blocks, rocks etc)
    4. ;
    5. ; input:
    6. ;    d1 = width / 2
    7. ;    d2 = height / 2 (when in the air (air_radius))
    8. ;    d3 = height / 2 (when on the object (stand_radius))
    9. ;    d4 = x-axis position of the object
    10. ; ---------------------------------------------------------------------------
    11.  
    Before looking at what the objects do specifically, let's first look at the actual solidity code at fault.

    Here's the S1 code when Sonic first lands on an object.
    (Note, he's in the air before landing, so this uses the air_radius)
    Code (ASM):
    1.  
    2. Solid_Landed:
    3.         ; Some horizontal checks
    4.         subq.w    #4,d3
    5.         moveq    #0,d1
    6.         move.b    obActWid(a0),d1
    7.         move.w    d1,d2
    8.         add.w    d2,d2
    9.         add.w    obX(a1),d1
    10.         sub.w    obX(a0),d1
    11.         bmi.s    Solid_Miss
    12.         cmp.w    d2,d1
    13.         bcc.s    Solid_Miss
    14.         tst.w    obVelY(a1)
    15.         bmi.s    Solid_Miss
    16.  
    17.         ; Here, Sonic is now landing on the object
    18.         sub.w    d3,obY(a1)    ; correct Sonic's position by subtracting the distance to the surface - this places Sonic's Y Position at [the object's surface Y - Sonic's height radius]
    19.         subq.w    #1,obY(a1)    ; Subtracts 1 to align correctly!! <<<
    20.         bsr.s    Solid_ResetFloor
    21.         move.b    #2,obSolid(a0)
    22.         bset    #3,obStatus(a0)
    23.         moveq    #-1,d4
    24.         rts
    25.  
    As you can see, it does things correctly. 1 is subtracted, and Sonic aligns. Wowie!

    However, here's the code that keeps Sonic aligned on the object if he's already standing on it.
    (Note, because Sonic is standing on it this uses the stand_radius)

    Code (ASM):
    1.  
    2. ; ---------------------------------------------------------------------------
    3. ; Subroutine to    change Sonic's position with an object
    4. ; ---------------------------------------------------------------------------
    5.  
    6. ; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||
    7.  
    8.  
    9. MvSonicOnPtfm:
    10.         lea    (v_player).w,a1
    11.         move.w    obY(a0),d0             ; d0 = object y
    12.         sub.w    d3,d0                 ; d0 = object y - object stand_radius (object surface y)
    13.         bra.s    MvSonic2
    14. ; End of function MvSonicOnPtfm
    15.  
    16. ; ---------------------------------------------------------------------------
    17. ; Subroutine to    change Sonic's position with a platform
    18. ; ---------------------------------------------------------------------------
    19.  
    20. ; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||
    21.  
    22.  
    23. MvSonicOnPtfm2:
    24.         lea    (v_player).w,a1
    25.         move.w    obY(a0),d0             ; d0 = platform y
    26.         subi.w    #9,d0                ; d0 = platform y - 9 (platform surface y)
    27.         ; that 9 is suspicious...
    28.  
    29. MvSonic2:
    30.         tst.b    (f_playerctrl).w
    31.         bmi.s    locret_7B62
    32.         cmpi.b    #6,(v_player+obRoutine).w
    33.         bhs.s    locret_7B62
    34.         tst.w    (v_debuguse).w
    35.         bne.s    locret_7B62
    36.         moveq    #0,d1                 ; d1 = 0
    37.         move.b    obHeight(a1),d1     ; d1 = Sonic's height radius
    38.         sub.w    d1,d0                  ; d0 = object/platform surface y - Sonic's height radius
    39.         move.w    d0,obY(a1)             ; Sonic's y set to d0 - this places Sonic's Y Position at [the object's surface Y - Sonic's height radius]
    40.                                     ; Does not subtract 1!! <<<
    41.         sub.w    obX(a0),d2
    42.         sub.w    d2,obX(a1)
    43.  
    44. locret_7B62:
    45.         rts
    46. ; End of function MvSonicOnPtfm2
    47.  
    48.  
    The only difference that matters is that 1 is not subtracted when standing on objects, and this is the cause of it all. Just 1 missing line.


    So, focusing back on the objects themselves, almost all objects that act solid add use the above code.

    Each object's code is expected to add 1 to the stand_radius used while Sonic is standing on it, purely to compensate for this.

    For example, here's the unmodified code for the push block. Its height radius for collision *should* be 16, half of it's full height. BUT, the game uses 17 as the block's stand_radius, 1 pixel more than the object's actual size, so that Sonic will stand on it correctly:
    Code (ASM):
    1.  
    2. ;Solidity code for the push block
    3. loc_BF6E:    ; Routine 2
    4.         tst.b    $32(a0)
    5.         bne.w    loc_C046
    6.         moveq    #0,d1
    7.         move.b    obActWid(a0),d1
    8.         addi.w    #$B,d1
    9.         move.w    #$10,d2    ; < air_radius 16 ($10)
    10.         move.w    #$11,d3    ; < stand_radius 17 ($11)
    11.         move.w    obX(a0),d4
    12.         bsr.w    loc_C186
    13.  
    EpicPushBlock.png bro is correctly aligned, by using the incorrect radius

    And here's the unmodified code for the button, which shows both air_radius and stand_radius are the same, 5. Of course, 5 *should* be correct, but only ends up wrong thanks to the 1 pixel issue with the main collision code.
    Code (ASM):
    1.  
    2. ;Solidity code for the button
    3. But_Pressed:    ; Routine 2
    4.         tst.b    obRender(a0)
    5.         bpl.s    But_Display
    6.         move.w    #$1B,d1
    7.         move.w    #5,d2    ; < air_radius 5
    8.         move.w    #5,d3    ; < stand_radius 5
    9.         move.w    obX(a0),d4
    10.         bsr.w    SolidObject
    11.  
    SillyButton.png bro is incorrectly aligned, by using the correct radius

    Notice that when I showed the code for keeping Sonic aligned when standing on an object, a value of 9 sneaks in instead of 8 for the y radius of platforms? Same reason!

    Any object that "forgets" to do this will have the issue. It's a bizarre solution, passing the responsibility onto every single object's code rather than adding one line in the object collision itself. Maybe someone here can let me know about a good reason it is set up like this, but from what I can tell, it's unnecessary. Only 1 extra line was needed!

    This setup (and all related issues) for object collision sizes lives on through past Sonic 1 through to Sonic 3, but I think there are fewer examples of the radius being incorrectly adjusted for an object the further you look.


    Here I modify these objects to demonstrate what the button does wrong, if not clear.
    If I edit the push block code to use the actual height radius of the block, 16, for stand_radius, Sonic would appear sunken in by 1 pixel.
    Code (ASM):
    1.  
    2. ;Solidity code for the push block
    3. loc_BF6E:    ; Routine 2
    4.         tst.b    $32(a0)
    5.         bne.w    loc_C046
    6.         moveq    #0,d1
    7.         move.b    obActWid(a0),d1
    8.         addi.w    #$B,d1
    9.         move.w    #$10,d2    ; < air_radius 16 ($10)
    10.         move.w    #$10,d3    ; < stand_radius MODIFIED to be the actual radius 16 ($10)
    11.         move.w    obX(a0),d4
    12.         bsr.w    loc_C186
    13.  
    SillyPushBlock.png

    Here's the button "fixed" in the same way the push block is by default, with it's stand_radius increased by 1 to be 6:

    Code (ASM):
    1.  
    2. ;Solidity code for the button
    3. But_Pressed:    ; Routine 2
    4.         tst.b    obRender(a0)
    5.         bpl.s    But_Display
    6.         move.w    #$1B,d1
    7.         move.w    #5,d2    ; < air_radius 5
    8.         move.w    #6,d3    ; < stand_radius MODIFIED to be 6
    9.         move.w    obX(a0),d4
    10.         bsr.w    SolidObject
    11.  
    EpicButton.png

    There are other quirks thanks to object solidity having 2 height radius values. If you disable a Vertical Spring's action, you'll notice they have an incorrect stand_radius. Since the air_radius is correct, and you obviously can't stand on springs, you'll never notice this in gameplay.
    SillySpringAir.png SillySpringStand.png

    Here's the code:
    Code (ASM):
    1.  
    2. Spring_Up:    ; Routine 2
    3.         move.w    #$1B,d1
    4.         move.w    #8,d2             ; < air_radius 8 (correct)
    5.         move.w    #$10,d3            ; < stand_radius 16 ($10) (incorrect)
    6.         move.w    obX(a0),d4
    7.         bsr.w    SolidObject
    8.  
     
    Last edited: Sep 11, 2024
    • Informative Informative x 8
    • List
  2. BenoitRen

    BenoitRen

    Tech Member
    936
    573
    93
    It does look like this made its way into Sonic 3 (& Knuckles). Here's the C version of Solid_Landed from hitchk.c:
    Code (C):
    1. static hitchk_results plhitchk_pop_up(sprite_status* p_actwk, sprite_status* p_playerwk, hitchk_edge_distances edge_distances, unsigned char plridebit) {
    2.   hitchk_results results = { 0, 0 };
    3.   edge_distances.vertical -= 4;
    4.   unsigned char width = p_actwk->sprhs * 2;
    5.   short relative_pos = p_actwk->sprhs + LONG_HIGH(p_playerwk->xposi) - LONG_HIGH(p_actwk->xposi);
    6.   if (relative_pos >= 0 && relative_pos >= width) { // unsigned
    7.     p_playerwk->yposi -= SHORT_TO_LONG_HIGH(1); // ALIGNMENT CORRECTION
    8.     if (g_mdRam.reverse_gravity_flag != 0) {
    9.       edge_distances.vertical = -edge_distances.vertical;
    10.       p_playerwk->yposi += SHORT_TO_LONG_HIGH(2);
    11.     }
    12.     p_playerwk->yposi -= SHORT_TO_LONG_HIGH(edge_distances.vertical);
    13.     if (p_playerwk->yspeed >= 0) {
    14.       RideObject_SetRide(p_actwk, p_playerwk);
    15.       SET_BIT(results.plcoli, plridebit + 1);
    16.       results.colitype = -1;
    17.       return results;
    18.     }
    19.   }
    20.   return results;
    21. }
    I haven't ported MvSonicOnPtfm2 yet so I can't confirm that there's still no adjustment there. However, I can look at the values that monitors give to hitchk:
    Code (C):
    1. hitchk_data data = { 25, 16, 17, LONG_HIGH(p_actwk->xposi) };
    The air_radius is 16, while the stand_radius is 17.

    upload_2024-8-17_15-24-31.png

    (I probably did something wrong while enlarging this image because the colours are faded)
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  3. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    Absolutely, It's surprising something like that wasn't sorted in Sonic 2 when I can only imagine it adds extra dev time in the long run to consistently give every object two 1 pixel slightly different radius values.


    A couple more observations about the tops of solid objects.

    Landing on objects is very weird
    This right here is probably what bothers me the most about object collision. Object solidity has a lot of issues, but this one is the most outwardly obvious and affects gameplay.

    When landing on solid objects, the horizontal criteria for actually sticking the landing does not always match that for standing on an object. In fact, it almost never does.

    If Sonic's exact X Position is not directly over the object, he will NOT land. Even when he is at a position where he would be fine to stand on the object. The game does not account for Sonic's horizontal size, it's as if he's a 1px wide column.
    So essentially, any X Position where he would be balancing gets rejected. When this happens he will continue to fall, only being pushed out when the object decides Sonic has touched it's sides.
    SPGObjectBugSlipping2.gif
    What does this mean?
    - Well this makes landing on objects, especially thin ones, is more precise than it should be by at least 7 pixels either side of any object. 7 is Sonic's minimum width radius for ground collision.
    - It means that even if you are standing on an object, if your centre is slightly over the edge and you jump perfectly vertically, you won't land back on it. All of this behaviour is very different to normal Solid Tile collision which unlike object collision is very consistent.

    So why does this happen?

    When landing on an object, the object obviously checks if Sonic is above it on the X axis. I actually showed this code in my first post, but now we need to focus on the first part.
    Code (ASM):
    1.  
    2. Solid_Landed:
    3.         ; Removing unrelated vertical offset
    4.         subq.w    #4,d3
    5.  
    6.         ; Landing horizontal checks, here the game makes sure Sonic is directly over the object
    7.         moveq    #0,d1
    8.         move.b    obActWid(a0),d1       ;d1 = the width radius of object
    9.         move.w    d1,d2
    10.         add.w    d2,d2                  ;d2 = full object width
    11.         add.w    obX(a1),d1             ;d1 = object's width radius + Sonic's X Position
    12.         sub.w    obX(a0),d1             ;d1 = object's width radius + Sonic's X Position - object's X Position
    13.                                         ;     AKA the distance between Sonic's X Position and object's left edge
    14.         bmi.s    Solid_Miss             ; if distance is less than 0, Sonic is off the left side, dont land
    15.         cmp.w    d2,d1
    16.         bcc.s    Solid_Miss             ; if distance is more than full object width, Sonic is off the right side, dont land
    17.  
    Notice there is absolutely no use or compensation for Sonic's size here, it only uses his position. All that would need to be done to remedy this, is to add Sonic's current width radius to d1 after setting it to obActWid, essentially pretending the object is wider than it is.

    In Sonic 3, d1 is not set to obActWid and therefore the slipping issue is largely solved.

    Funnily enough, In Sonic 1, they put the effort in to remedy this in a strange way for specifically item monitors (which use their own solidity code) These, rather than using Sonic's radius, give a stingy extra 4 pixels either side for landing. But hey, at least it's something.
    Code (ASM):
    1.  
    2. loc_A4EA:
    3.         moveq    #0,d1
    4.         move.b    obActWid(a0),d1       ;d1 = the width radius of object
    5.         addq.w    #4,d1                ;d1 = object width radius + 4. Woooo!
    6.  
    7.         ; The rest follows a similar format to normal
    8.         move.w    d1,d2
    9.         add.w    d2,d2
    10.         add.w    obX(a1),d1
    11.         sub.w    obX(a0),d1
    12.         bmi.s    loc_A4E2
    13.         cmp.w    d2,d1
    14.         bcc.s    loc_A4E2
    15.         moveq    #-1,d1
    16.         rts
    17. ; End of function Mon_SolidSides
    18.  
    Item monitor solidity changes between games and I don't remember right now if Sonic 2 does anything strange like this - but all the general Solid object mess carries on.

    So... is it a mistake? Possibly not.
    If Sonic were able to land on the very most corner of an object, it's possible he'd "catch" himself on the various hidden solid objects you find placed flush in walls (the ones placed to either modify the level layout or facilitate crushing). Even if the object didn't poke out of the wall, this could still be possible as object collision isn't quite as precise as tiles and has some tolerances (which I will talk about in future). And of course, once Sonic is standing on an object it takes over somewhat from tile collision. So, that's to say this may actually be a heavy handed way to prevent this, though there would certainly be better ways.

    This is not emulated in the remakes.


    Edit: Bonus info, because sonic slips past the edges of objects and this makes him effectively 1px wide, he can slip in tight spaces between objects, even if they are almost touching. The objects will still push him out to the side, but one object has to do this last and so he remains where he lands.
    SPGObjectBugSlipping1.gif




    Standing on objects is weird (part 2)

    Yep, there's more, and it's directly related to the above issue.

    Jump through platforms have the above issue... but WHILE Sonic is standing and walking on them. Yes. Sonic will fall off of a platform as soon as his centremost pixel leaves the edge.

    The cyan line represents the entire walkable surface for Sonic's x position on the platform:
    upload_2024-8-22_11-37-50.png

    Here's the walkable area for a normal solid object for contrast:
    upload_2024-8-22_11-41-21.png

    Platforms use their own code to allow Sonic to become airborne when walking off the edges, and platforms pass in that pesky obActWid value without adding Sonic's radius.

    For some context, all normal fully solid object solidity (like push blocks, etc) will accept the objects width radius as an input, as seen here:
    Code (ASM):
    1.  
    2. ; ---------------------------------------------------------------------------
    3. ; Solid    object subroutine (includes spikes, blocks, rocks etc)
    4. ;
    5. ; input:
    6. ;    d1 = width / 2
    7. ;    d2 = height / 2 (when in the air (air_radius))
    8. ;    d3 = height / 2 (when on the object (stand_radius))
    9. ;    d4 = x-axis position of the object
    10. ; ---------------------------------------------------------------------------
    11.  
    Well, not quite. What all these objects actually input is the object's width radius + Sonic's push radius (which is always 10, just Sonic's size for pushing stuff) plus 1. So the value will always be 11 more than usual. Here's an example of that for the push block:
    Code (ASM):
    1.  
    2. ;Solidity code for the push block
    3. loc_BF6E:    ; Routine 2
    4.         tst.b    $32(a0)
    5.         bne.w    loc_C046
    6.         moveq    #0,d1
    7.         move.b    obActWid(a0),d1 ; < d1 = object's width radius
    8.         addi.w    #$B,d1               ; < adds 11 ($B) to d1 to compensate for Sonic's push radius plus 1
    9.         move.w    #$10,d2
    10.         move.w    #$11,d3
    11.         move.w    obX(a0),d4
    12.         bsr.w    loc_C186
    13.  
    And this width is also used when standing on an object and checking whether Sonic should exit at either side.

    The extra 1 pixel is added because the object essentially wants to check 1 extra pixel either side to facilitate pushing and other things, even if Sonic isn't actually overlapping yet.

    Anyway, jump through platforms don't do this.
    Code (ASM):
    1.  
    2. loc_7F06:
    3.         moveq    #0,d1
    4.         move.b    obActWid(a0),d1 ; < d1 = object's width radius
    5.                                                 ; No Sonic radius compensation :(
    6.         bsr.w    ExitPlatform
    7.  
    It's just their raw width radius, so Sonic will fall off of a platform as soon as his position leaves the edge. In fact, the game actually compensates by giving all solid objects including platforms a few extra pixels for Sonic to balance (4 extra on each side, meaning 4 before the edge) on than normal.
    upload_2024-8-22_11-38-42.png He's balancing even though his centre isn't over the edge. This isn't how it works for terrain cliffs.

    Is THIS deliberate? Yes, I believe so. If you force all platforms to add Sonic's radius to the walkable area, it causes sloped platforms to glitch out at the edges as I assume it can lead them to reference non-existent slope data. All of this could have been solved. But again, seems a strange bandaid solution was reached instead of solving the core issue.
     
    Last edited: Sep 1, 2024
    • Like Like x 3
    • Informative Informative x 2
    • List
  4. JcFerggy

    JcFerggy

    Do you want to taco 'bout it? Member
    1,409
    110
    43
    This has been quite the interesting read. I always assumed Sonic not being able to land on the edge of blocks was to imply some "realistic" logic. That a ball, when falling down, will slide/roll off a corner instead of landing perfectly. Seeing it slowed down like that though, it's almost jarring how fast Sonic shifts on the X-asis. I wonder what would need to be done to smooth out that repositioning, like only moving so many pixels outward per frame. Alternately, I wonder how a more realistic take on an edge collision would be, having Sonic be knocked away at the angle of deflection relative to the surface.
     
  5. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    I definitely agree, it does make sense from a realistic physics perspective.

    What makes it inconsistent and potentially annoying is that this doesn't happen with cliffs of the terrain, only objects.
    If you learn how to handle corner jumps on the terrain, suddenly those instincts will be incorrect when making jumps onto objects, and off of platforms.

    Regarding that pop: when Sonic is overlapping a solid object, the object will decide which way to push Sonic out - either horizontally or vertically. The pop happens when this decision changes.

    For some extra detail, it does so like this:
    It calculates the Y distance from Sonic to the nearest horizontal edge of the object (top or bottom), and the X distance from Sonic to the closest vertical edge of the object (left or right). Whichever of these values is smaller/larger will determine a vertical or horizontal collision.
    Code (ASM):
    1.  
    2. Solid_Collision:
    3.         ; Unrelated checks
    4.         tst.b    (v_lock_multi).w  
    5.         bmi.w    Solid_NoCollision  
    6.         cmpi.b    #id_Sonic_Death,(v_ost_player+ost_routine).w
    7.         if Revision=0
    8.             bcc.w    Solid_NoCollision
    9.         else
    10.             bcc.w    Solid_Debug
    11.         endc
    12.         tst.w    (v_debug_active).w  
    13.         bne.w    Solid_Debug
    14.  
    15.         ; Get horizontal edge distance
    16.         move.w    d0,d5                    ; d0/d5 = x pos of Sonic on object
    17.         cmp.w    d0,d1                    ; d1 = object half width
    18.         bcc.s    .sonic_left                ; skip if Sonic is on the left side (next lines just do some inversion)
    19.         add.w    d1,d1
    20.         sub.w    d1,d0
    21.         move.w    d0,d5
    22.         neg.w    d5                    ; d5 = x dist of Sonic from left/right edge (nearest)
    23.  
    24.     .sonic_left:
    25.  
    26.         ; Get vertical edge distance
    27.         move.w    d3,d1                    ; d1/d3 = y pos of Sonic's feet on object
    28.         cmp.w    d3,d2                    ; d2 = object half height
    29.         bcc.s    .sonic_top                ; skip if Sonic is on top half (next lines just do some inversion)
    30.         subq.w    #4,d3
    31.         sub.w    d4,d3
    32.         move.w    d3,d1
    33.         neg.w    d1                    ; d1 = y dist of Sonic from top/bottom edge (nearest)
    34.  
    35.     ; Here's where the actual comparison happens
    36.     .sonic_top:
    37.         cmp.w    d1,d5         ;is d5 (x edge dist) higher than d1 (y edge dist)
    38.         bhi.w    Solid_TopBottom                ; if so, Sonic is nearer top/bottom than left/right
    39.  
    Here's a visual example:
    SPGSolidObjectNearerSide.png
    (This range if when Sonic is airborne while upright, if he was curled the range would be a bit shorter vertically)

    Only when Sonic's position enters the purple horizontal area will the object push him out to the side, this is why he snaps outwards. The reason why it seems like it happens a bit late is because before the horizontal pop, Sonic is within the green vertical area, but isn't landing due to the failed horizontal check which cancels the collision.

    To smooth it out, they'd have to put more effort in to change how much he gets pushed based on the distance from the top.
     
    Last edited: Aug 22, 2024
    • Informative Informative x 3
    • Like Like x 1
    • List
  6. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    Landing on objects is very weird (part 2)

    A related observation to those above, the "slipping over corners while landing on objects" issue allows you to skip this puzzle in Marble Zone. I don't know how well known this is but it's probably the most egregious bug this causes - that I know of. But it is pretty tough to do by accident.
    SPGMZButtonSkipPart1.gif

    I don't really have any extra source code to show for this, I just wanted to show this example as it's quite interesting to see how this issue can actually "break" the game.

    There's around only 1 pixel you can to be standing on to make it work. You'd be able to do it more easily if the corners of the terrain weren't so close by, as the terrain doesn't allow for any "slipping" like objects do.
    SPGMZButtonSkipPart2.gif
    Notice how I nudge Sonic up so that his centre is 1 pixel off the object? Luckily, it's also just enough so that his floor sensors don't touch the terrain on the way down.

    Before he jumps, his width radius (which dictates where his floor sensors are) is 9, though when curled up this changes to 7 making his "feet" thinner by 2 pixels either side. So even though his left floor sensor is on the terrain before he jumps, it just misses on the way down. That's the only reason this is so specific.

    And yes, the other side works too.
    SPGMZButtonSkipPart3.gif
     
    Last edited: Aug 25, 2024
    • Informative Informative x 8
    • Like Like x 3
    • Useful Useful x 3
    • List
  7. Tiberious

    Tiberious

    Yeah, I'm furry. Got a problem? Oldbie
    Since we're on the subject of the block 'puzzle' in Marble 1, has anyone adequately explained precisely WTH is going on with that push block that lets you just roll into it for a hands-free solution? This is the only spot you encounter this, as it doesn't work in later games, and due to block position in the proto, it's not 100% hands-free.

    That said, I spent a fair bit of time in the proto's Marble 1 (as that's what I had on-hand), and couldn't get the skip to work. This did, however, work in Sonic Delta's Marble Zone 1, at both 0A27, and 0A98, but it's a bit fidgety. I wonder what the proto is doing different that prevents it from happening.
     
  8. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    That's a great question. Complex reasons behind this one too. I've covered the S1 Push Block mechanics on the Sonic Physics Guide, but writing this post might facilitate a better explanation of what's happening (I'll try, anyway).

    First I'll explain a bit about how the Push Block works, and then explain why rolling into it causes Sonic to get stuck pushing it.

    Here's the code at the moment Sonic makes contact with the Push Block:
    Code (ASM):
    1.  
    2. ; ===========================================================================
    3.  
    4. PushB_Solid_Detect:
    5.         bsr.w    Solid_ChkCollision            ; Block acts solid & update flags for interaction (this is where it checks for Sonic, and pushes him out if he is overlapping)
    6.         tst.w    d4
    7.         beq.w    PushB_Solid_Exit            ; branch if no collision
    8.         bmi.w    PushB_Solid_Exit            ; branch if top/bottom collision
    9.         tst.b    ost_pblock_lava_flag(a0)
    10.         beq.s    PushB_Solid_Side            ; branch if not on lava
    11.         bra.w    PushB_Solid_Exit
    12.  
    13.  
    14. ; The following code will run when the game has confirmed Sonic has collided with a side of the Push Block and the push block is not in lava, normal Solid Object collision code and repositioning has already occurred at this point
    15.  
    16. PushB_Solid_Side:
    17.         tst.w    d0                    ; where is Sonic?
    18.         beq.w    PushB_Solid_Exit            ; if inside the object, branch
    19.         bmi.s    PushB_Solid_Left            ; if left of the object, branch
    20.  
    21. ; The following code will run if Sonic is pushing the left of the object
    22.  
    23.         btst    #status_xflip_bit,ost_status(a1)    ; is Sonic facing left?
    24.         bne.w    PushB_Solid_Exit            ; if yes, branch
    25.         move.w    d0,-(sp)
    26.         moveq    #0,d3
    27.         move.b    ost_displaywidth(a0),d3
    28.         jsr    (FindWallRightObj).l
    29.         move.w    (sp)+,d0
    30.         tst.w    d1                    ; has the block hit right wall?
    31.         bmi.w    PushB_Solid_Exit            ; if not, branch
    32.         addi.l    #$10000,ost_x_pos(a0)            ; Move the Push Block 1px right and clear its subpixels
    33.         moveq    #1,d0
    34.         move.w    #$40,d1                    ; d1 = Sonic's new Ground Speed will be 0.25 (64 subpixels) when he has pushed left
    35.         bra.s    PushB_Solid_Side_Sonic
    36. ; ===========================================================================
    37.  
    38. ; The following code will run if Sonic is pushing the right of the object
    39. PushB_Solid_Left:
    40.         btst    #status_xflip_bit,ost_status(a1)    ; is Sonic facing right?
    41.         beq.s    PushB_Solid_Exit            ; if yes, branch
    42.         move.w    d0,-(sp)
    43.         moveq    #0,d3
    44.         move.b    ost_displaywidth(a0),d3
    45.         not.w    d3
    46.         jsr    (FindWallLeftObj).l
    47.         move.w    (sp)+,d0
    48.         tst.w    d1                    ; has the block hit left wall?
    49.         bmi.s    PushB_Solid_Exit            ; if not, branch
    50.         subi.l    #$10000,ost_x_pos(a0)            ; Move the Push Block 1px left and clear its subpixels
    51.         moveq    #-1,d0
    52.         move.w    #-$40,d1    ; d1 = Sonic's new Ground Speed will be -0.25 (-64 subpixels) when he has pushed left
    53.  
    54. ;Now that the side Sonic is on has been established and the speeds have been calculated, he following code will run to complete the push
    55. PushB_Solid_Side_Sonic:
    56.         lea    (v_ost_player).w,a1
    57.         add.w    d0,ost_x_pos(a1)            ; + or - 1 to Sonic's X Position
    58.         move.w    d1,ost_inertia(a1)            ; Set Sonic's Ground Speed to the value in d1 which was calculated above. This will either be 0.25 (64 subpixels) or -0.25 (-64 subpixels) (either $40 or -$40)
    59.         move.w    #0,ost_x_vel(a1)
    60.         move.w    d0,-(sp)
    61.         play.w    1, jsr, sfx_Push            ; Play pushing sound
    62.  
    When Sonic touches the Push Block, after popping Sonic out and setting his X Speed to 0 like a normal solid object, it uniquely sets his Ground Speed to 0.25 (64 subpixels) in the direction of the push. This will be the most important detail.
    (It also moves itself and Sonic 1 pixel ahead, this is the actual push, but this is irrelevant as they both move together).

    The Push Block was made in such a way that relies on the very specific way the object collision works, and then puts it's own spin on it. There's a reason that pushing is as slow as it is. When Sonic is pushing normally, this push happens around every 2-3 frames. Sonic isn't moving the Push Block each frame, because when pushing on any object he isn't even making contact with it each frame. Yep. Sonic will actually only touch an object once every few frames while pushing.

    Object collision ignores Sonic's subpixel (and the object's own subpixel) so when the object checks for a collision with Sonic, it will consider both of their positions effectively rounded down. Also, when Sonic gets popped out of an object, his subpixel is preserved.

    Here's an example where Sonic is pushing the left side of the Push Block:
    --
    • If on the frame of contact Sonic's X Position is 50.25, this means his leftmost edge is 40.25 (his X Position minus his Push Radius, which is always 10).
    • However, when the collision check occurs the Push Block will consider Sonic's X Position as 50 and his leftmost edge as 40, ignoring subpixel.
    • If the block's rightmost edge was also 40, the block simply considers Sonic to be overlapping by 1, and will push Sonic out by 1. Resulting in a new X Position of 51.25. This places Sonic 0.75 pixels away from touching the object once again.

    No matter the difference between the rightmost edge of the object and the leftmost position of Sonic is, the object will always push him out a whole number of pixels.
    --

    This means each time Sonic makes contact with the block, he's somewhere between 0 and 1 pixels away from the block. At low speeds this is going to be closer to 1 pixel away. Sonic has to cover this subpixel distance before the block considers him overlapping and the push will activate again. This is why Sonic's Ground Speed is set to 0.25 (or -0.25), it's a way to control how long this takes. If the Push Block didn't set Sonic's Ground Speed to anything, he'd actually push it much slower.


    • When upright and not holding forward, Sonic's friction (0.046875 or 12 subpixels) is more than enough to stop Sonic before he touches the block again when starting at a Ground Speed of 0.25. This is because when starting at 0.25, Sonic can stop after decelerating for just over half a pixel (around 0.6 of a pixel).
    • However, when rolling, friction is half of this (0.0234375 or 6 subpixels). At this low of a friction, Sonic will not stop before passing over 1 pixel (around 1.3 pixels) when starting at 0.25, which guarantees he will touch the block again each time, resulting in this endless pushing "loop".

    You may expect that after actively pushing into the block (while upright) and then releasing, due to the apparent randomness of what Sonic's subpixel position might be on the frame of collision, it's possible some extra pushes will happen until Sonic is popped out to a "safe" distance from the block, but Sonic can't actually gain enough speed (even while actively accelerating into the block) from within 1 pixel and starting at 0.25 to travel too far into the block to cause any issues.

    If you were to modify this 0.25 value, increasing it, to 0.5 or so, Sonic will both push the block faster, and become stuck pushing it as he does when rolling, all while upright.
    To conclude, Sonic having too low of a friction value (happens when rolling), or the speed Sonic's set to upon contact with the block being too high (hypothetically) will both cause this issue.

    So yeah, that's why!

    Edit: Should also note that aside from any object specific quirks, this is exactly how the pushing against terrain walls happens too. Sonic will be popped out, and have to cover the distance within the pixel toward the wall before being popped out again.



    -----------------
    I'm not certain what the later games do for their pushable objects, I need to look at them closer, but it's likely they do it completely differently.
    As for the proto, I have no idea! It's possible the object slipping is lesser or absent, I haven't explored it enough to compare (I shall though, that's very interesting)

    EDIT: I tested the prototype out and just from observation, it's clear to me the object edge slipping when landing is much lesser (it might be absent entirely, but some slipping will always remain due to how object collision decides which side Sonic collided with), this suggests it's certainly a deliberate change. Possibly for the reason I stated but who knows.
     
    Last edited: Aug 31, 2024
    • Informative Informative x 4
    • List
  9. RetroKoH

    RetroKoH

    Member
    1,734
    112
    43
    S1Fixed: A successor to ReadySonic
    Sorry, I just wanna clarify. When you say hands-free solution, I assume you mean rolling into the block and not needing to push anything because Sonic continues to push the block forward while spinning, right?
     
  10. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    That's what I took it to mean


    More solid object slander -
    Pushing Is Uneven
    Note: This is yet another 1px misalignment explanation, but it has always bothered me, even moreso once I learned what's actually happening

    Take a look at this image:
    upload_2024-8-29_16-18-7.png
    Sonic is pushing the wall on the right, he is perfectly aligned to the wall. Flush. Lovely.

    Now let's take a look at the other side:
    upload_2024-8-29_16-18-29.png
    Uh oh, Sonic is 1 pixel inside the wall. Notice the dark area of the palm of his hand isn't visible. Before I explain why this is, I need to show you the same when Sonic pushes against solid objects.

    upload_2024-8-29_16-35-4.png
    Well, these look perfect! Exactly flush. Both palms visible. Bravo!

    It would be reasonable to look at this and conclude that the pushing against terrain walls is uneven, and that object collision is actually perfect for once.

    You might wonder why the terrain is wrong...
    "Is Sonic's left terrain wall sensor 1 pixel too short?"
    "Is there something wrong with the solidty of walls on the left?"
    .

    Well... no. Looks can be deceiving.
    It is in fact the object that is incorrect.

    I'll go over how things are set up for both types of collision, and then explain why it looks like the opposite is happening visually.

    Terrain Pushing:
    Sonic's sensor arrangement for terrain is perfectly symmetrical. He will push against walls from the same distance either side.
    upload_2024-8-29_19-43-40.png
    The red and purple lines that extend out either side, and specifically those white points at the end, are Sonic's Push Sensors, where the wall terrain checks occur. Both are exactly 10 pixels away from the centre, 10 being Sonic's Push Radius.

    To really simplify things, if the Push Sensors on the sides of Sonic overlap a wall, Sonic will be pushed out by the amount they are overlapping. These are always positioned perfectly even at his sides, there's never any uneven misalignments caused by Sonic's sensors.

    You can safely bet on any apparent misalignment with terrain being the fault of the terrain solidity itself. But here, even the terrain itself is perfect, and essentially every terrain wall is "perfect".
    upload_2024-8-29_16-25-4.png

    Regardless of being perfect, pushing left against terrain looks wrong.​

    Object Pushing:
    Objects are NOT perfect. They never are.

    The problem here with solid objects is that the solid area actually extends 1 pixel too far.

    Quick refresher: as stated in a previous post, all normal fully solid object solidity (like push blocks, etc) will accept the objects width radius as an input, as seen here:
    Code (ASM):
    1.  
    2. ; ---------------------------------------------------------------------------
    3. ; Solid    object subroutine (includes spikes, blocks, rocks etc)
    4. ;
    5. ; input:
    6. ;    d1 = width / 2 (the combined_x_radius)
    7. ;    d2 = height / 2 (when in the air)
    8. ;    d3 = height / 2 (when on the object)
    9. ;    d4 = x-axis position of the object
    10. ; ---------------------------------------------------------------------------
    11.  
    But not quite. What all these objects actually input is the object's width radius + Sonic's push radius (which is always 10, just Sonic's size for pushing stuff) + 1. So the value will always be 11 more than usual.

    For example, a Push Block will use a value of 27, which is [Push Block's Width Radius (16) + Sonic's Push Radius (10) + 1]
    Note: The 1 that's added here is not the cause of the issue, this is added for both sides of the object and is accounted for, it just allows the object to "sense" Sonic 1 pixel early.

    I'll call this input value the object's combined_x_radius, and combined_x_diameter will be double the combined_x_radius.

    With that, here's the code which checks the initial horizontal overlap of Sonic and a Solid Object.
    Code (ASM):
    1.  
    2. Solid_ChkCollision:
    3.         tst.b    ost_render(a0)                ; is object onscreen?
    4.         bpl.w    Solid_NoCollision            ; if not, branch
    5.  
    6. Solid_SkipRenderChk:
    7.         lea    (v_ost_player).w,a1
    8.  
    9.         ; Because the extra 11 pixels was added for the object's combined_x_radius, the object can just calculate using Sonic's position
    10.         move.w    ost_x_pos(a1),d0        ; d0 = Sonic's X Position
    11.         sub.w    ost_x_pos(a0),d0        ; d0 = Sonic's X Position - Object's X Position
    12.         add.w    d1,d0                    ; d0 = (Sonic's X Position - Object's X Position) + combined_x_radius
    13.                                         ; To clarify: d0 = x position of Sonic relative to the object's leftmost edge (we'll call this the comparison_position)
    14.  
    15.         ;Here is the check for Sonic being too far to the left to collide
    16.         bmi.w    Solid_NoCollision        ; if d0 is negative, Sonic is outside left edge, so dont collide
    17.  
    18.         ;Here is the check for Sonic being too far to the right to collide
    19.         move.w    d1,d3                    ; d3 = Object's combined_x_radius (in the case of a Push Block this will be 27)
    20.         add.w    d3,d3                    ; d3 = Object's combined_x_diameter (this will be 54)
    21.         cmp.w    d3,d0                    ; compare d0 (the comparison_position) with d3, the Object's combined_x_diameter
    22.         bhi.w    Solid_NoCollision        ; if the comparison_position is higher than the combined_x_diameter, Sonic is outside right edge, so don't collide
    23.  
    The issue here is that last line, which cancels collision only if the difference between Sonic's X Position and the leftmost position of the object is higher than the combined_x_diameter. Higher is the keyword.

    All collision ignores the subpixels of both the object and Sonic, so if the comparison_position was 54.75 pixels, which is technically larger than combined_x_diameter (54), the object would still see it as being equal and he'd still collide as it disregards that 0.75 - meaning he'd have to be a whole 1 pixel or more away from the object to not be considered touching it, and thus the object has an extra pixel of solid size.

    There are multiple parts of the code which reinforce this extra 1 pixel which make this issue consistent, but this is a good example to show here.​

    In case you need a visual representation of what the result of this is:
    upload_2024-8-29_16-31-4.png
    Notice the extra line of green pixels on the bottom and right edge of the Push Block that extends past the sprite. You can also see them on the right edge of the Spike Trap. This affects every solid object

    That's right, Sonic is actually 1 pixel further away from an object when pushing it on the right, because the solid box extends too far.
    upload_2024-8-29_19-26-1.png
    Notice Sonic's wall sensor (which isn't used by objects, but does mark where Sonic's leftmost edge for pushing is supposed to be) is 1 pixel away from the Push Block. When Sonic pushes the left side of the Push Block, there is no gap between the Push Block and the push sensor.

    Regardless of being wrong, pushing left against terrain looks perfect.

    So when Sonic is flush with the wall, he appears 1px inside it, and when he's 1px away from the object, he appears flush. At this point, it might be obvious what's going on...


    Visual Pushing:
    The reason terrain pushing appears wrong (even though it's correct) while object pushing appears correct (even though it wrong), is that Sonic's sprite is misaligned by 1 pixel to the left when it's flipped.

    Ideally, the pixel of the sprite that is overlapping Sonic's X Position would still be overlapping Sonic's X Position when flipped, but it is not.
    SPGSensors.gif
    The result of this is that it shifts the sprite 1 pixel left when facing left. Sure, this handily compensates for the 1 pixel gap when pushing objects, but also causes the imperfect overlap when pushing the terrain.

    This issue happens throughout all the classic games - though Sonic 3 changes the pushing sprite position so he just overlaps everything.​


    It's more than just the solid size of objects. This sizing issue permeates any radius based box/size in the game, including hitboxes.

    Hitboxes:
    For example, a Ring will use 6 as it's hitbox's width and height radius (more accurately it chooses a hitbox of this size from a table of predefined hitboxes). This radius value is supposed to be half the hitbox's width/height, but it is not.
    SPGHitBoxRadius.png
    The hitbox has to calculate where it's leftmost and rightmost edges are. One way this can occur is simply by adding or subtracting the radius from it's position (the "origin" in this case) as this invertedly results in the origin being included in the apparent size of the hitbox, making the total size both an odd number. This makes it 1px too big on the right and bottom edges, just like the solid area of objects. This gives the Ring hitbox a total size of 13 rather than 12.

    Because the box is an odd number in size, this means you can collect a ring placed poking 4px out of a wall on the left, but you can't collect a ring placed poking 4px out of a wall on the right.
    SPGRingTest.gif
     
    Last edited: Aug 30, 2024
    • Informative Informative x 6
    • Like Like x 1
    • List
  11. Hivebrain

    Hivebrain

    Administrator
    3,077
    211
    43
    53.4N, 1.5W
    Github
    I always assumed this was purely a visual bug. In my hack I added a check to BuildSprites that shifts xflipped sprites 1px to the right if a bit is set in the render flags.
     
    • Agree Agree x 2
    • Like Like x 1
    • List
  12. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    It certainly is only visual for the terrain! The object stuff is the only thing that overcomplicates this issue, annoyingly.
    And yeah that fix is ideal for the visual side though I'd be curious to know if you notice a 1px gap when you push on the right sides of objects since making that change. Assuming your hack's object collision hasn't been changed and the graphics of the object are even, I imagine you would.
     
  13. Hivebrain

    Hivebrain

    Administrator
    3,077
    211
    43
    53.4N, 1.5W
    Github
    s1built_000.png s1built_001.png
    I rewrote object collision from scratch so this probably doesn't apply to the original game. Visually there's no gap between Sonic and the green block. I'm now noticing that Sonic's feet shouldn't overlap the metal block by 1px, so some tinkering is needed.
     
  14. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    Looks like you nailed the object pushing collision then! That's very satisfying tbh, love it.

    I decided to apply the same X flip alignment fix to barebones Sonic 1 to demonstrate what happens. Essentially, it exposes the truth of what's really happening

    Code (ASM):
    1.  
    2. ; This is the code to build a sprite which is flipped on the X axis
    3. BuildSpr_FlipX:
    4.         btst    #1,d4        ; is object also y-flipped?
    5.         bne.w    BuildSpr_FlipXY    ; if yes, branch
    6.  
    7.     .loop:
    8.         cmpi.b    #$50,d5        ; check sprite limit
    9.         beq.s    .return
    10.         move.b    (a1)+,d0    ; y position
    11.         ext.w    d0
    12.         add.w    d2,d0         ; add y-position
    13.         move.w    d0,(a2)+
    14.         move.b    (a1)+,d4    ; size
    15.         move.b    d4,(a2)+  
    16.         addq.b    #1,d5        ; link
    17.         move.b    d5,(a2)+
    18.         move.b    (a1)+,d0    ; art tile
    19.         lsl.w    #8,d0
    20.         move.b    (a1)+,d0  
    21.         add.w    a3,d0
    22.         eori.w    #$800,d0    ; toggle flip-x in VDP
    23.         move.w    d0,(a2)+    ; write to buffer
    24.         move.b    (a1)+,d0    ; get x-offset
    25.         ext.w    d0
    26.         neg.w    d0            ; negate it
    27.         add.b    d4,d4        ; calculate flipped position by size
    28.         andi.w    #$18,d4
    29.         addq.w    #8,d4
    30.         sub.w    d4,d0
    31.         add.w    d3,d0
    32.  
    33.         ; EXTRA LINE, this is the "fix", adds 1 to the sprite x
    34.         add.w    #1,d0         ; add 1
    35.  
    36.         andi.w    #$1FF,d0    ; keep within 512px
    37.         bne.s    .writeX
    38.         addq.w    #1,d0
    39.  
    40.     .writeX:
    41.         move.w    d0,(a2)+    ; write to buffer
    42.         dbf    d1,.loop        ; process next sprite piece
    43.  
    44.     .return:
    45.         rts  
    46.  
    upload_2024-8-30_10-55-17.png
    Pushing against terrain walls now looks perfect, as it actually is.

    upload_2024-8-30_10-56-46.png
    Pushing against objects shows a noticeable gap, as there actually is.
     
    Last edited: Aug 30, 2024
  15. RetroKoH

    RetroKoH

    Member
    1,734
    112
    43
    S1Fixed: A successor to ReadySonic
    Please correct me if I'm wrong, but the Animal Prison button also has a similar quirk to the Vertical Spring, no?

    I tried it out today, and it looks like Sonic doesn't even properly land on it. It sinks down on the same frame that he collides with it, and it looks like he lands in air.
     
  16. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    upload_2024-8-30_16-23-14.png
    In Sonic 1, when he lands on it the button does move down 8 pixels so that it appears pressed, however it seems like they gave up on any idea of it being solid after this as it also immediately forces him off the button (makes him airborne) and the button solidity seems to vanish too (because the routine that provides the button's solidity stops running), so he can never actually walk on it. You typically end up falling down onto the main solid box of the capsule/prison as you move off to the side.
    Code (ASM):
    1.  
    2. Pri_Switch:    ; Routine 4
    3.         ; Here the button acts solid
    4.         move.w    #$17,d1
    5.         move.w    #8,d2
    6.         move.w    #8,d3   ; and it NEGLECTS to add 1 to the stand_radius, tsk tsk. Not that sonic can ever stand on it
    7.         move.w    ost_x_pos(a0),d4
    8.         jsr    (SolidObject).l
    9.         lea    (Ani_Pri).l,a1
    10.         jsr    (AnimateSprite).l
    11.         move.w    ost_prison_y_start(a0),ost_y_pos(a0)
    12.         tst.b    ost_solid(a0)                ; is Sonic on top of the switch?
    13.         beq.s    .not_on_top                ; if not, branch
    14.  
    15.         ; Activate the capsule
    16.         addq.w    #8,ost_y_pos(a0)            ; move switch down 8px
    17.         move.b    #id_Pri_Explosion,ost_routine(a0)    ; goto Pri_Explosion next
    18.         move.w    #60,ost_anim_time(a0)            ; set time for explosions to 1 sec
    19.         clr.b    (f_hud_time_update).w            ; stop time counter
    20.         clr.b    (f_boss_boundary).w            ; lock screen position
    21.         move.b    #1,(f_lock_controls).w            ; lock controls
    22.         move.w    #(btnR<<8),(v_joypad_hold).w        ; make Sonic run to the right
    23.  
    24.         ; Here Sonic's status is cleared, making him airborne
    25.         clr.b    ost_solid(a0)
    26.         bclr    #status_platform_bit,(v_ost_player+ost_status).w
    27.         bset    #status_air_bit,(v_ost_player+ost_status).w
    28.  
    29.     .not_on_top:
    30.         rts
    31.  
    In fact, I believe it forces you off the button because the solidity disappears, otherwise Sonic doesn't get updated and remains walking "on the button" through the air all the way off screen.
    Also, as a little throwback to the first post, the capsule button is yet another object that has an "unfixed" stand_radius, like the switch.

    In Sonic 2, the button also moves down when pressed (and back up when unpressed) but obviously it doesn't disappear or make Sonic airborne or anything strange.
    Code (ASM):
    1.  
    2. loc_3F354:
    3.     ; Here the button acts solid
    4.     move.w    #$1B,d1
    5.     move.w    #8,d2
    6.     move.w    #8,d3 ; and it ALSO NEGLECTS to add 1 to the stand_radius, and you can see Sonic stands 1px inside of it
    7.     move.w    x_pos(a0),d4
    8.     jsr    (SolidObject).l
    9.     move.w    objoff_30(a0),y_pos(a0)
    10.     move.b    status(a0),d0
    11.     andi.b    #standing_mask,d0
    12.     beq.s    +
    13.  
    14.     addq.w    #8,y_pos(a0)                ; move switch down 8px
    15.     clr.b    (Update_HUD_timer).w
    16.     move.w    #1,objoff_32(a0)
    17. +
    18.     jmp    (MarkObjGone).l
    19.  
    As you can see, the stand_radius wasn't fixed in Sonic 2 so Sonic sinks 1 pixel into the button:
    upload_2024-8-30_16-43-24.png
     
    Last edited: Aug 30, 2024
    • Informative Informative x 4
    • Like Like x 2
    • List
  17. TwoSpaces

    TwoSpaces

    Member
    They also completely ruined pushing against solid walls in some versions of the J2ME source port of Sonic 1:
    java_kjxDN2PE4G.gif
     
    • Agree Agree x 2
    • Like Like x 1
    • Informative Informative x 1
    • List
  18. Lapper

    Lapper

    Lappering Tech Member
    1,782
    1,014
    93
    England
    Sonic Studio, Sonic Physics Guide, Kyle & Lucy, Freedom Planet 2
    Hidden Solid Objects

    I want shine a spotlight on some of the various different types of solid objects that get hidden or disguised in walls in the classic games. I actually mentioned this phenomenon previously in my post regarding landing on solid objects as a possible reason for object edge slipping.

    There are a few different reasons the games will hide solid objects within the terrain.

    Crushing
    You may know these best as the Robotnik icons in debug mode. Any time Sonic needs to be crushed between an object he is standing on and the ceiling of the terrain, there will be an object embedded in the wall above Sonic. This is because crushing is only activated by Sonic touching the bottom of an object while standing on something (on an object or on the ground), so no matter the scenario, an object needs to be above Sonic.
    Code (ASM):
    1.  
    2. ; Sonic is touching the object, and he's below it
    3. Solid_Below:
    4.         tst.w    ost_y_vel(a1)
    5.         beq.s    Solid_Squash                ; Check if the object should kill Sonic if he isn't moving up/down
    6.         bpl.s    Solid_TopBtmAir                ; branch if moving downwards
    7.         tst.w    d3
    8.         bpl.s    Solid_TopBtmAir                ; branch if nearer top (he can't be)
    9.         sub.w    d3,ost_y_pos(a1)            ; correct Sonic's position
    10.         move.w    #0,ost_y_vel(a1)            ; stop Sonic moving
    11.  
    12. Solid_TopBtmAir:
    13.         moveq    #-1,d4                    ; return top/bottom collision
    14.         rts
    15. ; ===========================================================================
    16.  
    17. ; Check if the object should kill sonic
    18. Solid_Squash:
    19.         btst    #status_air_bit,ost_status(a1)        ; is Sonic in the air?
    20.         bne.s    Solid_TopBtmAir                ; if yes, dont kill sonic
    21.  
    22.         ; And here it actually kills Sonic
    23.         ; To recap, if Sonic is on the ground while not moving up and down and is touching the bottom of an object, he will die
    24.         move.l    a0,-(sp)                ; save address of OST of current object to stack
    25.         movea.l    a1,a0                    ; temporarily make Sonic the current object
    26.         jsr    (KillSonic).l                ; kill Sonic
    27.         movea.l    (sp)+,a0                ; restore address of OST of current object from stack
    28.         moveq    #-1,d4                    ; return top/bottom collision
    29.         rts
    30.  

    Pushing
    Some objects are placed in walls to actually make them solid when they otherwise wouldn't be.

    Though even when there IS a solid terrain wall, when Sonic is standing still on an object he isn't actively searching for terrain to his sides. Sonic will only collide with terrain walls when he is actively walking towards them. So, if said object was to pass through a wall Sonic would also pass right through. Objects however, are always actively looking for Sonic and will push him out of them, so these are placed in walls to ensure he can't be pulled through.

    Level Design Additions
    Sometimes, especially in Sonic 1, solid objects will be used to actually modify the layout of the terrain. Especially the blockier levels. These will fill gaps or become platforms, pretending to be level geometry, hiding in plain sight.

    Other
    There are also some that are purely visual, but those aren't too interesting.


    Let's look at some specific examples (not exhaustive at all)


    Green Hill Zone
    GHZ uses a lot of "fake" solid object edge pieces to stop Sonic passing through what would otherwise be "wall-less" jump-through terrain. These dont necessarily add to or change the level design, they moreso create solidity for existing walls that otherwise wouldn't have it, which Green Hill needs a lot of due to most of it being jump-through.

    For example this area. If we remove the fake edge object, we can see the level chunks don't line up at all and Sonic can just walk right through this seam.

    Note: Green represents object solidity, checkered areas are terrain tiles (blue are top-solid only).
    upload_2024-9-1_14-58-19.png
    These are interesting as they have unique collision code. Sonic can't land on these at all, he'll pass right through the top and will only get popped out the sides or the bottom.
    Code (ASM):
    1.  
    2. .topbottom:
    3.         tst.w    ost_y_vel(a1)                ; is Sonic moving downwards?
    4.         bpl.s    .exit2                    ; if yes, don't collide
    5.         tst.w    d3                    ; is Sonic above the object?
    6.  
    7.         ; Here, Sonic has hit the top
    8.         bpl.s    .exit2                    ; if yes, don't collide. This right here is in replacement of being able to land on it.
    9.  
    10.         ; Here, Sonic has hit the bottom
    11.         sub.w    d3,ost_y_pos(a1)            ; correct Sonic's position
    12.         move.w    #0,ost_y_vel(a1)            ; stop Sonic moving
    13.  
    14.     .exit2:
    15.         rts
    16.  
    There's a good reason for this. Most of these fake walls are placed right up against the top of the terrain, and the normal object solidity code has a 4 pixel surface tolerance to snap Sonic onto the top of it! So if these used typical object solidity, Sonic would potentially snap down onto them through the terrain, not good.

    Another example is this towering behemoth of fake edges to stop Sonic being able to pass through this tower. It goes all the way to the top.
    upload_2024-9-1_15-0-49.png

    Here in the tunnel you can notice the collision for the fake wall objects extends a bit further down than the sprite, so you get stopped early while jumping in this tunnel by apparently nothing.
    Sonic1BumpEdge.gif


    Marble Zone and Labyrinth Zone
    MZ and LZ are the biggest offenders of fake level geometry, here are just some examples:
    upload_2024-9-1_15-7-23.png
    upload_2024-9-1_15-7-47.png
    Enough said, all of the green areas are "fake" objects that you may otherwise never notice.


    Funfact: the crushing when a MZ Spike Trap gets close to the ceiling is totally custom and does not use any hidden solids. It just assumes there is a ceiling and kills Sonic if he's standing on it at a certain chain length.
    Code (ASM):
    1.  
    2. ; Routine 2 for the Spike Trap
    3. CStom_Block:
    4.         bsr.w    CStom_Types                ; update Spike Trap speed & position
    5.  
    6.         move.w    ost_y_pos(a0),(v_cstomp_y_pos).w    ; store Spike Trap y position for pushable block interaction
    7.  
    8.         ;Here the Spike Trap will act solid
    9.         moveq    #0,d1
    10.         move.b    ost_displaywidth(a0),d1
    11.         addi.w    #$B,d1
    12.         move.w    #$C,d2
    13.         move.w    #$D,d3
    14.         move.w    ost_x_pos(a0),d4
    15.         bsr.w    SolidObject
    16.  
    17.         ; Now it will check if Sonic is standing on it
    18.         btst    #status_platform_bit,ost_status(a0)    ; is Sonic standing on it?
    19.         beq.s    .display                ; if not, branch
    20.         cmpi.b    #$10,ost_cstomp_chain_length(a0)    ; is chain longer than $10?
    21.         bcc.s    .display                ; if yes, branch
    22.  
    23.         ; Here, the chain length is 16 or less ($10) and Sonic is standing on the Spike Trap, so Sonic is crushed
    24.         movea.l    a0,a2
    25.         lea    (v_ost_player).w,a0
    26.         jsr    (KillSonic).l                ; Sonic gets crushed against ceiling
    27.         movea.l    a2,a0
    28.  
    Chemical Plant Zone
    upload_2024-9-1_15-10-26.png
    During the infamous climb through the Mega Mack, the wall is completed lined with invisible solids. These are to stop him being dragged or pushed through the walls by the moving platforms.

    Casino Night Zone
    upload_2024-9-1_15-12-21.png
    There are a lot of moving block objects in CNZ, so a lot of objects are needed in the walls to ensure Sonic doesn't enter them and gets crushed.

    Though, they forgot to even place solid in many places you should be crushed - so you can get dragged into the walls and also not get hurt:
    Sonic2SolidMishap.gif

    Oil Ocean Zone
    The most interesting hidden solid is this:
    Sonic2OOZPlatform.gif
    A hidden platform follows you throughout the entire zone, waiting for you to fall into the oil in order to catch you and slowly lower you down to your death.

    Notice there's no position marker on the platform? The platform is not it's own object. The object performing this collision is more of a "controller object" for the oil generally, and handles this collision separately for both characters and their respective oil submersion. There's essentially a platform for each character, and each is only targeted at a single character exclusively (meaning, Sonic can't stand on the one that follows Tails).
    Each character is given a submersion value that controls how deep the platform is. The bigger the submersion value, the higher the platform is, counterintuitively - This is because this submersion value is actually changing the "height radius" this oil controller inputs for this platform solidity, which is how it dynamically moves the platform up and down using the existing platform code.
    Code (ASM):
    1.  
    2. ; loc_24054:
    3. Obj07_Main:
    4.     ; check player 1
    5.     tst.w    (Debug_placement_mode).w
    6.     bne.w    Obj07_End
    7.  
    8.     ; Check if the player is standing on the platform
    9.     lea    (MainCharacter).w,a1 ; a1=character
    10.     moveq    #p1_standing,d1
    11.     move.b    status(a0),d0
    12.     and.b    d1,d0
    13.     bne.s    Obj07_CheckKillChar1 ;if he is, skip the next lines of code
    14.  
    15.     ; Here, the player is not standing on the platform.
    16.     ; The submersion value is updated
    17.     cmpi.b    #$30,oil_char1submersion(a0) ; 48 ($30) is the maximum submersion value
    18.     beq.s    Obj07_CheckSupportChar1
    19.     addq.b    #1,oil_char1submersion(a0) ;If the player is not standing on the platform, bring it up 1 pixel per frame
    20.     bra.s    Obj07_CheckSupportChar1
    21. ; ---------------------------------------------------------------------------
    22. ; loc_24078:
    23. Obj07_CheckKillChar1:
    24.     ; Here, the player is standing on the platform
    25.     ; Test the submersion, if this value is 0 Sonic will suffocate
    26.     tst.b    oil_char1submersion(a0)
    27.     beq.s    Obj07_SuffocateCharacter
    28.  
    29.     ; If the value isn't 0 and if the player is standing on the platform, drag it down 1 pixel per frame
    30.     subq.b    #1,oil_char1submersion(a0)
    31.  
    32. ; Here is where the "fake" platform is actually created
    33. Obj07_CheckSupportChar1:
    34.     ; Stop the character from falling past the oil by creating a "fake" solid platform
    35.     moveq    #$20,d1                     ; Inputs platform's width radius as 32 ($20)
    36.     moveq    #0,d3
    37.     move.b    oil_char1submersion(a0),d3     ; Inputs submersion value as the height radius
    38.     moveq    #p1_standing_bit,d6
    39.     move.w    x_pos(a1),d4                 ; Inputs platform x position as Player's x position
    40.     move.w    d4,x_pos(a0)                ; Move the controller object to the player
    41.     jsrto    (PlatformObject_SingleCharacter).l, JmpTo_PlatformObject_SingleCharacter ;The Platform solidity -  targeted at a single character, this will be repeated for the other
    42.  
    43.  
    44.  
    The death as it drags you down is also custom, not happening at the bottom of the camera like normal, but rather being handled by this oil controller object.
     
    Last edited: Sep 2, 2024
    • Informative Informative x 8
    • Like Like x 1
    • List
  19. Blastfrog

    Blastfrog

    See ya starside. Member
    Now I’m wondering if 3K handles the oil in MGZ and the quicksand in SOZ the same way. That is just so hacky! But… it works.
     
  20. RetroKoH

    RetroKoH

    Member
    1,734
    112
    43
    S1Fixed: A successor to ReadySonic
    As I was reading this update, I was getting ready to be cheeky and point out the chained stomper tidbit, only to then immediately read it in your post! XD

    Your post raises another question from me. Am I wrong in saying that horizontal crushing does not exist in Sonic 1? I feel like it should, and one instance that comes to my mind is the diagonal moving blocks in Spring Yard Zone (though there are no solid walls to crush you against in that instance).

    Could this be related to why the sideways stomper was taken out of the game... simply not having worked out horizontal crushing and simply removing the object for simplicity's sake? Or... they just felt it was unnecessary?