Mini-tutorials Thread - MDDC Edition

Inferno

Blazing Creator
Member
Messages
19
Location
Sky Base Zone, South Island
So, I figured I'd kickstart this thread over here, as a place for smaller tutorials that don't really deserve their own posts, mostly just to save thread space. Credit goes to DeltaWooloo for the original thread over on SSRG.


So, let's start with a simple tutorial of mine: changing basic level order in S3&K.

So, S3&K, instead of using an array of level order, does something different, mostly due to how it handles stuff in general.
What it does is call StartNewLevel:
Code:
StartNewLevel:
        move.w    d0,(Current_zone_and_act).w
        move.w    d0,(Apparent_zone_and_act).w
        move.w    #1,(Restart_level_flag).w
        clr.b    (Last_star_post_hit).w
        clr.b    (Special_bonus_entry_flag).w
        rts

As you can see, StartNewLevel takes an input of d0 for what level it sets up. Now, let's show an example of a basic use of this routine:
Code:
        move.w    #$A01,d0
        jmp    (StartNewLevel).l

This is the general format all branches to StartNewLevel follow: moving the level ID to d0 and then branching.

Now, let's do a general example: Restoring the original level order.
(This example will only work in S3K, for obvious reasons.)

In loc_6E80C, we find the branch to ICZ during the CNZ cannon cutscene:
Code:
        move.w    #$500,d0
        jsr    (StartNewLevel).l
        jmp    (Go_Delete_Sprite_2).l

Simply change #$500 to #$400.

But now we'll just skip to SOZ after CNZ & FBZ! And MHZ still goes to FBZ!

We need to change other branches.

loc_7645E is where MHZ to FBZ takes place:
Code:
loc_7645E:
        move.w    #$400,d0
        jsr    (StartNewLevel).l
        jmp    (Delete_Current_Sprite).l

So, change the value to SOZ: #$800.

And now, to make FBZ go to ICZ.
loc_70938 holds the FBZ to SOZ piece:
Code:
loc_70938:
        move.w    #$800,d0
        jsr    (StartNewLevel).l
        jmp    (Delete_Current_Sprite).l
Change #$800 to #$500.

And with that, Sonic and Tails play through FBZ after CNZ and Knuckles skips it!

Hopefully that's helpful, and let's see how this iteration of the thread goes.
 
I knew someone would eventually make a version inspired by my version at some point. Thanks, man. =P

Now for my little tutorial: Gaining extra lives when gaining points in Sonic 1.

To briefly summarise: if you get points within a multiple of 50000 points, you will be awarded a life a la Sonic 1 REV01 JPN and Sonic 2 later. To get this working in Sonic 1, let's take a look at the AddPoints subroutine:

Code:
AddPoints:
        move.b    #1,($FFFFFE1F).w ; set score counter to    update
        lea    ($FFFFFFC0).w,a2
        lea    ($FFFFFE26).w,a3
        add.l    d0,(a3)        ; add d0*10 to the score
        move.l    #999999,d1
        cmp.l    (a3),d1        ; is #999999 higher than the score?
        bhi.w    loc_1C6AC    ; if yes, branch
        move.l    d1,(a3)        ; reset    score to #999999
        move.l    d1,(a2)

loc_1C6AC:
        move.l    (a3),d0
        cmp.l    (a2),d0
        bcs.w    locret_1C6B6
        move.l    d0,(a2)

What this is doing is it collects points along the way you score it in multiple ways. We need to focus on RAM $FFFFFFC0 as that isn't usually anywhere and is considered a dummy RAM... or is that so? Replace the whole subroutine with this:

Code:
AddPoints:                      ; ...
        move.b    #1,($FFFFFE1F).w
        lea    ($FFFFFE26).w,a3
        add.l    d0,(a3)
        move.l    #999999,d1
        cmp.l    (a3),d1
        bhi.s    loc_1C6AC
        move.l    d1,(a3)

loc_1C6AC:                      ; ...
        move.l    (a3),d0
        cmp.l    ($FFFFFFC0).w,d0
        blo.s    locret_1C6B6
        add.l    #$1388,($FFFFFFC0).w
        jmp        (ExtraLife).l

locret_1C6B6:
     rts
; End of function AddPoints

To walk you through this, let me walk you through this line:

Code:
#$1388,($FFFFFFC0).w

