Make Knuckles and Custom Characters Work in 2 Player VS Mode
From Sonic Retro
(Original guide by E-122-Psi)
Contents
Making spritework for VS mode:
As many know, Sonic 2's VS mode is infamously finicky displaying sprites. To expand the resolution, the split screen interlace loads tiles as 8 by 16 rather than 8 by 8, interchanging between odd and even scanlines of each tile frame by frame.
This means it needs all pixel art to be loaded as tiles of 8 by 16 pixels, both in level art chunks, and sprites for objects and characters, otherwise it will mix tiles up and the art will often display as an incoherent mess.
Since tiles are 8 by 8 pixels this means they can be any length horizontally, but must be made of chunks with even numbers vertically like so:
Tile IDs must also be even, though assuming you kept the characters located in the same areas in the VRAM and reused blocks between sprites in a similar way, this will likely be inevitable anyway.
To compare, here are Knuckles' sprite files properly optimised for split screen (info) (68 kB) against his original sprite setup (info) (70 kB) in Sonic the Hedgehog 2.
Also remember not to overload the sprites with tiles in the process otherwise they might override VRAM for other stuff like Tails.
In addition, also make sure the character's routine has the same check to adjust to split screen settings like Sonic and Tails' routines do when loading their art:
move.w #$780,art_tile(a0)
bsr.w Adjust2PArtPointer
Making characters selectable in VS mode:
Now we know how to make a character look right in splitscreen, however, unless you added them in place of Sonic or Tails, there's no way to choose them. Sonic 2's two player mode automatically chooses Sonic and Tails mode by default, so we're gonna hot wire the character select to work around it.
NOTE: To make this work, you'll need to have added a new mode for the character in one player mode that uses a second character, eg. Knuckles and Tails mode. If you haven't accomplished this yet, I'd advise checking the player and special stage routines as well as any checks for #0,(Player_mode), Sonic and Tails mode player ID, to see how the game handles co-op player modes. For example InitPlayers checks to load Tails as a second player in certain levels, while the end of loc_4F9C checks to do so in the special stage:
; sub_446E:
InitPlayers:
move.w (Player_mode).w,d0
bne.s InitPlayers_Alone ; branch if this isn't a Sonic and Tails game
move.b #1,(MainCharacter).w ; load Obj01 Sonic object at $FFFFB000
move.b #8,(Sonic_Dust).w ; load Obj08 Sonic's spindash dust/splash object at $FFFFD100
cmpi.b #6,(Current_Zone).w
beq.s return_44BC ; skip loading Tails if this is WFZ
cmpi.b #$E,(Current_Zone).w
beq.s return_44BC ; skip loading Tails if this is DEZ
cmpi.b #$10,(Current_Zone).w
beq.s return_44BC ; skip loading Tails if this is SCZ
move.b #2,(Sidekick).w ; load Obj02 Tails object at $FFFFB040
move.w (MainCharacter+x_pos).w,(Sidekick+x_pos).w
move.w (MainCharacter+y_pos).w,(Sidekick+y_pos).w
subi.w #$20,(Sidekick+x_pos).w
addi.w #4,(Sidekick+y_pos).w
move.b #8,(Tails_Dust).w ; load Obj08 Tails' spindash dust/splash object at $FFFFD140
return_44BC:
rts
....
move #$2300,sr
lea (VDP_control_port).l,a6
move.w #$8F02,(a6) ; VRAM pointer increment: $0002
bsr.w ssInitTableBuffers
bsr.w ssLdComprsdData
move.w #0,($FFFFDB0A).w
moveq #$3C,d0
bsr.w RunPLC_ROM
clr.b (Level_started_flag).w
move.l #0,(Camera_X_pos).w ; probably means something else in this context
move.l #0,(Camera_Y_pos).w
move.l #0,($FFFFEEF0).w
move.l #0,($FFFFEEF4).w
cmpi.w #1,(Player_mode).w
bgt.s loc_514C
move.b #9,(MainCharacter).w ; load Obj09 (special stage Sonic)
tst.w (Player_mode).w
bne.s loc_5152
loc_514C:
move.b #$10,(Sidekick).w ; load Obj10 (special stage Tails)
Once we have the character setup ready in one player, the answer lies with a simple edit to 'sub_4450' or 'Level_SetPlayerMode' depending on which disassembly you're using.
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
sub_4450:
cmpi.b #$88,(Game_Mode).w ; pre-level demo mode?
beq.s +
tst.w (Two_player_mode).w
bne.s +
move.w (Player_option).w,(Player_mode).w
rts
; ---------------------------------------------------------------------------
+
move.w #0,(Player_mode).w ;force Sonic and Tails
rts
; End of function sub_4450
As you can see, the branch for two player mode automates the character selection to Sonic and Tails mode. We can just switch that to whichever character mode you want to use. Assuming your custom mode with two characters is the first one you added after 'Tails Alone', it will be ID 3. After that one, 4, 5, and so forth.
move.w #3,(Player_mode).w ;force 'X character' and Tails
rts
Buuut, now we can't switch to Sonic and Tails anymore, but having the option is as simple as putting in the same allignment code as single player to recognise the player selection.
move.w (Player_option).w,(Player_mode).w
rts
However THIS can have sloppy results if you choose one of the single character options like Sonic/Tails Alone, which will simply load one character and another unoccupied screen. So we'll merge the two methods together and add some checks for which mode is highlighted so as to narrow things down.
(Be sure to add an extra + to the branch for demo mode, since that uses the same branch as two player, and your custom character might break some sequences.)
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
sub_4450:
cmpi.b #$88,(Game_Mode).w ; pre-level demo mode?
beq.s ++
tst.w (Two_player_mode).w
bne.s +
move.w (Player_option).w,(Player_mode).w
rts
; ---------------------------------------------------------------------------
+
move.w (Player_option).w,(Player_mode).w
cmpi.w #3,(Player_option).w ; Is 'X character' mode or higher selected?
bcs.s + ; if not, branch
move.w #3,(Player_mode).w ;force 'X character' and Tails
rts
+
move.w #0,(Player_mode).w ;force Sonic and Tails
rts
; End of function sub_4450
If you have done this correctly, VS mode's character format should now be normal when selecting Sonic and Tails/Sonic Alone/Tails Alone, but switch to your own character format for any new entries.
Bug Fixes
Changing the Signpost art
You may notice that, even after adding a custom signpost for your character in single player, VS mode still shows Sonic. This is because 2 Player VS actually uses a separate signpost from single player, using uncompressed art rather than nemesis compressed art.
To fix this, redo your custom signpost but save it as uncompressed (or use this fixed art file if you're adding Knuckles (info) (2 kB)) and put in the allocated folder. Then add it to your asm:
;---------------------------------------------------------------------------------------
; Uncompressed art
; Signpost ; ArtUnc_7A18A:
; Yep, it's in the rom twice, once compressed and once uncompressed
even
ArtUnc_Signpost: BINCLUDE "art/uncompressed/Signpost.bin"
even
ArtUnc_Signpost_Alt: BINCLUDE "art/uncompressed/Signpost alt.bin"
;---------------------------------------------------------------------------------------
Then add a branch in loc_19564 to use the new signpost art if your character is chosen:
loc_19564:
move.w d1,d3
lsr.w #8,d3
andi.w #$F0,d3
addi.w #$10,d3
andi.w #$FFF,d1
lsl.l #5,d1
cmpi.w #3,(Player_option).w ; Is 'X character' mode or higher selected?
bcs.s + ; if not, branch
addi.l #ArtUnc_Signpost_Alt,d1
jmp ++
+
addi.l #ArtUnc_Signpost,d1
+
move.w d4,d2
add.w d3,d4
add.w d3,d4
jsr (QueueDMATransfer).l
dbf d5,loc_19560
And voila, your character will now have the right signpost art.
Fixing Knuckles' Death Reset
If you're adding Knuckles or making a character using Knuckles' S2K asm, you may run into an odd glitch. When Knuckles dies, rather than panning back to his respawn area, the level fades out and resets like in single player mode. This is due to S2K rearranging some variables such as the check for two player mode. If we wanna fix it, go to the resetlevel routine in Knuckles' code:
Obj01_ResetLevel_Part2: ; ...
tst.w ($FFFFFFDC).w
beq.s return_316F64
move.b #0,($FFFFEEBE).w
move.b #$A,$24(a0)
move.w ($FFFFFE32).w,8(a0)
move.w ($FFFFFE34).w,$C(a0)
move.w ($FFFFFE3C).w,2(a0)
move.w ($FFFFFE3E).w,$3E(a0)
clr.w ($FFFFFE20).w
clr.b ($FFFFFE1B).w
move.b #0,$2A(a0)
move.b #5,$1C(a0)
move.w #0,$10(a0)
move.w #0,$12(a0)
move.w #0,$14(a0)
move.b #2,$22(a0)
move.w #0,$2E(a0)
move.w #0,$3A(a0)
return_316F64: ; ...
rts
; End of function CheckGameOver
The fix is simple, just change the starting branch to two player mode's correct flag:
tst.w (Two_player_mode).w
beq.s return_316F64