Actions

SCHG How-to

Speed Up Ring Loss Process (With Underwater)

From Sonic Retro

Guide written by redhotsonic. Also, special thanks to SpirituInsanum for his guide at SSRG.

NOTE - This guide is written for Sonic 2, using Xenowhirl’s 2007 disassembly. For Sonic 1, and/or other disassemblies, it shouldn't be too hard still if you follow this guide. Also, before using this guide, you may want to try the following guide first: Underwater Physics Fix (Sonic 2)

Now, after following the above guide, your rings should behave properly underwater. Now to fix another problem. When losing rings (especially when underwater...) it can cause considerable lag. This guide will now walk you through removing this lag, and speeding up the ring loss process considerably.

Part 1

The following code below is part of the scattered rings code. This code repeats itself a maximum of 32 times; each time for each ring (As it will make you scatter a max of 32 rings, but still set your counter to 0). What this code does, is works out how high and how far to scatter a ring, then then whether to make it fly left or right. Once it's done that, it does it again for another ring, and another... up to 32. Whilst it's doing this, nothing else can be done. It's not much of a problem when there's not much about, but if you lose a lot of rings when there's so many other objects about, and/or while you are in the water, it can noticeably lag. Anyway, look below:

loc_120BA:
        _move.b #$37,0(a1) ; load obj37
        addq.b  #2,routine(a1)
        move.b  #8,y_radius(a1)
        move.b  #8,x_radius(a1)
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        move.l  #Obj25_MapUnc_12382,mappings(a1)
        move.w  #$26BC,art_tile(a1)
        bsr.w   Adjust2PArtPointer2
        move.b  #$84,render_flags(a1)
        move.b  #3,priority(a1)
        move.b  #$47,collision_flags(a1)
        move.b  #8,width_pixels(a1)
        move.b  #-1,(Ring_spill_anim_counter).w
        tst.w   d4
        bmi.s   loc_12132
        move.w  d4,d0
        bsr.w   JmpTo4_CalcSine
        move.w  d4,d2
        lsr.w   #8,d2
        asl.w   d2,d0
        asl.w   d2,d1
        move.w  d0,d2
        move.w  d1,d3
        addi.b  #$10,d4
        bcc.s   loc_12132
        subi.w  #$80,d4
        bcc.s   loc_12132
        move.w  #$288,d4

loc_12132:

        move.w  d2,x_vel(a1)
        move.w  d3,y_vel(a1)
        neg.w   d2
        neg.w   d4
        dbf     d5,loc_120B2

Anyway, This is quite big and slow, and now, let's show you how to reduce it significantly; speeding the ring loss process, thus freeing the processor for other work. Instead of doing lots of calculations on the fly over and over, we will use a pre-made table to insert how high, how far and which way we want our rings to go. This will be a lot quicker as we already have the calculations done this way. BUT how do we come up with our table? Well, I'm about to show you. But first, in this guide, I will be using two pre-made tables myself. These are for a max of 20 rings above water, and 8 rings below water. Also, the ring spill underwater will be using the effect from my other guide. If you're happy to do the same as me, you may skip ahead to step 4 (although you might want to read the first couple of steps so you know what's going on).

Step 1

(Change the max amount of rings to spill)

In the scattered rings object, go to "loc_120A2:" and the first line you should see is this:

        moveq   #$20,d0