This makes the dummy RAM byte useable by checking if Sonic has 50000 points (it's $1388 since that's 50000 in hexadecimal), and from there, it branches to ExtraLife giving Sonic an extra life. But, we're not done there as we need to initialize that feature during levels. Go to PlayLevel, LevSel_Level_SS, Demo_Level and Cont_GotoLevel, place this before the rts (for PlayLevel, do it before the lines where it fades the music out)

Code:
        move.l  #$1388,($FFFFFFC0).w    ; Initialize score limit for 1-up (50000)

And you should get it up and running successfully. Until then!

Edit: it's 50000 not 5000
 
Last edited:
So I decided to bring over two easy ASM tweaks to MDDC, so here we go.

CD Esque Explosion Animations

This is another one of those insanely easy tweaks for animations when you know what you're doing. In this case, we're gonna make a slight adjustment to the explosion animations (monitors, badniks, bosses) in Sonic 1. Specifically, we're adjusting the speed to resemble how they animated in Sonic CD.

HIVEBRAIN:

Head over to Obj27_Animate in sonic1.asm. This is due to how there aren't anim asm files for the explosions. The first line of code is this:

Code:
subq.b #1,$1E(a0) ; subtract 1 from frame duration

Just replace 1 with 2 and you're set.

GITHUB:

Head over to the "24, 27 & 3F Explosions.asm" file in the _incObj folder. Scroll down until you reach ExItem_Animate. You will see this as the first line of code in this section.
Code:
subq.b #1,obTimeFrame(a0) ; subtract 1 from frame duration
Just replace 1 with 2 and you're good to go. No credit required.

Disabling the Sparkles from the Signpost

Want to give the signposts in your Sonic hacks a little CD esque flavor? For those who prefer when there's no sparkles surrounding the signpost when you twirl it, here's how you do it. It does not require erasing the sprites for the ring sparkles or any complicated shit like that, it's an insanely easy ASM tweak.
Head down to Obj0D_Sparkle in sonic1.asm and you'll see these two lines of code:
Code:
move.b #$25,0(a1) ; load rings object
move.b    #6,$24(a1)    ; jump to ring sparkle subroutine
For GitHub disassembly users, take out these two lines of code in "0D Signpost.asm" which is in the _incObj folder:
Code:
move.b #id_Rings,0(a1) ; load rings object
move.b    #id_Ring_Sparkle,obRoutine(a1) ; jump to ring sparkle subroutine
All you have to do is remove these lines. Just that simple. No credit is required at all due to how easy this is.
 
Disabling the Sparkles from the Signpost

Want to give the signposts in your Sonic hacks a little CD esque flavor? For those who prefer when there's no sparkles surrounding the signpost when you twirl it, here's how you do it. It does not require erasing the sprites for the ring sparkles or any complicated shit like that, it's an insanely easy ASM tweak.
Head down to Obj0D_Sparkle in sonic1.asm and you'll see these two lines of code:
Code:
move.b #$25,0(a1) ; load rings object
move.b    #6,$24(a1)    ; jump to ring sparkle subroutine
For GitHub disassembly users, take out these two lines of code in "0D Signpost.asm" which is in the _incObj folder:
Code:
move.b #id_Rings,0(a1) ; load rings object
move.b    #id_Ring_Sparkle,obRoutine(a1) ; jump to ring sparkle subroutine
All you have to do is remove these lines. Just that simple. No credit is required at all due to how easy this is.
While this does remove the sparkles loading the whole routine in of itself is still trying to apply some properties to the sparkle object. It finds a free slot in object RAM and is trying to calculate the positions for it. If you would like to properly remove it then you should just make the routine into an rts.
 
Figured I'd bring this here (since the formatting on Gardeguey/GenesisFan64's original post on Retro is horribly broken):

Quick Palette Load (PalLoad_Loop Subroutine)

Several years ago, I found a very flexible method to changing palettes on the fly, courtesy of Gardeguey/GenesisFan64.

Place this new subroutine right after PalLoad2:


Code:
; ||||||||||||||| S U B    R O U T    I N E |||||||||||||||||||||||||||||||||||||||


PalLoad_Loop: ;Quick load
 
        move.w    (a1)+,(a2)+              ;Copy palette data to RAM
        dbf    d0,PalLoad_Loop               ;Loop d0 (length)
        rts

