don't click here

Some changes/fixes for Sonic 1

Discussion in 'Engineering & Reverse Engineering' started by RetroKoH, Sep 4, 2012.

  1. Hivebrain

    Hivebrain

    Administrator
    3,075
    211
    43
    53.4N, 1.5W
    Github
  2. Devon

    Devon

    Please do not contact me, overwhelmed with stuff Tech Member
    1,522
    1,878
    93
    your mom
    Another thing you can do to save on accumulated cycles is to change the object priority variable/SST into a word (there's a guide for that I think), but instead of doing it the S3K way of storing a multiple of $80, instead you'd store the last 16-bits of the beginning of your chosen draw queue (i.e. for priority layer 0, you'd store v_spritequeue, for priority layer 1, you'd store v_spritequeue+$80, for priority layer 2, you'd store v_spritequeue+$100, etc.). This is possible, because the draw queues are in the latter half of RAM, which can be addressed with a signed 16-bit value.

    Then, in DisplaySprite (and any other variant of it), you'd just do something like this:
    Code (ASM):
    1. ; ---------------------------------------------------------------------------
    2. ; Subroutine to display a sprite/object, when a0 is the object RAM
    3. ; ---------------------------------------------------------------------------
    4.  
    5. ; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
    6.  
    7.  
    8. DisplaySprite:
    9.                 movea.w obPriority(a0),a1       ; get sprite priority queue
    10.                 cmpi.w  #$7E,(a1)               ; is this part of the queue full?
    11.                 bhs.s   DSpr_Full               ; if yes, branch
    12.                 addq.w  #2,(a1)                 ; increment sprite count
    13.                 adda.w  (a1),a1                 ; jump to empty position
    14.                 move.w  a0,(a1)                 ; insert RAM address for object
    15.  
    16. DSpr_Full:
    17.                 rts
    18.  
    19. ; End of function DisplaySprite

    And on that note, you could save some memory (both ROM and RAM) and CPU cycles by taking more advantage of 16-bit addresses on the 68000. Not only is it half of a full pointer, but because it's only 1 word and not 2, it saves time on reading the address by only doing 1 bus read. One of the things Sonic 2 did was a bit of just that. You'll notice that an object parent pointers tended to be defined as longwords in Sonic 1, but in Sonic 2, that changed into definitions as words, since it was then understood that every object slot could be addressed with just 16 bits.
     
    Last edited: Sep 17, 2024
    • Like Like x 3
    • Informative Informative x 2
    • List
  3. Kilo

    Kilo

    Starting new projects every week Tech Member
    1,264
    1,198
    93
    Canada
    Changes with the weather
    djohe would be proud of what this thread is turning into. :V
     
  4. RetroKoH

    RetroKoH

    Member
    1,734
    112
    43
    S1Fixed: A successor to ReadySonic
    Sonic 4 control scheme in the Special Stages
    I did this a while back in Fixed (it's toggled off by default, so most people missed it) but here is a guide to do it. It's actually quite easy to do.

    EDIT: Shout out to TheInvisibleSun who also posted a similar mod elsewhere. I made mention of their tweak at the bottom.

    Open up _incObj/09 Sonic in Special Stage.asm first, as most of our work is going to be done there. Find Obj09_Display and remove these lines:
    Code (Text):
    1. Obj09_Display:
    2.    bsr.w    Obj09_ChkItems_Nonsolid
    3.    bsr.w    Obj09_ChkItems_Solid
    4.    jsr    (SpeedToPos).l
    5.    bsr.w    SS_FixCamera
    6.  
    7.     move.w    (v_ssangle).w,d0      ; REMOVE
    8.     add.w    (v_ssrotate).w,d0        ; REMOVE
    9.     move.w    d0,(v_ssangle).w      ; REMOVE
    10.  
    11.    jsr    (Sonic_Animate).l
    12.    jsr    (Sonic_LoadGfx).l
    13.    jmp    (DisplaySprite).l
    These three lines were responsible for taking the rotation speed in v_ssrotate and applying it to the stage angle in v_ssangle, allowing the stage to rotate at a variable speed. Removing these gets rid of the auto-rotation. Now, we need to change the left/right controls so that they rotate the stage. Scroll down to Obj09_MoveLeft, and we are going to remove a TON of stuff, and replace it with instructions to change the Special Stage angle. Change the entire routine to this:
    Code (Text):
    1. Obj09_MoveLeft:
    2.    bset    #staFacing,obStatus(a0)
    3.  
    4.     move.w    (v_ssangle).w,d0
    5.     sub.w    (v_ssrotate).w,d0
    6.     move.w    d0,(v_ssangle).w
    7.  
    8.    rts
    9. ; End of function Obj09_MoveLeft
    We are going to do something similar with Obj09_MoveRight:
    Code (Text):
    1. Obj09_MoveLeft:
    2.    bset    #staFacing,obStatus(a0)
    3.  
    4.     move.w    (v_ssangle).w,d0
    5.     add.w    (v_ssrotate).w,d0
    6.     move.w    d0,(v_ssangle).w
    7.  
    8.    rts
    9. ; End of function Obj09_MoveLeft
    Finally, we are going to adjust the starting rotation speed, because the default rotation speed is far too slow for this new control scheme. Open _sonic.asm_ and go to GM_Special. Find this instruction.
    Code (Text):
    1. move.w    #$40,(v_ssrotate).w
    The rotation speed (much like an object's movement speed) consists of two bytes: an integer (upper) byte and a "fraction" (lower) byte. A speed of #$0040 means a speed of 0.25 per frame, because $40/$100 = 1/4 = 0.25. Let's up this to a whole 1.0 speed like so:
    Code (Text):
    1. move.w    #$100,(v_ssrotate).w
    You can set it to something else, if you feel it's necessary to do so. Keeping it as a variable allows you to change it in real time, if you have a need to do so. On that note, though, we should remove the UP/DOWN and R blocks, as those don't seem to complement the new controls very well.

    Courtesy of TheInvisibleSun, one more change has been made in their attempt a couple of years ago that removes jumping (apparently S4Ep1 doesn't let you jump? IDK I haven't played that game since 2011). In any case, if you want this, you can replace everything in Obj09_Jump with a simple rts.

    You can use S1SSEdit or SonEd2 to handle that, or change them in real-time in the stage loading subroutine.
     
    Last edited: Sep 18, 2024
  5. Hivebrain

    Hivebrain

    Administrator
    3,075
    211
    43
    53.4N, 1.5W
    Github
    I had the same idea, but I was worried about what would happen if priority isn't defined. Am I right in thinking DisplaySprite will see a1 as 0 in the ROM (which should be $00FF), and exit the routine without doing anything?
     
  6. Devon

    Devon

    Please do not contact me, overwhelmed with stuff Tech Member
    1,522
    1,878
    93
    your mom
    Yeah, if it's 0, then all that happens is that DisplaySprite will attempt to write the object slot address to ROM, which, of course, does nothing.
     
  7. MarkeyJester

    MarkeyJester

    You smash your heart against the rocks Resident Jester
    2,267
    529
    93
    Japan
    Every frame, instead of resetting these lists to 0, set them to $80. Then subtract instead of add to remove the cmpi.

    Code (Text):
    1. DisplaySprite:
    2.         movea.w    obPriority(a0),a1
    3.         subq.w    #2,(a1)
    4.         ble.s    DSpr_Full
    5.         adda.w    (a1),a1
    6.         move.w    a0,(a1)+
    7.  
    8. DSpr_Full:
    9.         rts
    It may cost 2 cycles to pre-decrement per object in BuildSprites, to read out the order in the correct direction; but you save 12 cycles per object here. It's debatable if the order matters when they're on the same priority, so you could shrug and ignore it to save those 2. It'll take $4040 objects calling DisplaySprite in the same frame, using the same priority, to break this method, so don't worry about wrapping below $8000.

    Another method which would be safer, but still save cycles is to store the address of the list to itself instead using it as a counter. That way a movea can be used instead of an adda, and it'll be 4 cycles faster. Though you'll need different comparison addresses for different priority lists, maybe several subroutines, one for each priority. Might be a bit extreme just for 4 cycles.

    Ultimately though, the ideal thing to do is to rid the entire table, store only the pointer to the first object, and use the obPriority as the pointer to the next object, eliminating the need to check for a limit, and avoiding this nasty series of address adds, something along the lines of:

    Code (Text):
    1.         lea    (List).w,a1
    2.         move.w    (a1),obPriority(a0)
    3.         move.w    a0,(a1)
    4.         rts
    Whereby perhaps the caller can lea the a1 list depending on the priority.

    Some engines save themselves to lists to be left in permanently until the object is deleted, and then remove themselves from the list, avoiding the need to keep adding every single frame.

    I think though we're entering a situation where the changes/fixes are becoming a bit anal, it's entering into a realm of pure obsession and maybe some self satisfactory, and far outreaching from making of a decent game itself, where the gameplay quality becomes nothing more than a back seat thing, in the persuit of performance.
     
    • Like Like x 5
    • Informative Informative x 1
    • List
  8. DeltaW

    DeltaW

    Originally a Wooloo Member
    Or in simpler terms, it's called premature optimization. :p

    I will agree and say that from what I've seen, and what I'm guilty of myself, Sonic ROM hackers tend to focus too much on backporting or optimizing engine changes than the game and flashing their new big changes as if it's a major milestone in their ROM hacks. While it is commendable to see new ways to make a ROM hack as optimized as possible, the general public would not care about the different engines as they want to... y'know play the damn game. It's a waste of development time if optimizations are being made for the sake of it without serving a real purpose. This can damage the reputation of your project if too much focus is placed on performance rather than gameplay. Taking precautionary steps to reduce lag is understandable, but such improvements should be addressed as needed, not become the main focus.

    Additionally, backporting code from Sonic 3K doesn’t automatically make your ROM hack fully optimized. While S3K did rewrite a lot of code to handle its larger scope in terms of layouts and design, it’s a mistake to assume that whatever works in S3K is the best way to minimize lag. There’s a lot more that can be done to optimize a game beyond just using S3K’s methods. With the right effort, such as learning more about both the Sonic engines and 68k programming, you can rewrite code to achieve far better optimization than simply relying on what S3K implemented.

    To counter that point, I believe if you're using those engine changes to expand the game, it’s a different situation. For example, in one of my projects, I wanted larger layouts to accommodate characters with different abilities, allowing them to explore various paths while keeping the levels linear and focused on speed. In this case, I backported the Sonic 3K level layout, which required porting the S3K object and rings manager as well. With help from a few friends, we optimized the code to allow a large number of objects to load regardless of my position in-game, without worrying about swapping or lagging due to the number of objects in the level. This might not be the most universally viable solution, but it works for me and does the job, so I’m more than happy to tweak the code whenever I have spare time.

    I’d say that as long as the optimizations genuinely benefit your project by contributing to its core vision, go for it! There are definitely pros and cons to focusing on optimizations over the heart of the project, but it only becomes a problem when the engine itself is highlighted as the main selling point rather than the overall scope and content of the game.
     
    Last edited: Sep 17, 2024
  9. MarkeyJester

    MarkeyJester

    You smash your heart against the rocks Resident Jester
    2,267
    529
    93
    Japan
    I don't think you understood what I meant. I was trying to bring light to the idea; freedom reduces creativity, and it was issued as a mild warning, not to be taken as absolutely fact or rule, nor was I suggesting anyone stop. I wasn't expecting a wall of justifying text in a panic to try and protect the flow.

    It's also a bad idea to use your own project as an example; especially if it hasn't been publically released, and you and your team are the only witnesses to it, and you assume your project is good which you'll naturally be biased towards (regardless of truth).
     
  10. DeltaW

    DeltaW

    Originally a Wooloo Member
    I understand your perspective, and I appreciate the clarification. I intended to offer insight into how thoughtful certain optimizations can improve a project, not to impose a strict rule. I didn't mean to suggest that anyone should stop their creative process or come across as defensive. I acknowledge that too much freedom, especially in terms of optimization options, can sometimes overwhelm and stifle creativity. However, I believe that a measured and deliberate approach to making changes can be highly beneficial if applied appropriately. Balancing creativity with strategic optimizations can lead to a more refined and effective project.

    As for using my project as an example, I understand your point. Referencing something not publicly available can indeed come off as biased, which I had no intent of and I’ll keep that in mind. My main reason for stating my project as an example is to help me share an experience that highlighted why certain optimizations were beneficial in that context, but I can see how that might not always translate well and again sound biased.
     
  11. Devon

    Devon

    Please do not contact me, overwhelmed with stuff Tech Member
    1,522
    1,878
    93
    your mom
    There's nothing wrong with wanting good performance out of your game, but there's gotta be a game that exists to even optimize :eng99:
     
    Last edited: Sep 18, 2024
  12. RetroKoH

    RetroKoH

    Member
    1,734
    112
    43
    S1Fixed: A successor to ReadySonic
    Having applied this myself, the only thing that seems to happen if you fail to define a priority address is that the object won't appear. Then again I may have been lucky in my testing... it very well could lead to a crash. Provided everything is defined, it works great
     
  13. Kilo

    Kilo

    Starting new projects every week Tech Member
    1,264
    1,198
    93
    Canada
    Changes with the weather
    So I was watching redhotsonic's newest video and sure enough, I did not know about the Scrap Brain Zone Act 3 platform that he talks about around 6:17. So let's quickly fix it. It's actually really simple. The problem is that this specific platform has it's remember state flag enabled... But the object doesn't actually have any code to handle remembering it's state. So once it goes offscreen it assumes it's dead and doesn't come back. Simply pop open SonLVL, select the platform, disable it's remember state, and ya done. I wouldn't worry too much about it respawning anyways because once the player goes past that point in the level it's not possible to backtrack to the platform.
     
  14. I've noticed that in SonLVL, inserting a fresh instance of this object but setting it to have the "stand on it and fall upon wall contact" property works too (likely due to these types of objects not having the remember state flag enabled by default, including that very same platform object).
     
  15. Devon

    Devon

    Please do not contact me, overwhelmed with stuff Tech Member
    1,522
    1,878
    93
    your mom
    I mentioned a bug with the bubbles that come out of Sonic's mouth when he drowns in this post. In summary, the larger bubbles that come out of his mouth, if they hit the water surface, then, due to the setup of the animations, it will end up with an invalid animation ID.
    Code (ASM):
    1. Drown_ChkWater: ; Routine 4
    2.               move.w  (v_waterpos1).w,d0
    3.               cmp.w   obY(a0),d0                      ; has bubble reached the water surface?
    4.               blo.s   .wobble                         ; if not, branch
    5.  
    6.               move.b  #id_Drown_Display,obRoutine(a0) ; goto Drown_Display next
    7.               addq.b  #7,obAnim(a0)
    8.               cmpi.b  #$D,obAnim(a0)
    9.               beq.s   Drown_Display
    10.               bra.s   Drown_Display

    [​IMG]

    [​IMG]

    Sonic 2 fixed this, so we can just port the fix from that.
    Code (ASM):
    1. Drown_ChkWater: ; Routine 4
    2.               move.w  (v_waterpos1).w,d0
    3.               cmp.w   obY(a0),d0                      ; has bubble reached the water surface?
    4.               blo.s   .wobble                         ; if not, branch
    5.  
    6.               move.b  #id_Drown_Display,obRoutine(a0) ; goto Drown_Display next
    7.               addq.b  #7,obAnim(a0)
    8.               cmpi.b  #$D,obAnim(a0)
    9.               bls.s   Drown_Display                   ; DEV: Combine the "beq" and "blo" into "bls"
    10.               move.b  #$D,obAnim(a0)                  ; DEV: Set to "pop animation"
    11.               bra.s   Drown_Display
     
  16. Kilo

    Kilo

    Starting new projects every week Tech Member
    1,264
    1,198
    93
    Canada
    Changes with the weather
    (Project 128 Hivebrain) - Optimize the Level Layout Index
    The GitHub P128 iteration has already implemented this fix, so users of that disassembly cannot use this fix. P128 still uses the vanilla level layout index, which it doesn't really need to. Vanilla S1 has an entry for the foreground layout, background layout, and an unused entry that was likely the "Z layout" used by TTS' advanced parallax (Or at least that's my theory anyways). But the Sonic 2 level format interlaces the foreground and background layouts together, so you only need to index the 1 layout file. In fact, you're wasting ROM space by leaving it as is because the original background layouts go unused. Let's fix it.
    First go to Level_Index and replace everything up until Art_BigRing with this:
    Code (Text):
    1. Level_Index:
    2.         dc.l Level_GHZ1
    3.         dc.l Level_GHZ2
    4.         dc.l Level_GHZ3
    5.         dc.l Layout_Null
    6.         dc.l Level_LZ1
    7.         dc.l Level_LZ2
    8.         dc.l Level_LZ3
    9.         dc.l Level_SBZ3
    10.         dc.l Level_MZ1
    11.         dc.l Level_MZ2
    12.         dc.l Level_MZ3
    13.         dc.l Layout_Null
    14.         dc.l Level_SLZ1
    15.         dc.l Level_SLZ2
    16.         dc.l Level_SLZ3
    17.         dc.l Layout_Null
    18.         dc.l Level_SYZ1
    19.         dc.l Level_SYZ2
    20.         dc.l Level_SYZ3
    21.         dc.l Layout_Null
    22.         dc.l Level_SBZ1
    23.         dc.l Level_SBZ2
    24.         dc.l Level_SBZ2
    25.         dc.l Layout_Null
    26.         dc.l Level_END
    27.         dc.l Level_END
    28. Layout_Null:    dc.l 0
    29. Level_GHZ1:    incbin    levels\ghz1.bin
    30.         even
    31. Level_GHZ2:    incbin    levels\ghz2.bin
    32.         even
    33. Level_GHZ3:    incbin    levels\ghz3.bin
    34.         even
    35. Level_LZ1:    incbin    levels\lz1.bin
    36.         even
    37. Level_LZ2:    incbin    levels\lz2.bin
    38.         even
    39. Level_LZ3:    incbin    levels\lz3.bin
    40.         even
    41. Level_LZ3_WALL:    incbin    levels\lz3_wall.bin    ; MJ: layout with LZ's wall change (When the switch is pressed) data is not in ram anymore,
    42.         even                ; and altering values in rom is prohibited, so a new layout is loaded in its place.
    43. Level_SBZ3:    incbin    levels\sbz3.bin
    44.         even
    45. Level_MZ1:    incbin    levels\mz1.bin
    46.         even
    47. Level_MZ2:    incbin    levels\mz2.bin
    48.         even
    49. Level_MZ3:    incbin    levels\mz3.bin
    50.         even
    51. Level_SLZ1:    incbin    levels\slz1.bin
    52.         even
    53. Level_SLZ2:    incbin    levels\slz2.bin
    54.         even
    55. Level_SLZ3:    incbin    levels\slz3.bin
    56.         even
    57. Level_SYZ1:    incbin    levels\syz1.bin
    58.         even
    59. Level_SYZ2:    incbin    levels\syz2.bin
    60.         even
    61. Level_SYZ3:    incbin    levels\syz3.bin
    62.         even
    63. Level_SBZ1:    incbin    levels\sbz1.bin
    64.         even
    65. Level_SBZ2:    incbin    levels\sbz2.bin
    66.         even
    67. Level_END:    incbin    levels\ending.bin
    68.         even
    69.  
    Then you're free to delete the background layout files from the levels folder.
    Next go to LevelLayoutLoad, and delete these 3 lines:
    Code (Text):
    1.         move.w    d0,d2
    2.         add.w    d0,d0
    3.         add.w    d2,d0
    And you're done.
    Edit: Realized that I had removed the ending layout because I was using this in my own project. Fixed that.
     
    Last edited: Nov 16, 2024
  17. Kilo

    Kilo

    Starting new projects every week Tech Member
    1,264
    1,198
    93
    Canada
    Changes with the weather
    More of an observation rather than an actual change, but I thought it was interesting. We've had this guide to port rev01's parallax into rev00 for a long time now. And while sifting through variables for my project, I noticed this section of code in LoadTilesAsYouMove:
    Code (Text):
    1.         tst.b    ($FFFFF745).w
    2.         beq.s    Draw_FG
    3.         move.b    #0,($FFFFF745).w    ;Reset the redraw flag
    4.         moveq    #-$10,d4            ;Go to top line oflevel data relative camera position
    5.         moveq    #$F,d6                ;
    6.  
    7. Draw_All:
    8.         movem.l    d4-d6,-(sp)            ; This whole routine basically redraws the whole
    9.         moveq    #-$10,d5            ; area instead of merely a line of tiles
    10.         move.w    d4,d1
    11.         bsr.w    Calc_VRAM_Pos
    12.         move.w    d1,d4
    13.         moveq    #-$10,d5
    14.         bsr.w    DrawTiles_LR
    15.         movem.l    (sp)+,d4-d6
    16.         addi.w    #$10,d4
    17.         dbf    d6,Draw_All
    18.         rts  
    19.  
    20. Draw_FG:
    This doesn't exist in rev01. So where'd it come from? Well, it's actually from Sonic 2. This code is used to quickly redraw the screen, and is used to unlock the CNZ boss area upon defeat, and to show the WFZ boss arena when you break it's lock thing.
    By default, when you use the guide, this ends up being dead code. So you can either delete it to eliminate an unnecessary check (which may cause problems in the first place if you unknowingly assigned $F745 for something else), or you can go ahead and actually use it for your project, by simply changing the chunks in RAM (Or changing the level layout address if you're reading it from ROM) and then setting $F745 when you need to do a redraw.

    And if you're a GitHub user, since the code there is actually from rev01, this isn't present, so you can copy it and place it under
    Code (Text):
    1. move.w    #$4000,d2            ; VRAM thing for selecting Plane A
    Should you desire the ability to quickly redraw the screen.
     
  18. As Sonic 1 Squared shows, it is possible to save another universal SST by getting rid of obPrevAnim in favour of using the high-bit of obAnim as a restart flag (this does effectively limit non-Sonic objects to $7F animations, but let's be honest, you will probably never reach close to that limit). This is considerably more complicated to implement given how many objects have broken animation, so the best I can do for now is to link the two commits where this change is applied. That being said, another free USST is another free USST.

    Part 1
    Part 2
     
  19. Hivebrain

    Hivebrain

    Administrator
    3,075
    211
    43
    53.4N, 1.5W
    Github
    The problem I encountered with that is some objects continually set their animation every frame, which means the animation restarts every frame and never actually animates. So those objects needed rewriting to be more sensible. More recently I've been increasing the RAM available to each object (currently at $48) instead of trying to squeeze more out the original $40. I extended width, height, displaywidth and priority to words. I also added separate words for hitbox width/height instead of pulling them from a table.
     
  20. Indeed, increasing the SST's size would be more optimal, however it is very, very challenging to do in older disassemblies (Hivebrain 2005 still remains widely used to how divisive the Retro GitHub disassembly) due to them not having proper constants/variable setup. Additionally, increasing the SST requires more object RAM, meaning RAM has to be saved elsewhere.

    I should mention I am attempting to pull this off in Sonic 2... I had to rewrite quite a bit of the monitor object (and some stuff is just crashing the game), but most things are working, just with occasionally broken animations. I'm guessing the programmers (mostly) realized that setting the animation every frame is just unoptimal for performance... that or I just haven't found all the issues yet.