This is hexadecimal for 32, this is the max number of rings it will spill. Take the $ sign out (so it's not hexadecimal anymore), and then change the number to the amount of rings you want it to spill at maximum. If you want it to lose a max of 44 rings for example, then simply change the 20 to 44. If you want to change the number of max rings spill for when underwater (as things get slower underwater) we can add a check to see if underwater and if so, to change the number. Say you want to lose 20 rings over water but only 8 when in the water, you would do this:

loc_120A2:
        moveq   #20,d0                  ; lose a max of 20 rings
        lea     (MainCharacter).w,a2    ; a2=character
        btst    #6,status(a2)           ; is Sonic underwater?
        beq.s   +                       ; if not, branch
        moveq   #8,d0                   ; lose a max of 8 rings when underwater
+
        cmp.w   d0,d5
        bcs.s   loc_120AA
        move.w  d0,d5

Make sure MainCharacter is loaded to a2, otherwise it may interrupt with the rest of our work or the original code.

You DON'T have to change the max for underwater, but it is advisable, because things get very slow underwater. The more rings there are, the slower things will get, because more processing power goes in to making them bounce and etc. That's purely the only reason.

Step 2

(Make our tables for pre-calculated figures)

Go to "loc_120AA:" and change this:

loc_120AA:
        subq.w  #1,d5
        move.w  #$288,d4
        bra.s   loc_120BA

to this:

loc_120AA:
        subq.w  #1,d5
        move.w  #$288,d4
        lea ($FFFFAA00).l,a4    ; Load $FFFFAA00 to a4
        bra.s   loc_120BA

$FFFFAA00 is Nemesis' decompression buffer's RAM. It's not used during gameplay, so we can use this. All we've done is copied the RAM address to a4. Then go to "loc_12132:" and change this:

loc_12132:
        move.w  d2,x_vel(a1)
        move.w  d3,y_vel(a1)
        neg.w   d2
        neg.w   d4
        dbf     d5,loc_120B2

to this:

loc_12132:
        move.w  d2,x_vel(a1)
        move.w  d3,y_vel(a1)
        neg.w   d2
        neg.w   d4
        move.w  d2,(a4)+        ; Move d2 to a4 then increment a4 by a word
        move.w  d3,(a4)+        ; Move d3 to a4 then increment a4 by a word
        dbf     d5,loc_120B2

Just added two lines here. Basically, once one ring has calculated it's x_vel and y_vel when it's going to spill out, it's copied it's values to a4, then a4 has incremented so that the next value can be added. The code repeats itself for the next ring and again, once it's got it's calculations, it's then copied to a4, and keeps doing this for all rings spilled.

Step 3

(Build and test. Let's get our new table!)

Save, build and test. Should be no errors. Now, using your emulator, go to the RAM viewer and go to $FFFFAA00. Load any level up. During level loading, the numbers at this RAM address will go crazy, but once the title cards have gone away, it should stop and not do anything (there will still be numbers there).

Now, collect the max number of rings (or more) and then get hurt and lose your rings. They'll scatter everywhere as usual, but all it's values has just been moved to $FFFFAA00. Ta-dah! There's our new table! What you need to do now is open notepad and write your new values in using words. Give it a label too! Here is a guide on how to do it (mine is a max of 20 rings):

; ===========================================================================
; ---------------------------------------------------------------------------
; Ring Spawn Array
; ---------------------------------------------------------------------------

SpillRingData:  dc.w    $00C4,$FC14, $FF3C,$FC14, $0238,$FCB0, $FDC8,$FCB0 ; 4
                dc.w    $0350,$FDC8, $FCB0,$FDC8, $03EC,$FF3C, $FC14,$FF3C ; 8
                dc.w    $03EC,$00C4, $FC14,$00C4, $0350,$0238, $FCB0,$0238 ; 12
                dc.w    $0238,$0350, $FDC8,$0350, $00C4,$03EC, $FF3C,$03EC ; 16
                dc.w    $0062,$FE0A, $FF9E,$FE0A, $011C,$FE58, $FEE4,$FE58 ; 20
                even
; ===========================================================================

It goes x_vel, y_vel, x_vel, y_vel, x_vel, y_vel, etc...


REMEMBER, if you're doing less rings for underwater, you need to get the max number of rings again, go underwater then get hurt. You'll get another new table. For my underwater, the max is 8. And again, I'm using the underwater scattered rings guide. Here's a guide for 8 rings.

; ===========================================================================
; ---------------------------------------------------------------------------
; Ring Spawn Array Underwater
; ---------------------------------------------------------------------------

SpillRingDataU: dc.w    $0064,$FE08, $FF9C,$FE08, $011C,$FE58, $FEE4,$FE58 ; 4
                dc.w    $01A8,$FEE4, $FE58,$FEE4, $01F8,$FF9C, $FE08,$FF9C ; 8
                even
; ===========================================================================
; ===========================================================================

Notice I've changed the label for the underwater table. I've just stuck a U after it. You will need to insert these (or your own, whatever) into your ASM file. A good place to put it, find "BranchTo5_DeleteObject" and stick it after

BranchTo5_DeleteObject 
        bra.w   DeleteObject

Step 4

(Loading our new tables.)

Now that we've got our new tables, let's use them. Go back to "loc_120A2:". This is where we will make it load our new tables. The tables will load into a3. Change it to make it look like this:

loc_120A2:
        lea     SpillRingData,a3        ; load the address of the array in a3
        moveq   #20,d0                  ; lose a max of 20 rings
        lea     (MainCharacter).w,a2    ; a2=character
        btst    #6,status(a2)           ; is Sonic underwater?
        beq.s   +                       ; if not, branch
        lea    SpillRingDataU,a3        ; load the UNDERWATER address of the array in a3
        moveq   #8,d0                   ; lose a max of 8 rings underwater
+
        cmp.w   d0,d5
        bcs.s   loc_120AA
        move.w  d0,d5

Basically, it will load the above water table and make 20 the max rings. It then checks if your underwater and if so, load the underwater table and change the max to 8. If not underwater, it will skip doing that bit and carry on with normal. REMEMBER to change the label and max rings to whatever you gave it. It only does this once every time you lose rings, it doesn't repeat itself unlike the calculations below it.

Now go to "loc_120AA:" and change this:

loc_120AA:
        subq.w  #1,d5
        move.w  #$288,d4
        lea ($FFFFAA00).l,a4    ; Load $FFFFAA00 to a4
        bra.s   loc_120BA

to this:

loc_120AA:
        subq.w  #1,d5
        bra.s   loc_120BA

The Nemesis RAM bit isn't needed anymore; that was just for creating our tables. The "move.w #$288,d4" isn't needed anymore either as that's part of the calculations of spilling the rings.

Step 5

(Using the data from our new tables)

Go to "loc_120BA:"

loc_120BA:
        _move.b #$37,0(a1) ; load obj37
        addq.b  #2,routine(a1)
        move.b  #8,y_radius(a1)
        move.b  #8,x_radius(a1)
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        move.l  #Obj25_MapUnc_12382,mappings(a1)
        move.w  #$26BC,art_tile(a1)
        bsr.w   Adjust2PArtPointer2
        move.b  #$84,render_flags(a1)
        move.b  #3,priority(a1)
        move.b  #$47,collision_flags(a1)
        move.b  #8,width_pixels(a1)
        move.b  #-1,(Ring_spill_anim_counter).w
        tst.w   d4              ; DELETE ME
        bmi.s   loc_12132       ; DELETE ME
        move.w  d4,d0           ; DELETE ME
        bsr.w   JmpTo4_CalcSine ; DELETE ME
        move.w  d4,d2           ; DELETE ME
        lsr.w   #8,d2           ; DELETE ME
        asl.w   d2,d0           ; DELETE ME
        asl.w   d2,d1           ; DELETE ME
        move.w  d0,d2           ; DELETE ME
        move.w  d1,d3           ; DELETE ME
        addi.b  #$10,d4         ; DELETE ME
        bcc.s   loc_12132       ; DELETE ME
        subi.w  #$80,d4         ; DELETE ME
        bcc.s   loc_12132       ; DELETE ME
        move.w  #$288,d4        ; DELETE ME

loc_12132:                      ; DELETE ME
        move.w  d2,x_vel(a1)    ; DELETE ME
        move.w  d3,y_vel(a1)    ; DELETE ME
        neg.w   d2              ; DELETE ME
        neg.w   d4              ; DELETE ME
        move.w  d2,(a4)+        ; DELETE ME     ; Move d2 to a4 then increment a4 by a word
        move.w  d3,(a4)+        ; DELETE ME     ; Move d3 to a4 then increment a4 by a word
        dbf     d5,loc_120B2

See where it says "DELETE ME"? Do what it says. These are not needed anymore, in fact, they slow the ring loss down! This calculates the ring loss speeds and etc. Instead, we using our tables. Now, where you just deleted that table, insert this instead:

      move.w  (a3)+,x_vel(a1)         ; move the data contained in the array to the x velocity and increment the address in a3
      move.w  (a3)+,y_vel(a1)         ; move the data contained in the array to the y velocity and increment the address in a3

So, you have something looking like this:

loc_120BA:
        _move.b #$37,0(a1) ; load obj37
        addq.b  #2,routine(a1)
        move.b  #8,y_radius(a1)
        move.b  #8,x_radius(a1)
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        move.l  #Obj25_MapUnc_12382,mappings(a1)
        move.w  #$26BC,art_tile(a1)
        bsr.w   Adjust2PArtPointer2     ; This is only needed for two player
        move.b  #$84,render_flags(a1)
        move.b  #3,priority(a1)
        move.b  #$47,collision_flags(a1)
        move.b  #8,width_pixels(a1)
        move.b  #-1,(Ring_spill_anim_counter).w
        move.w  (a3)+,x_vel(a1)         ; move the data contained in the array to the x velocity and increment the address in a3
        move.w  (a3)+,y_vel(a1)         ; move the data contained in the array to the y velocity and increment the address in a3
        dbf     d5,loc_120B2

ALL DONE! Phew! Let me explain though. What it does now, is load a word from a3 (the table) and inserts it into x_vel, and then increment a3 by a word. Then it loads a word from a3 again (which has been incremented, so it won't be the same word) and inserts it into y_vel, then increment a3 again. Now one ring has it's speeds. It now knows how high to jump, how far to jump and which way to go, a hell of a lot quicker then doing all them calculations! If you have used my tables I've supplied, then when out of water, you can only lose a max of 20 rings, and when in water, you can only lose a max of 8 rings, and it will act like they're in water. Feel free to change these both back to 32... or to whatever you like.

The ring loss process will now be much faster! And if in water, it shouldn't slow down (as much). To continue speeding up the process EVEN MORE, continue to Part 2, below.

Part 2

Ok, so I ported the S3&K priority manager into my hack [Sonic 2 Recreation], and you can make the scattered rings object do something extremely similar. So this is only making one object do it, but because of the amount of rings you can lose, this actually makes a BIG difference (as usual, especially underwater).

Go to "loc_120BA:" (part of the scattered rings object) and delete this line:

        move.b  #3,priority(a1)

When the rings are being created, it won't have to move 3 to it's priority anymore, saving a command per ring created and slightly speeding it up (you'll probably won't notice any difference, but this isn't what I am trying to accomplish here).

Now you're probably thinking "WTF! The rings aren't going to be displayed now!" Well, in a way, you're wrong. It will still be displayed, but with a priority of 0. Now, we do not want that, we still want it to be 3, and we're about to fix it, and this will be what speeds it up dramatically.

Go to "loc_121B8" and you should see a command:

        bra.w   DisplaySprite

We are going to replace this with a new code, similar to how S3K does it. In S2, before it can display the sprite, it converts the object's priority into a word, and then displays it. It does calculations that are about 3 lines long to convert it into a word. When lots of objects use this code every single frame (Sonic and Tails constantly for example), it can be a slow process.

Now, imagine you just got hurt and lost 32 rings, each one of them 32 rings branches to DisplaySprite, does the calculations, then displays the sprite; every single frame! All 32 of them! This slows it down quite a bit! Now, you can't just turn the scattered ring's priority into a word, otherwise it will over-write the scattered rings's "width_pixel". So, what do we do? Well, we can just copy part of the DisplaySprite's coding and insert it into the scattered rings' object coding.

Now, normally, after the rings have jumped to DisplaySprite to convert it's priority into a word, it becomes $180. Look at this:

Priority = 3 (byte)

DisplaySprite:
        lea     (Sprite_Table_Input).w,a1
        move.w  priority(a0),d0                 ; To be deleted
        lsr.w   #1,d0                           ; To be deleted
        andi.w  #$380,d0                        ; To be deleted
        adda.w  d0,a1                           ; To be changed
        cmpi.w  #$7E,(a1)
        bcc.s   return_16510
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)

return_16510:
        rts

Now, Priority = $180 (word)

So, we're going to copy this code, then edit it a bit, then move it to the scattered rings object. So copy it, then move it to where the branch was in the scattered rings object. So you have something looking like this:

WAS:

loc_121B8:

        tst.b   (Ring_spill_anim_counter).w
        beq.s   BranchTo5_DeleteObject
        move.w  (Camera_Max_Y_pos_now).w,d0
        addi.w  #$E0,d0
        cmp.w   y_pos(a0),d0
        bcs.s   BranchTo5_DeleteObject
        bra.w   DisplaySprite

And change that to this:

loc_121B8:

        tst.b   (Ring_spill_anim_counter).w
        beq.s   BranchTo5_DeleteObject
        move.w  (Camera_Max_Y_pos_now).w,d0
        addi.w  #$E0,d0
        cmp.w   y_pos(a0),d0
        bcs.s   BranchTo5_DeleteObject
        lea     (Sprite_Table_Input).w,a1       ; To be changed
        move.w  priority(a0),d0                 ; To be deleted
        lsr.w   #1,d0                           ; To be deleted
        andi.w  #$380,d0                        ; To be deleted
        adda.w  d0,a1                           ; To be deleted
        cmpi.w  #$7E,(a1)
        bcc.s   +
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)
+
        rts

"To be changed" means that line is going to be edited and the "To be deleted", well, guess what we will do with that? =P

Change

        lea     (Sprite_Table_Input).w,a1       ; To be changed

To this:

       lea     Sprite_Table_Input+$180,a1

As we now know 3 would equal $180, we can just add straight to a1. No more calculations every single frame, we've already done it! You should have something like this now:

loc_121B8:

        tst.b   (Ring_spill_anim_counter).w
        beq.s   BranchTo5_DeleteObject
        move.w  (Camera_Max_Y_pos_now).w,d0
        addi.w  #$E0,d0
        cmp.w   y_pos(a0),d0
        bcs.s   BranchTo5_DeleteObject
        lea     Sprite_Table_Input+$180,a1
        cmpi.w  #$7E,(a1)
        bcc.s   +
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)
+
        rts

So, what have we done? Well, we saved a line of it branching somewhere for a start, it's already there, so that's a plus! Also, we're moving $180 straight to a1, rather than doing them 3 lines of calculations! What a time saver! ALL done. That was easy, eh? Now your scattered rings will be even quicker, hopefully not slowing anything else down!

Part 2.5 (optional)

This is optional; you do not have to do it if you do not want to.

If you look at the code you just edited:

Obj_37_sub_4:
        addq.b  #2,routine(a0)
        move.b  #0,collision_flags(a0)
        move.b  #1,priority(a0)
        bsr.w   sub_11FC2

Obj_37_sub_6:
        lea     (byte_1237A).l,a1
        bsr.w   AnimateSprite
        bra.w   DisplaySprite
; ===========================================================================

BranchTo5_DeleteObject 
        bra.w   DeleteObject

You can see a command move 1 to priority, and displaying it again. This is for when you collect the rings (when the rings turn into them sparkly effects). You can do something extremely similar here if you like. It will make quite a bit of a difference if you collect a lot of scattered rings at the same time, otherwise, it won't do too much. If you want it to do the same, then you can. But it won't be $180 again! When 1 has been through DisplaySprite's calculations, it will equal $80 instead!

So, at "Obj_37_sub_4:", delete this line/command:

        move.b  #1,priority(a0)

Then, at "Obj_37_sub_6:", replace:

        bra.w   DisplaySprite

with this:

        lea     (Sprite_Table_Input).w,a1
        adda.w  #$80,a1
        cmpi.w  #$7E,(a1)
        bcc.s   +
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)
+
        rts