and then call for it like this, in your code [edit: anywhere you'd like to use the routine]:

Code:
move.w #$F,d0                ;Length ($F = full line)
lea    (Pal_Sonic),a1  ;Palette location - reference the label name of the palette under Palette Data here
lea    ($FFFFFB00),a2        ;RAM location ($FB00 = line 1,$FA80 underwater)
jsr    PalLoad_Loop

This allows you to almost fully control the palette change (without dealing with the pointers), by dictating how many colors to change (in the first line), and where the change starts by changing the RAM location (meaning, you could specifically change Sonic's blues if you wanted to). Each color represents $2 in ram, so each line of 16 colors accounts for $20. If you want the change to start on the second palette line for example, then you would load the given palette to '$FFFFFB20'.

Enjoy!
 
Last edited:
How to Remove the Points Object in Sonic 1 (Hivebrain 2005)

If you hate the occasional instance of the points art fucking up some aspects of your Sonic art, or need to save some VRAM, removing the points art/object may be the way to go. Here's how you can do it! This is based off a finding by Cinossu.

1. Head to sonic1.asm and you'll need to find this line of code:
Code:
        move.b    #$29,0(a1)    ; load points object
There are three traces of this line of code throughout the asm. They are in loc_90C0, Obj47_Score, and Obj51_Smash. You've gotta delete those.

2. Find Nem_Points in the same file, which is the incbin for the art.
Code:
Nem_Points:    incbin    artnem\points.bin    ; points from destroyed enemy or object
        even
Delete it. Now the game will refuse to build due to it missing from the game but requiring the pattern load cues present. We need to take care of the pointer in another file.

3. Head to the Pattern load cues.asm in the _inc folder, which has Nem_Points as the last part of Standard block 1.
Code:
        dc.l Nem_Points        ; points from enemy
        dc.w $F2E0
You think that simply deleting it will work, right? Wrong! Do that and it crashes right at the title screen when built. So what you need to do is change the dc.w digit from 4 to 3, lowering the amount of objects that need to be loaded, also making sure correct amounts of data are read.

4. One last thing! Head to the Object pointers.asm and you'll see Obj29 like this:
Code:
    dc.l Obj29, Obj2A, Obj2B, Obj2C
Replace the singular instance with DeleteObject.

Build the game and you should be able to dish out broken badniks and various level-specific objects, and still earn the points even with the art for it gone. You also have some extra free tiles in VRAM when you do it, that's always nice.

How to Remove the Points Object in Sonic 1 (Github and Hivebrain 2021)

1. You'll need to find this line of code across three ASM files in the Objects/_incObj folder:
Code:
        move.b    #id_Points,0(a1) ; load points object
This needs to be deleted in SYZ Bumper, MZ Smashable Green Block, Animals, .

2. Find Nem_Points in the same file, which is the incbin for the art.
Code:
        nemfile    Nem_Points
Delete it. Now the game will refuse to build due to it missing from the game but requiring the pattern load cues present. We need to take care of the pointer in another file.

3. Head to the Pattern load cues.asm in the disassembly's root folder, which has Nem_Points as the last part of Standard block 1.
Code:
        plcm    Nem_Points, $F2E0    ; points from enemy
What you need to do is delete the entry, which will automatically lower the amount of objects that need to be loaded while also making sure correct amounts of data are read.

4. Head to the Execute Objects and Object Pointers.asm and you'll see this:
Code:
        ptr Points
Replace the Points with DeleteObject.

5. One last thing! Delete the include code for Points.asm in sonic.asm.

Build the game and you should be able to dish out broken badniks and various level-specific objects, and still earn the points even with the art for it gone. You also have some extra free tiles in VRAM when you do it, that's always nice.

Credit should go to Cinossu for his input on simply disabling it, I prefer it considering this is merely a goodwill tutorial meant to help newcomers who think the points object/art is rather pesky. Thanks!

EDIT: 12/13/2021 - All steps (except for 2) have been adjusted following criticism regarding RAM issues from vladikcomper.
EDIT: 12/20/2021 - Github/HB2021 section added for maximum compatiblity.
 
Last edited:
So can't believe this wasn't noticed before but I only saw it because I thought I broke it before checking S2.

Chemical Plant's Pause Water is broken, it doesn't work at all.
See here is Rev 2:
nGm9LaE.png

Game is paused, I checked Rev 1 also.

The reason is actually surprisingly simple:
Code:
; ===========================================================================
; loc_20930:
Obj_WaterSurface_Action:
    move.w    (Water_Level_1).w,d1
    move.w    d1,y_pos(a0)
    tst.b    objoff_32(a0)
    bne.s    Obj_WaterSurface_Animate
    btst    #button_start,(Ctrl_1_Press).w    ; is Start button pressed?
    beq.s    loc_20962    ; if not, branch
    addq.b    #3,mapping_frame(a0)    ; use different frames
    move.b    #1,objoff_32(a0)    ; stop animation
    bra.s    loc_20962 ; <-- This is branching into the normal animation code Change to BranchTo_JmpTo10_DisplaySprite
; ===========================================================================
; loc_20952:
Obj_WaterSurface_Animate:
    tst.w    (Game_paused).w    ; is the game paused?
    bne.s    loc_20962        ; if yes, branch  <-- This is branching into the normal animation code also change to BranchTo_JmpTo10_DisplaySprite
    move.b    #0,objoff_32(a0)    ; resume animation
    subq.b    #3,mapping_frame(a0)    ; use normal frames

loc_20962:
    lea    (Anim_Obj_WaterSurface).l,a1
    moveq    #0,d1
    move.b    anim_frame(a0),d1
    move.b    (a1,d1.w),mapping_frame(a0)
    addq.b    #1,anim_frame(a0)
    andi.b    #$3F,anim_frame(a0)
    jmpto    (DisplaySprite).l, JmpTo10_DisplaySprite

The issue is that the code is branching to loc_20962 even though it should really be branching to "BranchTo_JmpTo10_DisplaySprite" instead causing the pause frames in Chemical Plant (and Hidden Palace Zone) to go completely unused. Figured this out by comparing Obj_WaterSurface_Action to Obj_WaterSurface_Action2 where it does work as intended.


Also as a bonus here's how to fix the misaligned pause sprite from happening:
315bcaQ.png

Code:
; ---------------------------------------------------------------------------
; Subroutine to move the water or oil surface sprites to where the screen is at
; (the closest match I could find to this subroutine in Sonic 1 is Obj_SpeedBooster_Action)
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_44E4:
UpdateWaterSurface:
    tst.b    (Water_flag).w
    beq.s    ++    ; rts
    move.w    (Camera_X_pos).w,d1
    btst    #0,(Timer_frames+1).w
    beq.s    +
    addi.w    #$20,d1
+        ; match obj x-position to screen position
    move.w    d1,d0
    addi.w    #$60,d0
    move.w    d0,(WaterSurface1+x_pos).w
    addi.w    #$120,d1
    move.w    d1,(WaterSurface2+x_pos).w
+
    rts
; End of function UpdateWaterSurface

Change this to:

Code:
; ---------------------------------------------------------------------------
; Subroutine to move the water surface sprites to where the screen is at
; (the closest match I could find to this subroutine in Sonic 1 is Obj_SpeedBooster_Action)
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_44E4:
UpdateWaterSurface:
    tst.b    (Water_flag).w
    beq.s    ++    ; rts
    move.w    (Camera_X_pos).w,d1
    btst    #button_start,(Ctrl_1_Press).w        ; is Start button pressed?
    bne.s    +                                    ; if yes, branch
    btst    #0,(Timer_frames+1).w
    beq.s    +
    addi.w    #$20,d1
+        ; match obj x-position to screen position
    move.w    d1,d0
    addi.w    #$60,d0
    move.w    d0,(WaterSurface1+x_pos).w
    addi.w    #$120,d1
    move.w    d1,(WaterSurface2+x_pos).w
+
    rts
; End of function UpdateWaterSurface
This

Adds a start button check to prevent it from moving over. However if you build now it's still broken what gives?

Well you need to move the call to UpdateWaterSurface in Level_MainLoop to be above RunObjects and you'll be golden!

Thanks Devon for helping me figure that part out.


For completion sake this bug is in Sonic 1 also so here:

i9nWHqS.png


So in Git Sonic 1 or in Hivebrain 2021 the code is as follows in _incOBJ/1B Water Surface.asm
Code:
Surf_Action:    ; Routine 2
        move.w    (v_screenposx).w,d1
        andi.w    #$FFE0,d1
        add.w    surf_origX(a0),d1
        btst    #0,(v_framebyte).w
        beq.s    @even        ; branch on even frames
        addi.w    #$20,d1

    @even:

What we need to do is add a press start check between "add.w surf_origX(a0),d1" and btst #0,(v_framebyte).w like so:
Code:
Surf_Action:    ; Routine 2
        move.w    (v_screenposx).w,d1
        andi.w    #$FFE0,d1
        add.w    surf_origX(a0),d1
        btst    #bitStart,(v_jpadpress1).w ; is Start button pressed?
        bne.s    @even    ; if yes, branch
        btst    #0,(v_framebyte).w
        beq.s    @even        ; branch on even frames
        addi.w    #$20,d1

    @even:
This will fix the gap.
For the old hivebrain disasm go to Obj1B_Action and do the same thing but worse:
Code:
Obj1B_Action:                ; XREF: Obj1B_Index
        move.w    ($FFFFF700).w,d1
        andi.w    #$FFE0,d1
        add.w    $30(a0),d1
        btst    #7,($FFFFF605).w ; is Start button pressed?
        bne.s    loc_11114    ; if yes, branch
        btst    #0,($FFFFFE05).w
        beq.s    loc_11114
        addi.w    #$20,d1

loc_11114:
and there you go. Enjoy the finer water surfaces in life.
 
Last edited:
Manual Special Stage Rotation Control in Sonic 1

Would you like to have Sonic 1 Special Stages where you control the rotation with the d-pad, a la Sonic 4: Episode 1? It's actually quite simple, and should only involve about 7 lines of code.

In the Special gamemode's init code, comment out this line:

Code:
move.w    #$40,(v_ssrotate).w ; set stage rotation speed

Optionally, comment out this line under the 'SS_ShowLayout' subroutine, for smooth rotation (courtesy of Cinossu):
Code:
andi.b    #$FC,d0

Next, in Object 09's code, add these lines to disable Sonic's movement, and make your input rotate the stage instead:
Code:
...
Obj09_MoveLeft:
        sub.w #$XX,(v_ssangle).w    ; TIS - include whatever value you'd like, tweak to desired rotation speed
        rts    ;TIS     - Disable Sonic's left movement
        bset    #0,obStatus(a0)
        move.w    obInertia(a0),d0
        ...

Code:
Obj09_MoveRight:
        add.w #$XX,(v_ssangle).w    ;TIS - include whatever value you'd like, tweak to desired rotation speed
        rts      ;TIS - Disable Sonic's right movement
        bclr    #0,obStatus(a0)
        move.w    obInertia(a0),d0
        ...

Finally, disable Object 09's jump here:


Code:
Obj09_Jump:
        rts    ;TIS - Disable Jump Routine
        move.b    (v_jpadpress2).w,d0
        andi.b    #btnABC,d0    ; is A,    B or C pressed?
        ...

Enjoy!
 
Fix some graphical bubble glitch when drowning in Sonic 1

I want you to take a look at this image:
S1WOOLOOFIED000.png

Now see where I circled the issue in pink, you may believe it's intentional, but that little graphic hangs there when you drown, which is quite unlogical. Looking at the code in a standard Hivebrain disassembly appals me:

Code:
Obj0A_ChkWater:                ; XREF: Obj0A_Index
        move.w    ($FFFFF646).w,d0
        cmp.w    $C(a0),d0    ; has bubble reached the water surface?
        bcs.s    Obj0A_Wobble    ; if not, branch
        move.b    #6,$24(a0)
        addq.b    #7,$1C(a0)
        cmpi.b    #$D,$1C(a0) ; DW: look here
        beq.s    Obj0A_Display ; DW: here
        bra.s    Obj0A_Display; DW: and here

The issue is they set a check to animation ID D for it to branch instantly to Obj0A_Display, but at the same time, you'll still be branching there, nevertheless making the code pretty redundant if you ask me. So if we check out what animation ID D does in obj0A:

Code:
byte_14148:    dc.b $E, $FC

What it does is set a frame that blanks out the art from loading when you drown to 0. To fix it, we would need to set a check, so the animation ID loads in so in Obj0A_ChkWater (or Drown_ChkWater for GitHub/Hivebrain 2021 users), under the second-last line, insert this:

Code:
        bcs.s    Obj0A_Display ; that would be "bcs.s    Drown_Display" for Git/Hive2021 users
        move.b    #$D,$1C(a0)      ; change $1C to obAnim if you using the disassembly I mentioned above

This makes sure it loads animation ID D well, so the art will be blank the next time you drown. From there, save, build and test it, and you should get this:

S1WOOLOOFIED001.png

Well done! You have done and dusted another bug in Sonic 1.
 
Fix rings disappearing too early at the top of the screen in Sonic 2

97l1V6W.png


If you pay close attention, rings that go offscreen at the top will disappear too early. This is due to a small bug with the check.

Code:
    move.w    4(a0),d2    ; get ring Y pos
    sub.w    4(a3),d2    ; subtract camera Y pos
    andi.w    #$7FF,d2
    addi_.w    #8,d2
    bmi.s    BuildRings_NextRing    ; dunno how this check is supposed to work
    cmpi.w    #240,d2
    bge.s    BuildRings_NextRing    ; if the ring is not on-screen, branch

The "dunno how this check is supposed to work" comment refers to how it ANDs the Y offset with $7FF, adds 8, and then checks if it's negative, in which will never be the case. In the prototype version of the code, that AND wasn't in there, which allowed the upper limit check to work. They most likely added it for Y wrapping in levels, but they forgot to fix the check.

To fix it, we can do what S3K does:
Code:
        move.w  4(a0),d2        ; get ring Y pos
        sub.w   4(a3),d2        ; subtract camera Y pos
        addq.w  #8,d2
        andi.w  #$7FF,d2
        cmpi.w  #224+16,d2
        bhs.s   BuildRings_NextRing     ; if the ring is not on-screen, branch

The 2 player code also suffers from the bug as well:
Code:
    move.w    4(a0),d2    ; get ring Y pos
    sub.w    4(a3),d2    ; subtract camera Y pos
    andi.w    #$7FF,d2
    addi.w    #128+8,d2
    bmi.s    BuildRings_2P_NextRing
    cmpi.w    #240+128,d2
    bge.s    BuildRings_2P_NextRing

Which calls for a similar fix:
Code:
        move.w  4(a0),d2        ; get ring Y pos
        sub.w   4(a3),d2        ; subtract camera Y pos
        addq.w  #8,d2
        andi.w  #$7FF,d2
        cmpi.w  #224+16,d2
        bhs.s   BuildRings_2P_NextRing

And also, go to BuildRings_P1 and change the "128-8" to a "128+128-8" and, then go to BuildRings_P2 and change the "224+128-8" to a "224+128+128-8".
 
Last edited:
Optimise the BossMove subroutine for Sonic 1 and 2

Earlier last month, I've had a conversation with vladikcomper to find the best methods to optimise Sonic 1. He came across how it is possible to optimise the BossMove subroutine, which moves the bosses. And after doing my quick analysis by playing GHZ3, I've noticed that the platforms tend to glitch for about a frame or two every time the boss moves. However, upon looking at the code, I saw how quick and easy it was to optimise the code as it shares similar footsteps to two other subroutines: SpeedToPos and ObjectMove.

So this mini-tutorial will show you how to optimise the BossMove code in vein to S3K. It won't look fast, but it takes less processing time which matters if you intend to have custom bosses with different objects.

This will work under any Sonic 1 disassembly. (Sonic 1 Git will be referenced, but it shouldn't be challenging to cross-port the code to Hivebrain 2005 as it's only constants).

Firstly, let's go and check it out:

Code:
BossMove:
        move.l    $30(a0),d2
        move.l    $38(a0),d3
        move.w    obVelX(a0),d0
        ext.l    d0
        asl.l    #8,d0
        add.l    d0,d2
        move.w    obVelY(a0),d0
        ext.l    d0
        asl.l    #8,d0
        add.l    d0,d3
        move.l    d2,$30(a0)
        move.l    d3,$38(a0)
        rts
; End of function BossMove

I optimised it by comparing how redhotsonic shortened the subroutine and applied it here. The result you should get is this:

Code:
BossMove:
        move.w    obVelX(a0),d0     
        ext.l    d0
        lsl.l    #8,d0            
        add.l    d0,$30(a0)
        move.w    obVelY(a0),d0      
        ext.l    d0
        lsl.l    #8,d0             
        add.l    d0,$38(a0)
        rts
; End of function BossMove

In Sonic 2, the code is only used twice, and that is for the Chemical Plant Zone boss and the rest is for most of the Sonic 2 bosses. This will work under any Sonic 2 disassembly. (Sonic 2 Git will be referenced with comments for Xenowhirl's 2007 disassembly).

So let's take a look at Obj_CPZBoss_Main_Move: (loc_2DB0E for the 2007 disassembly)

Code:
Obj_CPZBoss_Main_Move:
    move.l    Obj_CPZBoss_x_pos_next(a0),d2
    move.l    Obj_CPZBoss_y_pos_next(a0),d3
    move.w    x_vel(a0),d0
    ext.l    d0
    asl.l    #8,d0
    add.l    d0,d2
    move.w    y_vel(a0),d0
    ext.l    d0
    asl.l    #8,d0
    add.l    d0,d3
    move.l    d2,Obj_CPZBoss_x_pos_next(a0)
    move.l    d3,Obj_CPZBoss_y_pos_next(a0)
    rts
I optimised it by comparing how redhotsonic shortened the subroutine and applied it here. The result you should get is this:

Code:
Obj_CPZBoss_Main_Move:
    move.w    x_vel(a0),d0
    ext.l    d0
    lsl.l    #8,d0
    add.l    d0,Obj_CPZBoss_x_pos_next(a0) ; for 2007 users, Obj_CPZBoss_x_pos_next should be objoff_30
    move.w    y_vel(a0),d0
    ext.l    d0
    lsl.l    #8,d0
    add.l    d0,Obj_CPZBoss_y_pos_next(a0) ; for 2007 users, Obj_CPZBoss_y_pos_next should be objoff_38
    rts

Now, let's check out Boss_MoveObject (loc_2D5DE for 2007 users). This is the code:

Code:
Boss_MoveObject:
    move.l    (Boss_X_pos).w,d2
    move.l    (Boss_Y_pos).w,d3
    move.w    (Boss_X_vel).w,d0
    ext.l    d0
    asl.l    #8,d0
    add.l    d0,d2
    move.w    (Boss_Y_vel).w,d0
    ext.l    d0
    asl.l    #8,d0
    add.l    d0,d3
    move.l    d2,(Boss_X_pos).w
    move.l    d3,(Boss_Y_pos).w
    rts

We will need to do the same optimisations as RHS did. The outcome should look something like this:

Code:
loc_2D5DE:
    move.w   (Boss_X_vel).w,d0 ; Boss_X_vel should be $FFFFF758 for 2007 users
    ext.l    d0
    lsl.l    #8,d0           
    add.l    d0,(Boss_X_pos).w ; Boss_X_pos should be $FFFFF750 for 2007 users
    move.w    (Boss_Y_vel).w,d0  ; Boss_Y_vel should be $FFFFF75A for 2007 users
    ext.l    d0
    lsl.l    #8,d0             
    add.l     d0,(Boss_Y_pos).w ; Boss_Y_pos should be $FFFFF754 for 2007 users
    rts

And that's it. Every time the boss moves, it will do the coding quicker in each frame.

EDIT - 17/1/21: Updated the Sonic 2 side of the guide to mention a subroutine that multiple bosses share.
 
Last edited:
Not bad; we might be able to do slightly better though:

Code:
		move.l	obVelX(a0),d0
		move.w	d0,d2
		asr.l	#$08,d0
		sf.b	d0
		add.l	d0,obX(a0)
		ext.l	d2
		asl.l	#$08,d2
		add.l	d2,obY(a0)

Assuming the X and Y speeds are together (as they are in the original); this will save you 4 cycles and 2 bytes.

You could potentially remove the sf and save yourself another 4 cycles and 2 bytes, the fraction space is 8-bits, and the quotient is rarely larger than +/- $10, so that "might" be enough for Y speed not to have any significant or noticeable affect on X speed.
 
Not bad; we might be able to do slightly better though:

Code:
        move.l    obVelX(a0),d0
        move.w    d0,d2
        asr.l    #$08,d0
        sf.b    d0
        add.l    d0,obX(a0)
        ext.l    d2
        asl.l    #$08,d2
        add.l    d2,obY(a0)

Assuming the X and Y speeds are together (as they are in the original); this will save you 4 cycles and 2 bytes.

You could potentially remove the sf and save yourself another 4 cycles and 2 bytes, the fraction space is 8-bits, and the quotient is rarely larger than +/- $10, so that "might" be enough for Y speed not to have any significant or noticeable affect on X speed.

Pretty good optimization there, but there's actually an additional trick we can take advantage of here for a cleaner result that's just as fast as this, sans the 'sf.b d0' instruction:
Code:
        movem.w    obVelX(a0),d0/d2
        lsl.l    #8,d0
        add.l    d0,obX(a0)
        lsl.l    #8,d2
        add.l    d2,obY(a0)

Odd as it seems, movem.w actually sign extends to a longword, even on data registers. This way, we load the velocity values into their appropriate registers and sign-extend them to a long in a single instruction. I imagine this saves a few extra bytes, too.
 
Ahhhh that's amazing! Sign extension of movem on data registers, now that is something I did not expect, good call~
 
Thanks for the optimizations, you two. You are much appreciated! djohe reminded me that a standard subroutine already handles the boss moving code that I wasn't aware of in Sonic 2. So whoever hacks Sonic 2 and uses the code I mentioned above, please recheck the tutorial as the guide has been updated to mention this.

Thanks!
 
Last edited:
I don't know if that classifies as a tutorial at all, considering that it's just one line of code but uh...
Do you want to make the Sonic 1 ending screen be more like the Retro Engine version of Sonic 1?
Do you somehow not want the game to load in the HUD in the ending?
Well in this tutorial it's as easy as pie, however this tutorial uses the Sonic 1 GitHub disassembly so yea...
Just go to "End_LoadSonic" and you should find this
Code:
        move.b    #id_HUD,(v_objspace+$40).w ; load HUD object <--- This is how it looks in GitHub, could look different in 2005 Hivebrain disassembly
Either delete it (untested) or comment it out (already tested)
Here's how it looked before:
blastem_20220520_203224.png
and here's the after:
blastem_20220520_203324.png
Should note that Sonic 1 is the only time the HUD was loaded at all during the ending sequence.
Feel free to figure out what is wrong with that method and/or find new, much more simpler, ways of not loading the HUD during the ending sequence.
Have fun using this trick in your hacks.
 
I don't know if that classifies as a tutorial at all, considering that it's just one line of code but uh...
Do you want to make the Sonic 1 ending screen be more like the Retro Engine version of Sonic 1?
Do you somehow not want the game to load in the HUD in the ending?
Well in this tutorial it's as easy as pie, however this tutorial uses the Sonic 1 GitHub disassembly so yea...
Just go to "End_LoadSonic" and you should find this
Code:
        move.b    #id_HUD,(v_objspace+$40).w ; load HUD object <--- This is how it looks in GitHub, could look different in 2005 Hivebrain disassembly
Either delete it (untested) or comment it out (already tested)
Here's how it looked before:
View attachment 187
and here's the after:
View attachment 188
Should note that Sonic 1 is the only time the HUD was loaded at all during the ending sequence.
Feel free to figure out what is wrong with that method and/or find new, much more simpler, ways of not loading the HUD during the ending sequence.
Have fun using this trick in your hacks.
I can actually say that you did the simplest and least hacky way of not having the HUD in the ending: by making it LITERALLY not load at all.
 
Pretty good optimization there, but there's actually an additional trick we can take advantage of here for a cleaner result that's just as fast as this, sans the 'sf.b d0' instruction:
Code:
        movem.w    obVelX(a0),d0/d2
        lsl.l    #8,d0
        add.l    d0,obX(a0)
        lsl.l    #8,d2
        add.l    d2,obY(a0)

Odd as it seems, movem.w actually sign extends to a longword, even on data registers. This way, we load the velocity values into their appropriate registers and sign-extend them to a long in a single instruction. I imagine this saves a few extra bytes, too.
i made an optimized version which is faster than this by 2 cycles
Code:
ObjectMove:
    lea     x_vel(a0),a5  ; 8 cycles
    move.l  (a5),d0  ;12 cycles (x vel+yvel)
    move.w  d0,d1       ; 4 cycles
    swap    d0  ; swap to the first word (4 cycles)
    ext.l   d1
    asl.l    #$8,d1 ; multyply by $80  (whatever pixels) (depends on value)
    add.l    d1,-(a5) ; add our x value  (y pos)
    ; and now d1 contains y vel !  ; (20 cycles)
    ext.l   d0    ;(4 cycles)
    asl.l    #$8,d0  ;depends on value)
    add.l    d0,-(a5)    ;20 cycles
    rts
 
Sorry to break it to you, but no, it's not.
Code:
        movem.w obVelX(a0),d0/d2        ; 24 (16+(4*2))
        lsl.l   #8,d0                   ; 24 (8+(2*8))
        add.l   d0,obX(a0)              ; 24
        lsl.l   #8,d2                   ; 24 (8+(2*8))
        add.l   d2,obY(a0)              ; 24
                                        ; Total: 120
Code:
        lea     x_vel(a0),a5            ; 8
        move.l  (a5),d0                 ; 12
        move.w  d0,d1                   ; 4
        swap    d0                      ; 4
        ext.l   d1                      ; 4
        asl.l   #$8,d1                  ; 24 (8+(2*8))
        add.l   d1,-(a5)                ; 22
        ext.l   d0                      ; 4
        asl.l   #$8,d0                  ; 24 (8+(2*8))
        add.l   d0,-(a5)                ; 22
                                        ; Total: 128

"add.l dN,-(aN)" is 22 cycles, not 20, according to this and this. Even if it was 20, it'd total to 124 still. Honestly, the only way I see it being made any faster is to convert the velocity variables into 16.16 point instead of 8.8, so you can just do this:
Code:
        movem.l obVelX(a0),d0/d2        ; 32 (16+(8*2))
        add.l   d0,obX(a0)              ; 24
        add.l   d2,obY(a0)              ; 24
                                        ; Total: 80
 
Back
Top