Both the water and split-screen effects rely on horizontal interrupts, and the game isn't programmed to account for both effects at once. The interrupt handler itself decides which effect to use based on whether you're in two-player mode, and the game as a whole only ever prepares for a single horizontal interrupt. To get both effects working, you'd have to include some extra logic to calculate when each of the interrupts is supposed to happen (i.e. on which scan line), as well as what each interrupt is supposed to do. Ideally you'd do this outside the interrupt handler, since you don't want to waste time on anything other than data transfers there. This was actually a topic in the community a few years ago, whether it was even possible to do up to three full palette transfers each frame.
This is gonna be harder than I thought. :P Hill Top seems to be the 'easy' fix of the lot. I think to get the other levels running competently I'd need someone with scrolling and compression data knowledge apparent. I suppose there is another factor to consider adding to multiplayer though, and that's more characters: Making a 2 player friendly Knuckles that uses one palette and has correct tile mappings would likely be simple enough, but even then I'd need to figure out how to make a basic character select option for 2 player mode. The best ideas for projects I can think of otherwise are adding a 'Fixed Items' option like Mania did where the monitor generation is normal. Other than that....er, THREE player split screen if people wanna get REALLY ambitious? (I think you'll be on your own for that one though.) :P
So, I've been working on the interrupt thing I mentioned earlier and it turns out it's a little more complicated than I outlined previously. If you look past the graphical garbage, you'll see splitscreen with water present both on the top and bottom. It's not perfect yet, though. For one, it flickers pretty bad whenever slowdown occurs. I haven't been able to pin down why that happens here, but not on waterless splitscreen (I left the original H-Int handlers intact and just switch to this special one for zones with water). The title cards are also a little wonky, but everything seems to be playable, excluding object loading glitches and slowdown.
You got it running? Bravo. :D (I may desire to pick your brain later :P) Ah well the garbage might be down to things I had to run through. Did you customise the SwScrl data for the level? If not switch it to one of the two player branches like I did here (this redirects to HTZ's 2 player scroll routine). It won't make it perfect background wise (I'll need to figure out how to make custom 2 player scroll routines) but it will make Tails' screen more coherent: Code (Text): SwScrl_ARZ: tst.w (Two_player_mode).w bne.w loc_CB10 move.w ($FFFFEEB0).w,d4 ext.l d4 muls.w #$119,d4 ........ Also some of the badnik tiles interrupt the split screen data in the VRAM, which is around the $A000 area (this is also what corrupts the title card). Turn off or redirect Whisp, Chopchop and Grounder's art in the PLC entry for the level.
Maybe I could start looking into the scroll routines. I did fully document most of them a while back, so I might have an easier time making two-player versions of them. So, I was able to fix the flickering bug. Just forgot to reset my scroll registers and sprite tables in one place. There's still two more bugs that I want to iron out, though. For some reason, when Tails is fully under water, then moves up enough to put the water surface on screen, for one frame his screen will fully use the above water palette. Not sure what's causing that... On the top screen, the water palette will sometimes show up a little too late (i.e. a few lines below the water surface) if it is too close to the top of the screen. This is because of the Do_Updates routine taking too long and thus delaying the interrupt handler. I need to find a smarter way of scheduling that call, if it's possible. Or maybe I could just post my code changes here and see if someone else can find the fix
Can if you want. This is a tutorial thread after all (and I'd love all these VS fixes to be open source really). Worse comes to worse, I suppose we can just turn off the water palette for VS mode. Wouldn't be the first time a Sonic game didn't use them.
I have no intention of keeping it secret. I would just prefer to have it fully working before I make a fully fledged guide. Edit: I suppose I could post a preliminary version. That way people could look it over and tell me if I'm doing something terribly wrong
Our good friend Zeta_Null has still been busy tweaking CPZ's tileset to work in split screen by the way, and right now we almost have a working version: There's only a couple objects not reworked at this point (Grabber, the boost pads, and the tipping platforms). Obviously the level tiles themselves needed to be streamlined a good deal to make this work as well but at least it looks coherent and clean now. Zeta_Null fixed the background as well, but I need to fix the scrolling in 2 player to properly show that. The objects needed to be trimmed down in some areas due to being a vertical heavy level, but it is raceable: https://cdn.discordapp.com/attachments/202861068224036864/733438029347291287/s2built.bin EDIT: Updated version which fixes some of the remaining object art: https://cdn.discordapp.com/attachments/202861068224036864/733475500785270864/s2built.bin EDIT: ANOTHER refinement: https://cdn.discordapp.com/attachments/202861068224036864/733506928348495972/s2built.bin EDIT: ANOTHER OTHER refinement. :P This one adds a couple more objects back and fixes Spiny's projectile: https://cdn.discordapp.com/attachments/202861068224036864/733741229124092014/s2built.bin
OK, here's a quick rundown of the code changes I made with little commentary. I'll go into more detail and arrange everything better once I write the proper guide. Look out for ;<-- comments; those show where I changed something, or point out something important. First things first, we're going to need 23 bytes of extra RAM variables: Code (Text): ; I placed these two in the free space above Hint_flag, around DMA_data_thunk Hint_water: ds.w 1 ; cleared after top screen's water effect Hint_split: ds.w 1 ; cleared after the screen has been split H_int_routine: ds.l 1 ; first H-Int routine to run after V-Int ;<-- these two need to be together H_int_schedule_split_copy: ds.w 1 ; copied during V-Int H_int_schedule_water_copy: ds.w 1 ; copied during V-Int ; I placed these at the end, between Music_to_play_2 and Demo_mode_flag Water_fullscreen_flag_P2: ds.b 1 H_int_schedule_split: ds.w 1 ;<-- these also need to be together H_int_schedule_water: ds.w 1 ;<-- H_int_jmp: ds.w 1 ; JMP command H_int_addr: ds.l 1 ; address to jump to Next, we'll look at the code changes. I'll just present them from top to bottom, in the order that they show up in the code. Edit the interrupt vector to point to one of our new RAM addresses: Code (Text): ;Vectors: dc.l System_Stack, EntryPoint, ErrorTrap, ErrorTrap; 4 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 8 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 12 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 16 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 20 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 24 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 28 dc.l H_int_jmp, ErrorTrap, V_Int, ErrorTrap; 32 ;<-- dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 36 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 40 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 44 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 48 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 52 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 56 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 60 dc.l ErrorTrap, ErrorTrap, ErrorTrap, ErrorTrap; 64 Initialize some of our new variables after the checksum test: Code (Text): ;checksum_good: lea (System_Stack).w,a6 moveq #0,d7 move.w #bytesToLcnt($200),d6 - move.l d7,(a6)+ dbf d6,- move.b (HW_Version).l,d0 andi.b #$C0,d0 move.b d0,(Graphics_Flags).w move.l #'init',(Checksum_fourcc).w ; set flag so checksum won't be run again move.w #$4EF9,(H_int_jmp).w ;<-- move.l #H_Int,(H_int_routine).w ;<-- move.l #$8ADF8ADF,(H_int_schedule_split).w ; initialize ;<-- ; loc_370: GameInit: Reset some variables during V-Int: Code (Text): V_Int: movem.l d0-a6,-(sp) tst.b (Vint_routine).w beq.w Vint_Lag - move.w (VDP_control_port).l,d0 andi.w #8,d0 beq.s - move.l #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l move.l (Vscroll_Factor).w,(VDP_data_port).l btst #6,(Graphics_Flags).w beq.s + move.w #$700,d0 - dbf d0,- ; wait here in a loop doing nothing for a while... + move.b (Vint_routine).w,d0 move.b #VintID_Lag,(Vint_routine).w move.w #1,(Hint_flag).w move.w #1,(Hint_water).w ;<-- move.w #1,(Hint_split).w ;<-- move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w ;<-- move.l (H_int_routine).w,(H_int_addr).w ;<-- andi.w #$3E,d0 move.w Vint_SwitchTbl(pc,d0.w),d0 jsr Vint_SwitchTbl(pc,d0.w) VintRet: addq.l #1,(Vint_runcount).w movem.l (sp)+,d0-a6 rte More resetting and a branch to a new two player mode handler: Code (Text): loc_4C4: tst.b (Water_flag).w beq.w Vint0_noWater tst.w (Two_player_mode).w ;<-- bne.w Vint0_water2P ;<-- move.w (VDP_control_port).l,d0 btst #6,(Graphics_Flags).w beq.s + move.w #$700,d0 - dbf d0,- ; do nothing for a while... + ; restore our H-Int schedule from previous frame move.w #1,(Hint_flag).w move.w #1,(Hint_water).w ;<-- move.w #1,(Hint_split).w ;<-- move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w ;<-- move.l (H_int_routine).w,(H_int_addr).w ;<-- stopZ80 More resetting: Code (Text): Vint0_noWater: move.w (VDP_control_port).l,d0 move.l #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l move.l (Vscroll_Factor).w,(VDP_data_port).l btst #6,(Graphics_Flags).w beq.s + move.w #$700,d0 - dbf d0,- ; do nothing for a while... + ; restore our H-Int schedule from previous frame move.w #1,(Hint_flag).w move.w #1,(Hint_water).w ;<-- move.w #1,(Hint_split).w ;<-- move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w ;<-- move.l (H_int_routine).w,(H_int_addr).w ;<-- move.w (Hint_counter_reserve).w,(VDP_control_port).l This is new code that should be placed under Vint0_noWater: Code (Text): Vint0_water2P: move.w (VDP_control_port).l,d0 move.l #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l move.l (Vscroll_Factor).w,(VDP_data_port).l btst #6,(Graphics_Flags).w beq.s .notPAL move.w #$700,d0 .loopWatiPAL: dbf d0,.loopWatiPAL ; do nothing for a while... .notPAL: ; restore our H-Int schedule from previous frame move.w #1,(Hint_flag).w move.w #1,(Hint_water).w move.w #1,(Hint_split).w move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w move.l (H_int_routine).w,(H_int_addr).w stopZ80 tst.b (Water_fullscreen_flag).w bne.s .waterPal dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM bra.s .resetScreen ; --------------------------------------------------------------------------- .waterPal: dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM .resetScreen: move.w (Hint_counter_reserve).w,(a5) move.w #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l ; Set scroll A PNT base to $C000 move.l (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM bsr.w sndDriverInput startZ80 bra.w VintRet Even more resetting: Code (Text): loc_748: move.w (Hint_counter_reserve).w,(a5) move.w #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l ; Set scroll A PNT base to $C000 dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM bsr.w ProcessDMAQueue bsr.w sndDriverInput startZ80 movem.l (Camera_RAM).w,d0-d7 movem.l d0-d7,(Camera_RAM_copy).w movem.l (Camera_X_pos_P2).w,d0-d7 movem.l d0-d7,(Camera_P2_copy).w movem.l (Scroll_flags).w,d0-d3 movem.l d0-d3,(Scroll_flags_copy).w move.l (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w move.w #1,(Hint_water).w ;<-- move.w #1,(Hint_split).w ;<-- move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w ;<-- move.l (H_int_routine).w,(H_int_addr).w ;<-- ; cmpi.b #$5C,(Hint_counter_reserve+1).w ;<-- cmpi.b #$2F,(Hint_counter_reserve+1).w ; move updates into H-Int a little earlier bhs.s Do_Updates move.b #1,(Do_Updates_in_H_int).w rts Code (Text): loc_BD6: move.w (Hint_counter_reserve).w,(a5) dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM bsr.w ProcessDMAQueue jsr (DrawLevelTitleCard).l jsr (sndDriverInput).l startZ80 movem.l (Camera_RAM).w,d0-d7 movem.l d0-d7,(Camera_RAM_copy).w movem.l (Scroll_flags).w,d0-d1 movem.l d0-d1,(Scroll_flags_copy).w move.l (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w move.w #1,(Hint_water).w ;<-- move.w #1,(Hint_split).w ;<-- move.l (H_int_schedule_split).w,(H_int_schedule_split_copy).w ;<-- move.l (H_int_routine).w,(H_int_addr).w ;<-- bsr.w ProcessDPLC rts Small adjustment to regular H-Int handler: Code (Text): H_Int: tst.w (Hint_flag).w beq.w + tst.w (Two_player_mode).w beq.w PalToCRAM H_Int_splitscreen_nowater: move.w #0,(Hint_flag).w move.w #0,(Hint_split).w ;<-- move.l a5,-(sp) move.l d0,-(sp) Add our custom H-Int handlers under PalToCRAM: Code (Text): ; ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ; Special two player mode H-Int handlers for water + splitscreen ; ; We need to add extra interrupts between our water and splitscreen effects, ; because the H-Int counter doesn't take on a new value until one interrupt ; after it was set. ; =========================================================================== ; Interrupt handler for halfway point to top screen's water level ; ; Produces no visual effect, but is needed to set up the interrupt ; counter for the splitscreen effect H_Int_water_halfway: ; set interrupt counter for halfway between water and splitscreen move.w (H_int_schedule_split_copy).w,(VDP_control_port).l ; applys after next H-Int ; set next H-Int routine move.l #H_Int_water_top_screen,(H_int_addr).w tst.b (Do_Updates_in_H_int).w beq.s .return ; only do updates if we have enough time before doing the water palette cmpi.b #$2F>>1,(Hint_counter_reserve+1).w blo.s .return clr.b (Do_Updates_in_H_int).w movem.l d0-a6,-(sp) bsr.w Do_Updates movem.l (sp)+,d0-a6 .return: rte ; =========================================================================== ; Interrupt handler for top screen's water effect H_Int_water_top_screen: tst.w (Hint_water).w beq.w .return ; temporarily disable H-Int processing move #$2700,sr move.w #0,(Hint_water).w ; set next H-Int routine move.l #H_Int_splitscreen_halfway,(H_int_addr).w ; transfer water palette for top screen movem.l a0-a1,-(sp) lea (VDP_data_port).l,a1 lea (Underwater_palette).w,a0 ; load palette from RAM move.l #vdpComm($0000,CRAM,WRITE),4(a1) ; set VDP to write to CRAM address $00 rept 32 move.l (a0)+,(a1) ; move palette to CRAM (all 64 colors at once) endm movem.l (sp)+,a0-a1 tst.b (Do_Updates_in_H_int).w beq.s .return clr.b (Do_Updates_in_H_int).w movem.l d0-a6,-(sp) bsr.w Do_Updates movem.l (sp)+,d0-a6 .return: rte ; the H-Int counter value set in H_Int_water_halfway will now apply ; =========================================================================== ; Interrupt handler for halfway point to splitscreen effect ; ; Produces no visual effect, but is needed to set up the next interrupt ; counter for the bottom screen's water effect H_Int_splitscreen_halfway: ; set interrupt counter for bottom screen's water move.w (H_int_schedule_water_copy).w,(VDP_control_port).l ; applys after next H-Int ; set next H-Int routine move.l #H_Int_splitscreen,(H_int_addr).w tst.b (Do_Updates_in_H_int).w beq.s .return clr.b (Do_Updates_in_H_int).w movem.l d0-a6,-(sp) bsr.w Do_Updates movem.l (sp)+,d0-a6 .return: rte ; =========================================================================== ; Interrupt handler for splitscreen effect (with water potentially present) ; ; Because we have to transfer the palette again, we lose one more scanline ; than we would in H_Int_splitscreen_normal H_Int_splitscreen: tst.w (Hint_split).w beq.w .return ; temporarily disable H-Int processing move.w #0,(Hint_split).w ; set next H-Int routine move.l #H_Int_water_bottom_screen,(H_int_addr).w move.l a5,-(sp) move.l d0,-(sp) .waitForVblank: move.w (VDP_control_port).l,d0 ; loop start: Make sure V_BLANK is over andi.w #4,d0 beq.s .waitForVblank ; Display disable move.w (VDP_Reg1_val).w,d0 andi.b #$BF,d0 move.w d0,(VDP_control_port).l move.w #$8200|(VRAM_Plane_A_Name_Table_2P/$400),(VDP_control_port).l ; PNT A base: $A000 move.l #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l move.l (Vscroll_Factor_P2_HInt).w,(VDP_data_port).l stopZ80 dma68kToVDP Sprite_Table_2,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM tst.b (Water_fullscreen_flag_P2).w bne.s .water dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM bra.s .restartZ80 .water: dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM .restartZ80: startZ80 .waitForVblank2: move.w (VDP_control_port).l,d0 andi.w #4,d0 beq.s .waitForVblank2 ; Display enable move.w (VDP_Reg1_val).w,d0 ori.b #$40,d0 move.w d0,(VDP_control_port).l move.l (sp)+,d0 movea.l (sp)+,a5 .return: rte ; the H-Int counter value set in H_Int_splitscreen_halfway will now apply ; =========================================================================== ; Interrupt handler for bottom screen's water effect ; ; Also disables processing of future interrupts for this frame H_Int_water_bottom_screen: tst.w (Hint_flag).w beq.s H_Int_skip ; prevent future procesing of H-Ints for this frame move #$2700,sr move.w #0,(Hint_flag).w ; set next H-Int routine move.l #H_Int_skip,(H_int_addr).w ; set next interrupt to occur off screen (i.e. disabling H-Int) move.w #$8A6F+4,(VDP_control_port).l ; next H-Int in 224 lines ; transfer water palette for bottom screen movem.l a0-a1,-(sp) lea (VDP_data_port).l,a1 lea (Underwater_palette).w,a0 ; load palette from RAM move.l #vdpComm($0000,CRAM,WRITE),4(a1) ; set VDP to write to CRAM address $00 rept 32 move.l (a0)+,(a1) ; move palette to CRAM (all 64 colors at once) endm movem.l (sp)+,a0-a1 H_Int_skip: rte Prevent water from disabling split-screen; initialization: Code (Text): Level_InitWater: move.b #1,(Water_flag).w ; move.w #0,(Two_player_mode).w ;<-- + lea (VDP_control_port).l,a6 ;(...) move.w #$8ADF,(Hint_counter_reserve).w ; H-INT every 223rd scanline move.l #H_Int,(H_int_routine).w ; reset H-Int routine ;<-- tst.w (Two_player_mode).w beq.s + Add a two-player-mode version to MoveWater: Code (Text): MoveWater: clr.b (Water_fullscreen_flag).w moveq #0,d0 cmpi.b #aquatic_ruin_zone,(Current_Zone).w ; is level ARZ? beq.s + ; if yes, branch move.b (Oscillating_Data).w,d0 lsr.w #1,d0 + tst.w (Two_player_mode).w ;<-- bne.s MoveWater2P ;<-- add.w (Water_Level_2).w,d0 Add some new code after NonWaterEffects: Code (Text): MoveWater2P: clr.b (Water_fullscreen_flag_P2).w moveq #$6B,d2 ; scanline where second screen starts add.w (Water_Level_2).w,d0 move.w d0,(Water_Level_1).w move.w (Water_Level_1).w,d0 move.w d0,d1 ; save water position for later sub.w (Camera_Y_pos).w,d0 ; calculate distance between water surface and top of screen asr.w #1,d0 ; adjust to screen position ; since interrupts take a while to occur and handle, let's not trigger ; any too close to the top of the screen cmpi.w #3,d0 ; is water less than 4 lines below the top of the screen? bge.s .notFullScreen ; if not, branch move.b #1,(Water_fullscreen_flag).w bra.s .setSplitscreenP1 .notFullScreen: ; same for the bottom of the first screen. We don't want to have a ; palette change too close to the split cmpi.w #$6B-4,d0 ; is water within 4 pixels of the split? blo.s .setWaterP1 ; if not, branch .setSplitscreenP1: ; prepare directly for splitscreen, since there is no water on the top screen move.l #H_Int_splitscreen_halfway,(H_int_routine).w ; start with the split effect move.b #$6B>>1,(Hint_counter_reserve+1).w ; do two H-INTs at 108/2 lines each bra.s .p2 .setWaterP1: ; prepare for top screen's water lsr.w #1,d0 ; adjust water lines for two interrupts sub.w d0,d2 ; lines between water and splitscreen sub.w d0,d2 lsr.w #1,d2 ; adjust split lines for two interrupts subq.w #1,d0 ; H-Int counter is 0-based move.l #H_Int_water_halfway,(H_int_routine).w ; start with the water effect move.b d0,(Hint_counter_reserve+1).w ; do two H-INTs at d0/2 lines each move.b d2,(H_int_schedule_split+1).w ; linecount for the two H-INTs leading up to splitscreen .p2: move.w d1,d0 ; restore water position sub.w (Camera_Y_pos_P2).w,d0 ; calculate distance between water surface and top of screen asr.w #1,d0 ; adjust to screen position ; let's not have another interrupt too soon after the split cmpi.w #3,d0 bge.s .notFullScreenP2 move.b #1,(Water_fullscreen_flag_P2).w bra.s .setScreenBottomP2 .notFullScreenP2: cmpi.w #$6F-4,d0 ; is water within 4 pixels of the bottom? blo.s .setWaterP2 ; if not, branch .setScreenBottomP2: move.b #$6F+4,(H_int_schedule_water+1).w ; skip bottom screen water bra.w NonWaterEffects .setWaterP2: ; prepare for bottom screen's water addq.w #4-1,d0 ; account for screen separator's height (4 px) and make counter move.b d0,(H_int_schedule_water+1).w ; lines between split screen and bottom screen water bra.w NonWaterEffects ; End of function WaterEffects Switch out a RAM address in BuildSprites_P2: Code (Text): BuildSprites_P2: ; tst.w (Hint_flag).w ; has H-int occured yet? ;<-- tst.w (Hint_split).w ; has H-int occured yet? ;<-- bne.s BuildSprites_P2 ; if not, wait lea (Sprite_Table_2).w,a2 To reiterate on the two bugs I'm aware of, Tails' screen will briefly use the above water palette if you move upwards from fully underwater to where the water surface is on screen, and Sonic's water palette change sometimes happens too late if the water surface is too close to the top of the screen. The latter happens, because Do_Updates is taking too long and delaying the H-Int that does the palette change. No idea what's causing the former. I suspect it's either in MoveWater2P or H_Int_splitscreen, but I can't find the problem. Anyway, here it is. Have fun.
With the obvious exceptions of Sky Chase and Death Egg, is it feasible to be at a point where every level in Sonic 2 is playable in 2P, now that the water problem has seemingly been worked around? That'd be really something.
If you can provide a source with your alterations above already applied, I will provide some assistance, the H-blank routines are way too unoptimise and I have a few tricks up my sleave.
It also depends on VRAM. As shown with the CPZ conversion, it is theoretically possible to convert the tiles to split screen, but to fit it all in, some sacrifices had to be made. It also depends on how well certain obstacles mesh with two screens. CPZ still has an issue where dying can send you into the auto spin tubes for some reason, while the oil in OOZ doesn't display. The water conversion is looking hopeful. kudos to MoDule, though I'll need to translate it to Xenowhirl (or move my ARZ edits to a GitHub disassembly) before I can use it myself. By the way, I keep hearing about freeing up RAM to add these variables, what are the usual hot spots you guys fiddle with to get more room to put them in? (keeping in mind I'm using a ROM that outside the level conversions is otherwise unaltered and has no new flags.)
Quick minor update, but I THINK that you can fix the issue with Rexon with the same trick as the airlifts, ie. set Long Distance and Remember State on. It should then only load once (though it won't respawn even after being destroyed now). Also advisory tip, add one blank tile to the end of Rexon's art (don't worry there's a blank area in the VRAM for it) so that his fireball renders properly in splitscreen.
Okay so confirmed you CAN just use the same trick as with the airlifts to fix Rexxon, activate the Remember State and Long Distance flags in SonLVL (weird, I was certain I tried that before). Rexxon can still make the level kinda laggy though so be warned. Also I've fixed a behaviour problem with Sol. Normally Sol only shoots his fireballs at Sonic, edit this line in Sol's routine to make him fire at Tails as well: Code (Text): loc_371DC: move.w (MainCharacter+x_pos).w,d0 sub.w x_pos(a0),d0 bcc.s loc_371E8 neg.w d0 loc_371E8: cmpi.w #$A0,d0 bcc.s loc_371DC_Tails move.w (MainCharacter+y_pos).w,d0 sub.w y_pos(a0),d0 bcc.s loc_371FA neg.w d0 loc_371FA: cmpi.w #$50,d0 bcc.s loc_371DC_Tails tst.w (Debug_placement_mode).w bne.s loc_371DC_Tails move.b #1,anim(a0) loc_371DC_Tails: tst.w (Two_player_mode).w beq.w loc_3720C move.w (Sidekick+x_pos).w,d0 sub.w x_pos(a0),d0 bcc.s loc_371E8_Tails neg.w d0 loc_371E8_Tails: cmpi.w #$A0,d0 bcc.s loc_3720C move.w (Sidekick+y_pos).w,d0 sub.w y_pos(a0),d0 bcc.s loc_371FA_Tails neg.w d0 loc_371FA_Tails: cmpi.w #$50,d0 bcc.s loc_3720C tst.w (Debug_placement_mode).w bne.s loc_3720C move.b #1,anim(a0) loc_3720C: bsr.w JmpTo26_ObjectMove lea (off_372D2).l,a1 bsr.w JmpTo25_AnimateSprite andi.b #3,mapping_frame(a0) bra.w JmpTo39_MarkObjGone ; =========================================================================== Here's a demonstration ROM with Rexxon and Sol fixed: https://cdn.discordapp.com/attachments/202861068224036864/734845386534944778/s2built.bin Special thanx to D.A. Garden for bug finding.
Performance with Rexon on screen didn't seem too terrible to me, but maybe you can get better results by reducing its neck... balls from 4 to 3?
Well I'll leave it to people who use the tutorial to manage the objects. I suspect people who also ported the S3K priority/object managers might have a smoother experience. By the way, I think I've gotten CPZ as good as I can right now (I've even fixed some bugs like the autospin tubes sometimes cancelling out respawning and the Spiny's only aiming at Sonic). https://cdn.discordapp.com/attachments/202861068224036864/735128680728231946/s2built.bin Besides the neccessary simplified art, the remaining issue right now is the background scrolling, which I can just not get running properly in split screen. I'm gonna have to ask if anyone here is experienced with editing the scroll routines. If I can get that working, I might try and manage this all into another tutorial.
For the backgrounds you're going to be limited to 512x512 pixels, due to the way the graphics and scrolling are set up. The background tiles are shared between the two player's screens, so we can't do any tile loading. This works for the 2P zones, because all their backgrounds fit within these constraints (MCZ is an exact fit). For the non-2P zones (except HTZ and OOZ) you're going to have to shrink the backgrounds down a little for them to fit. ARZ is going to be the biggest problem, since it's three times as tall as the limit. As for the scrolling itself, I've achieved some minor successes so far. CPZ and ARZ scroll correctly for the top screen. The bottom screens are still giving me trouble, but I'll get it eventually. In other news, MarkeyJester has been hard at work optimizing and fixing the H-Int routines. The bugs I mentioned earlier are now gone thanks to him. Currently he is working on optimizing the tile remapping for interlace mode compatibility.
Ah that makes sense, I noticed even with just the one screen active there's some cutoff with CPZ's background. And yeah, I'm still flummoxed how to make two player's screen load properly besides just linking to one of the existing routines. If you want, I can give you Zeta_Null's tile fixes for CPZ so you have a clearer view. And you guys are tile mapping ARZ as well? It sounds like you two are pretty much doing ARZ yourselves. :P
Roughly speaking, you need to run the zone's scrolling code twice, but with only half as many lines, each. First, you calculate your background camera position, then use that to determine how far each section of the background has moved. Then you need to take that information and turn it into values that you can send to the H-scroll table in the VDP. Most zones actually do this in a semi-standardized way, where they prepare an array of scroll values (TempArray_LayerDef), then run some code that takes a table of height values and compares them against the current camera position until the first visible background section is found. Then it just needs to fill in a screen's worth of scroll values that it takes from TempArray_LayerDef. MCZ does this, for instance, and it shows how to do it for two screens. CNZ also uses this system, but it messes up the bottom screen's scrolling. I'll post a fix for that some other time. As for which tileset Marky's working on right now, it is indeed ARZ, which I found out just now.