So you have something like this:

Obj_37_sub_4:
        addq.b  #2,routine(a0)
        move.b  #0,collision_flags(a0)
        bsr.w   sub_11FC2

Obj_37_sub_6:
        lea     (byte_1237A).l,a1
        bsr.w   AnimateSprite
        lea     (Sprite_Table_Input).w,a1
        adda.w  #$80,a1
        cmpi.w  #$7E,(a1)
        bcc.s   +
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)
+
        rts

Done: you've made it slightly faster again!

SCHG How-To Guide: Sonic the Hedgehog 2 (16-bit)
Fixing Bugs
Fix Demo Playback | Fix a Race Condition with Pattern Load Cues | Fix Super Sonic Bugs | Use Correct Height When Roll Jumping | Fix Jump Height Bug When Exiting Water | Fix Spin Dash Code and Add Spin Dash Speeds | Fix Screen Boundary Spin Dash Bug | Correct Drowning Bugs | Fix Camera Y Position for Tails | Fix Tails Subanimation Error | Fix Tails' Respawn Speeds | Fix Accidental Deletion of Scattered Rings | Fix Ring Timers | Fix Rexon Crash | Fix Monitor Collision Bug | Fix EHZ Deformation Bug | Correct CPZ Boss Attack Behavior | Fix Bug in ARZ Boss Arrow's Platform Behavior | Fix ARZ Boss Walking on Air Glitch | Fix ARZ Boss Sprite Behavior | Fix Multiple CNZ Boss Bugs | Fix HTZ Background Scrolling Mountains | Fix OOZ Launcher Speed Up Glitch | Fix DEZ Giant Mech Collision Glitch | Fix Boss Deconstruction Behavior | Fix Speed Bugs
Design Choices
Remove the Air Speed Cap | Disable Floor Collision While Dying | Modify Super Sonic Transformation Methods & Behavior | Enable/Disable Tails in Certain Levels | Collide with Water After Being Hurt | Retain Rings When Returning at a Star Post | Improve the Fade In\Fade Out Progression Routines | Fix Scattered Rings' Underwater Physics | Insert LZ Water Ripple Effect | Restore Lost CPZ Boss Feature | Prevent SCZ Tornado Spin Dash Death | Improve ObjectMove Subroutines | Port S3K Rings Manager | Port S3K Object Manager | Port S3K Priority Manager | Edit Level Order with ASM‎ | Alter Ring Requirements in Special Stages | Make Special Stage Characters Use Normal DPLCs | Speed Up Ring Loss Process
Adding Features
Create Insta-kill and High Jump Monitors | Create Clone and Special Stage Monitors | Port Knuckles
Sound Features
Port Sonic 1 Sound Driver | Port Sonic 2 Clone Driver | Port Sonic 3 Sound Driver | Expand the Music Index to Start at $00 (Sonic 2 Clone Driver Version)
Extending the Game
Extend the Level Index Past $10 | Extend the Level Select | Extend Water Tables | Add Extra Characters | Free Up 2 Universal SSTs