[Sonic 2/Sonic 3 & Knuckles] Slightly improve the 2 player experience

Discussion in 'Tutorials' started by Giovanni, Apr 21, 2023.

  1. Giovanni

    Giovanni It's Joe-vanni, not Geo-vanni. Member

    Joined:
    Apr 16, 2015
    Messages:
    297
    Location:
    Italy
    I somewhat feel like the Co-op experience in the Classic Sonic games is kind of flawed. In this guide, you will find three changes that can provide a slight improvement to it.

    1) Make pausing player dependent

    Let's imagine a simple scenario. You're playing Sonic 2's 2PVS mode with a friend, or a sibling. You decide to pause the game for a second to get some water, and you come back to find the game unpaused, despite the fact that your opponent didn't even touch your controller! That's right, the cheeky bastard unpaused the game with their own controller!

    In Sonic 3 & Knuckles, this was somewhat fixed... by only letting player 1 pause the game. Which means that if you were playing, say, Sonic 3's Competition mode, and you, player 2, wanted to take a break, you had to trust in your opponent's kindness to pause the game.

    This modified code makes pausing player dependent, so that if player 1 pauses, only player 1 can unpause. Same for player 2.

    Before you continue, it is assumed you're using the AS disassembly. If you are not, remember to replace all of the dots with "@"s. Also, replace the "+" signs with new labels, also starting in "@".

    When the game gets paused by either player pressing the Start button, the game sets a flag. We can turn this flag into a variable that sets itself differently based on which player pressed the Start button.

    Just a few lines above Pause_Loop, you'll find this:
    Code:
        move.w    #1,(Game_paused).w    ; freeze time
    Under this line, place this:
    Code:
        btst    #button_start,(Ctrl_2_Press).w        ; was it player 2 who paused?
        beq.s    +
        move.w    #2,(Game_paused).w
    +    
    If it's player 2 that presses the Pause button, the game gets marked accordingly. If both players pause at the same time, player 2 gets port priority.

    Now, we need to edit the Pause_ChkStart routine. The original Pause_ChkStart routine checks for either player pressing the Start button. We don't want that: rather, we want to check if the player that presses the Start button is actually the one who paused.

    Replace everything inbetween the labels Pause_ChkStart and Pause_Resume with these lines:

    Code:
        btst   #button_start,(Ctrl_1_Press).w       ; is the Start button pressed?
        beq.s   +                                   ; if not, branch
        cmpi.w   #2,(Game_paused).w                   ; was it player 2 who paused?
        beq.s   +                                   ; if yes, branch
        bra.s   Pause_Resume                       ; resume game
     
    +
        btst   #button_start,(Ctrl_2_Press).w       ; is the Start button pressed?
        beq.s   Pause_Loop                           ; if not, branch
        cmpi.w   #2,(Game_paused).w                   ; was it player 2 who paused?
        bne.s   Pause_Loop                           ; if not, branch
    ; below this line should be Pause_Resume:
    
    Save, build, and try it out!

    When the game gets paused, the game sets a flag. We can turn this flag into a variable that sets itself differently based on which player pressed the Start button.

    But first, we have to allow the second player to pause, too.

    In Pause_Main, below this line...

    Code:
            move.b    (Ctrl_1_pressed).w,d0
    ...add this line:

    Code:
            or.b    (Ctrl_2_pressed).w,d0
    This line allows player 2 to pause the game, just like in Sonic 2.

    Now, close below, you'll find this line:

    Code:
            move.w    #1,(Game_paused).w
    Under this line, place this:

    Code:
            move.w    #1,(Game_paused).w
            btst    #7,(Ctrl_2_pressed).w        ; was it player 2 who paused?
            beq.s    +
            move.w    #2,(Game_paused).w
    +                
    If it's player 2 that presses the Pause button, the game gets marked accordingly. If both players pause at the same time, player 2 gets port priority.

    Now, we need to edit the Pause_ChkStart routine. The original Pause_ChkStart routine accounts only for player 1's actions. We don't want that: rather, we want to check if the player that presses the Start button is actually the one who paused.

    Replace everything inbetween the labels Pause_ChkStart and Pause_ResumeMusic with these lines:

    Code:
            btst    #7,(Ctrl_1_pressed).w                ; is the Start button pressed?
            beq.s    +                                    ; if not, branch
            cmpi.w    #2,(Game_paused).w                    ; was it player 2 who paused?
            beq.s    +                                    ; if yes, branch
            bra.s   Pause_ResumeMusic                     ; resume game
     
    +
            btst    #7,(Ctrl_2_pressed).w                    ; is the Start button pressed?
            beq.s    Pause_Loop                            ; if not, branch
            cmpi.w    #2,(Game_paused).w                    ; was it player 2 who paused?
            bne.s    Pause_Loop                            ; if not, branch
    ; under this line, there should be Pause_ResumeMusic:    
    Save, build, and try it out!

    2) Make score chains player dependent

    Is your opponent, by chance, racking up a lot of points in Sonic 2's 2PVS? Surely, that wouldn't sit well with you. Well, you can stop their awesome score chain by just landing on the floor. Congratulations! You just earned yourself a massive beating by one of the 3 other people in the world who actively go for score in Sonic games!

    In Sonic 3 & Knuckles, this is not as critical, given there's no 2P mode like Sonic 2's, but your score chain can still be hindered by your partner's meddling! So, let's fix this.

    You'll need RAM space for a new word sized variable, which we'll call Chain_Bonus_Counter_2P.

    First change that needs to be made is in Touch_KillEnemy.

    In between these two lines...

    Code:
         moveq    #0,d0
        move.w    (Chain_Bonus_counter).w,d0
    ...place this code:

    Code:
        cmpi.w    #MainCharacter,a0                ; is caller the main character?
        beq.s    +                                ; if not, branch
        move.w    (Chain_Bonus_counter_2P).w,d0
        addq.w    #2,(Chain_Bonus_counter_2P).w    ; add 2 to chain bonus counter
        bra.s    ++
    +    
    And, add a "+" label after this line:

    Code:
        addq.w    #2,(Chain_Bonus_counter).w    ; add 2 to chain bonus counter
    Now, in loc_3F802, in between these lines...

    Code:
        move.w    Enemy_Points(pc,d0.w),d0
        cmpi.w    #$20,(Chain_Bonus_counter).w    ; have 16 enemies been destroyed?
    ...add this:

    Code:
        cmpi.w    #MainCharacter,a0                ; is caller the main character?
        beq.s    +                                ; if not, branch
        cmpi.w    #$20,(Chain_Bonus_counter_2P).w    ; have 16 enemies been destroyed?
        blo.s    loc_3F81C            ; if not, branch
        bra.s    ++
    +
    then, add another "+" label under this line:

    Code:
        blo.s    loc_3F81C            ; if not, branch
    If you're confused, the code from Touch_KillEnemy until loc_3F81C (label not included) should look like this:

    Code:
    Touch_KillEnemy:
        bset    #7,status(a1)
        moveq    #0,d0
        cmpi.w    #MainCharacter,a0                ; is caller the main character?
        beq.s    +                                ; if not, branch
        move.w    (Chain_Bonus_counter_2P).w,d0
        addq.w    #2,(Chain_Bonus_counter_2P).w    ; add 2 to chain bonus counter
        bra.s    ++
    +
        move.w    (Chain_Bonus_counter).w,d0
        addq.w    #2,(Chain_Bonus_counter).w    ; add 2 to chain bonus counter
    +
        cmpi.w    #6,d0
        blo.s    loc_3F802
        moveq    #6,d0
    
    loc_3F802:
        move.w    d0,objoff_3E(a1)
        move.w    Enemy_Points(pc,d0.w),d0
        cmpi.w    #MainCharacter,a0                ; is caller the main character?
        beq.s    +                                ; if not, branch
        cmpi.w    #$20,(Chain_Bonus_counter_2P).w    ; have 16 enemies been destroyed?
        blo.s    loc_3F81C            ; if not, branch
        bra.s    ++
    +
        cmpi.w    #$20,(Chain_Bonus_counter).w    ; have 16 enemies been destroyed?
        blo.s    loc_3F81C            ; if not, branch
    +
        move.w    #1000,d0            ; fix bonus to 10000 points
        move.w    #$A,objoff_3E(a1)
    Now, you want to go under the code of any character that can be a sidekick or an opponent. If you've made core changes to the game, you know which characters to edit. Otherwise, it can only be Tails. So, go under his code, at Tails_ResetOnFloor_Part3.

    Replace this line...

    Code:
        move.w    #0,(Chain_Bonus_counter).w
    ...with this:

    Code:
        cmpi.w    #MainCharacter,a0
        beq.s    +
        move.w    #0,(Chain_Bonus_counter_2P).w
        bra.s    ++
    +
        move.w    #0,(Chain_Bonus_counter).w
    +    
    Save, build, and try it out! Now, both players should have their own score chains.

    First change that needs to be made is in .dontremember, in Touch_EnemyNormal.

    Replace these two lines...

    Code:
            move.w    (Chain_bonus_counter).w,d0    ; Get copy of chain bonus counter
            addq.w    #2,(Chain_bonus_counter).w    ; Add 2 to chain bonus counter
    with this:

    Code:
            cmpi.w    #Player_1,a0                ; is caller the main character?
            beq.s    +                                ; if not, branch
            move.w    (Chain_Bonus_counter_2P).w,d0
            addq.w    #2,(Chain_Bonus_counter_2P).w    ; add 2 to chain bonus counter
            bra.s    ++
    +
            move.w    (Chain_Bonus_counter).w,d0
            addq.w    #2,(Chain_Bonus_counter).w    ; add 2 to chain bonus counter
    +
    Then, in .notreachedlimit, replace this:

    Code:
           cmpi.w   #16*2,(Chain_bonus_counter).w   ; Have 16 enemies been destroyed?
           blo.s   .notreachedlimit2       ; If not, branch
           move.w   #1000,d0           ; Fix bonus to 10000 points
           move.w   #$A,objoff_3E(a1)
    
       .notreachedlimit2:
    
    With this:

    Code:
           cmpi.w    #Player_1,a0                ; is caller the main character?
           beq.s    +                                ; if not, branch
           cmpi.w    #$20,(Chain_Bonus_counter_2P).w    ; have 16 enemies been destroyed?
           blo.s    +++         ; if not, branch
           bra.s    ++
    +
           cmpi.w    #$20,(Chain_Bonus_counter).w    ; have 16 enemies been destroyed?
           blo.s    ++           ; if not, branch
    +
           move.w   #1000,d0           ; Fix bonus to 10000 points
           move.w   #$A,objoff_3E(a1)
    
    +
    
    Now, you want to go under the code of any character that can be a sidekick or an opponent. If you've made core changes to the game, you know which characters to edit. Otherwise, it can only be Tails. So, go under his code, at loc_1565E.

    Change this:

    Code:
            move.w    #0,(Chain_bonus_counter).w
    To this:

    Code:
            cmpi.w    #Player_1,a0
            beq.s    +
            move.w    #0,(Chain_Bonus_counter_2P).w
            bra.s    ++
    +
            move.w    #0,(Chain_Bonus_counter).w
    +    
    Save, build, and test it out!

    Do note, however, that this code opens up an exploit in Sonic 3 & Knuckles' 2 player co-op! In fact, since Tails landing no longer resets Sonic's score chain, a series of well timed jumps, as well as a pair of synchronized players, can lead to a never ending score chain! You're free to fix this by clearing player 1's score chain in player 2's code, but in my honest opinion, this actually sounds like a fun exploit. One could even make it part of a score based challenge hack. Who knows?

    3) In-game controller swap

    This one especially applies to emulators with netplay capabilities.

    One thing I absolutely HATE is the fact that not a single emulator for the SEGA Mega Drive/Genesis with netplay capabilities allows to remap controller ports like emulators for other systems (e.g. Dolphin). However, we can compensate for this by adding a way to swap controllers from within the game itself.

    This part assumes you've followed part 1 of the guide.

    You'll need a new byte sized variable, one which we'll call "Invert_Joypads_Flag". Make sure you place this one someplace that doesn't get reset!

    Go to ReadJoypads, and under this line...

    Code:
        lea    (Ctrl_1).w,a0    ; address where joypad states are written
    Place these two lines:

    Code:
        tst.b    (Invert_Joypads_Flag).w
        bne.s    JR_Alternateread
    Then, right before the comment that marks the end of ReadJoypads, add these lines:

    Code:
    JR_Alternateread:
        lea    (HW_Port_2_Data).l,a1    ; second joypad port
        bsr.s    Joypad_Read        ; do the second joypad
        subq.w    #2,a1      
        bra.s    Joypad_Read        ; do the first joypad    
    Now, we're going back to the pause feature.

    Change this:

    Code:
        btst   #button_start,(Ctrl_2_Press).w       ; is the Start button pressed?
        beq.s   Pause_Loop                           ; if not, branch
        cmpi.w   #2,(Game_paused).w                   ; was it player 2 who paused?
        bne.s   Pause_Loop                           ; if not, branch
    Into this:

    Code:
        btst    #button_start,(Ctrl_2_Press).w    ; is Start button pressed?
        beq.s    .checkplayerswap
        cmpi.w   #2,(Game_paused).w
       bne.s   .checkplayerswap
        bra.s    Pause_Resume
     
    .checkplayerswap:
        tst.w    (Two_player_mode).w
        bne.s    Pause_Loop
        move.b    (Ctrl_1_Held).w,d0
        and.b    (Ctrl_2_Held).w,d0
        cmpi.b    #button_A_mask,d0
        bne.s    Pause_Loop
        not.b    (Invert_Joypads_Flag).w
    You may end up having to fix a few branch distances in the Pause code, so make sure to do that.

    Go to Poll_Controllers, and under this line...

    Code:
            lea    (Ctrl_1).w,a0
    ...place this:

    Code:
            tst.b    (Invert_Joypads_Flag).w
            bne.s    PC_Alternateread    
    Then, right before the comment that marks the end of Poll_Controller, add these lines:

    Code:
    PC_Alternateread:
        lea    (HW_Port_2_Data).l,a1    ; second joypad port
        bsr.s    Poll_Controller        ; do the second joypad
        subq.w    #2,a1      
        bra.s    Poll_Controller        ; do the first joypad      
    
    Now, we're going back to the pause feature.

    Change this:

    Code:
            btst    #7,(Ctrl_2_pressed).w                    ; is the Start button pressed?
            beq.s    Pause_Loop                            ; if not, branch
            cmpi.w    #2,(Game_paused).w                    ; was it player 2 who paused?
            bne.s    Pause_Loop   
    Into this:

    Code:
           btst   #7,(Ctrl_2_pressed).w                    ; is the Start button pressed?
           beq.s   +                       ; if not, branch
           cmpi.w   #2,(Game_paused).w                   ; was it player 2 who paused?
           bne.s   +                           ; if not, branch
           bra.s   Pause_ResumeMusic
     
    +
           tst.w    (Competition_mode).w
           bne.w    Pause_Loop
           move.b    (Ctrl_1).w,d0
           and.b    (Ctrl_2).w,d0
           cmpi.b    #$40,d0
           bne.w    Pause_Loop
           not.b    (Invert_Joypads_Flag).w
        
    You may end up having to fix a few branch distances in the Pause code, so make sure to do that.

    Here's what the above code does:

    If both players press A while the game is paused, the game gets unpaused, but the two player controllers get swapped in such a way that reads from Player 1's controller on hardware get sent to Player 2's input RAM, and Player 2's reads go to Player 1's input RAM. The two players can swap at any time, with the exception of when they're in 2PVS gamemodes.

    With these three changes together, you'll be able to provide some small, but still meaningful improvements to the 2 player experience in Sonic! Why don't you try these changes out with a friend?
     
    Last edited: Apr 22, 2023
    Crimson Neo, Stdh, ProjectFM and 4 others like this.