S2_3K HUD Manager - RetroKoH/S1Fixed GitHub Wiki

(Original guide by RetroKoH)
Source: See commits
Original Commit: 9faa731

All 3 classic Sonic games make use of a Heads-Up Display (HUD) in order to inform the player of their Score, Time Elapsed, Ring Count, and Lives Count, all in real time, but did you know that Sonic 1 does this in a different manner than Sonic 2 or Sonic 3K? Sonic 1 uses a dedicated object to set up and manage the HUD, which occupies a slot in object RAM that could be used for something else, if we wanted/needed. It also takes up an object ID, which you probably don't want if you plan on adding many new objects to the game. First, we are going to remove the HUD object, then we will set up a new HUD builder function that will run in the game's sprite builder.

Part 1: Removing the HUD Object

The file for the HUD object is located at _incObj/21 HUD.asm. We can simply delete this file, then we need to remove the include directive that tells the assembler to include the file. Open sonic.asm, search for, and remove this line:

	include	"_incObj/21 HUD.asm"

Deleting the file also deletes the pointer to object $21, which means we now need to edit the object pointer table. Go to _inc/Object Pointers.asm. You should see this array of pointers:

; ---------------------------------------------------------------------------
; Object pointers
; ---------------------------------------------------------------------------
ptr_SonicPlayer:		dc.l SonicPlayer	; $01
ptr_Obj02:			dc.l NullObject
ptr_PathSwapper:		dc.l PathSwapper
ptr_Obj04:			dc.l NullObject
ptr_Obj05:			dc.l NullObject
ptr_Obj06:			dc.l NullObject
ptr_Obj07:			dc.l NullObject
ptr_Splash:			dc.l Splash		; $08
; ...

You'll see that every pointer on the left points to the address of object code on the right. We need to find ptr_HUD. Ah, here it is... Let's replace it like so:

-ptr_HUD:			dc.l HUD
+ptr_Obj21:			dc.l NullObject

Now we need to change the ID constant, because we renamed the pointer. Scroll to the bottom of the file and find this line. We are going to alter it slightly:

-id_HUD:			equ ((ptr_HUD-Obj_Index)/4)+1
+id_Obj21:			equ ((id_Obj21-Obj_Index)/4)+1

Finally, we need to remove the instructions that loaded the old HUD object into object RAM. In sonic.asm, go to Level_SkipTtlCard, and find move.b #id_SonicPlayer,(v_player).w. This loads Sonic into the game. After that instruction, you'll see three more lines. We can remove these:

	tst.w	(f_demo).w
	bmi.s	Level_ChkDebug
	move.b	#id_HUD,(v_hud).w ; load HUD object

Part 2: Adding a HUD Builder

Now that the object is gone, we don't have a HUD. Let's fix that. Here is the HUD Builder. Take this and save it in _inc/BuildHUD.asm:

; ---------------------------------------------------------------------------
; Subroutine to draw the HUD
; ---------------------------------------------------------------------------

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

; loc_17178:
BuildHUD:
		moveq	#0,d1
		tst.w	(v_rings).w
		beq.s	.norings				; blink ring counter if 0
		cmpi.b	#id_Special,(v_gamemode).w		; is this the Special Stage?
		beq.s	.goahead				; if yes, branch ahead
		btst	#3,(v_framebyte).w
		bne.s	.skip					; only blink on certain frames
		cmpi.b	#9,(v_timemin).w			; have 9 minutes elapsed?
		bne.s	.skip					; if not, branch
		addq.w	#2,d1					; set mapping frame time counter blink
.skip:
		bra.s	.goahead
.norings:
		moveq	#0,d1
		btst	#3,(v_framebyte).w
		bne.s	.skip					; only blink on certain frames
		addq.w	#1,d1					; set mapping frame for ring count blink

		cmpi.b	#9,(v_timemin).w			; have 9 minutes elapsed?
		bne.s	.skip					; if not, branch
		addq.w	#2,d1					; set mapping frame for double blink
.goahead:
		move.w	#128+16,d3				; set X pos
		move.w	#128+136,d2				; set Y pos
		lea	(Map_HUD).l,a1
		movea.w	#make_art_tile(ArtTile_HUD,0,0),a3	; set art tile and flags

		add.w	d1,d1
		adda.w	(a1,d1.w),a1				; load frame
		move.b	(a1)+,d1				; load # of pieces in frame
		subq.w	#1,d1
		bmi.s	.end
		bsr.w	BuildSpr_Normal				; draw frame
.end:
		rts
; End of function BuildHUD

It's fairly similar to what the old HUD object did. You'll notice that all this does is draw the HUD itself. None of the actual updating is handled here, nor was it handled by the object itself. That makes our work a lot easier! Now, we need to do a few more things to get this up and running. First and foremost is including the file into the project. I decided the best place to put this was underneath the BuildSprites function. So, search for this line:

+	include	"_inc/BuildHUD.asm"
	include	"_incObj/sub ChkObjectVisible.asm"

As you can see, we are going to place BuildHUD right above it. Now, to actually call the HUD builder. We're going to call it in the beginning of BuildSprites. You can find BuildSprites in sonic.asm in the standard Sonic 1 disassembly. We're gonna modify it at the start to call the HUD builder:

BuildSprites:
	lea	(v_spritetablebuffer).w,a2	; set address for sprite table
	moveq	#0,d5
+	tst.b	(f_levelstarted).w		; new engine flag
+	beq.s	.noHUD
+	bsr.w	BuildHUD					

+.noHUD:
	lea	(v_spritequeue).w,a4
	moveq	#7,d7

Notice the first line we added, this is a new engine flag. What's the purpose of this? This flag is set when the level starts (hence the name) and, when set, allows the HUD to be rendered on-screen. If you are also running the S2/3K Rings Manager, this flag will also allow the rings to be drawn as well. This is important, because without this flag, rings and the HUD can be drawn at times when we don't want it to be. Before we go any further, let's add the flag to our variables list. Open variables.asm and find one to use. For S1Fixed, I looked for somewhere in RAM that had at least 2 unused bytes, so that I could use the following byte along with this one, for another mod. You don't need to do it this way, though I do recommend it. As an example, here's what I did. In stock Sonic 1, this is located after v_ssrotate:

v_ssangle:		ds.w	1		; Special Stage angle
v_ssrotate:		ds.w	1		; Special Stage rotation speed
-			ds.b	$C		; unused
+			ds.b	$A		; unused
+f_levelstarted:	ds.w	1		; level start flag (for drawing non-objects) -- RetroKoH S2 Rings Manager
					; Second byte reserved for now

There were $B (11) unused bytes of RAM. I changed the $B to a 9, because I am now using two bytes. After that, I put in our new variable name, followed by a declaration of a word (two bytes). The second of these two bytes is still unused, but is reserved for future use.

Now we need to have the flag set and cleared where necessary, and we're all done!

Part 3: Setting the Level Start flag

Now, there are a few specific places where we need to set and clear our new flag. First, we are going to make sure the flag is cleared at the title screen, so the HUD doesn't appear at the title screen. Go to GM_Title and find bsr.w ClearScreen. After that line, insert this:

	bsr.w	ClearScreen
+	clr.b	(f_levelstarted).w	; clear flag so HUD doesn't appear

There is another point later on where we want f_levelstarted cleared, but if you put the flag somewhere in RAM similar to where I instructed, this is already done for you. Go to Level_ClrRam and take a look:

	clearRAM v_objspace,v_objend
	clearRAM v_misc_variables,v_misc_variables_end
	clearRAM v_levelvariables,v_levelvariables_end		; f_levelstarted should clear here
	clearRAM v_timingandscreenvariables,v_timingandscreenvariables_end

Go back into Variables.asm and make sure that f_levelstarted is located between v_levelvariables and v_levelvariables_end. If it is, then it will be cleared here, as it should be.

If you put it elsewhere in RAM, you can move it to somewhere within that boundary. If for some reason you can't or don't want to, then we can take the Sonic 2 approach, and clear it a little bit later. Go to Level_ChkWater and add it here:

Level_ChkWater:
	clr.w	(v_jpadhold2).w
	clr.w	(v_jpadhold1).w
+	clr.b	(f_levelstarted).w
	cmpi.b	#id_LZ,(v_zone).w	; is level LZ?
;...

Now, let's actually set our flag when our game starts.

Level_StartGame:
+	tst.w	(f_demo).w
+	bmi.s	.demo			; skip loading the HUD during the credits
+	move.b	#1,(f_levelstarted).w
+.demo:
	bclr	#7,(v_gamemode).w	; subtract $80 from mode to end pre-level stuff

This will make sure that the HUD appears for gameplay and Title Screen demos, but not for Credits (akin to what happened with the HUD prior to this mod). If you also decide to use a Rings Manager, this method will not allow rings to appear during credits sequences, but we will resolve this in that guide. For now, this works.

Next, we'll want the HUD variable to clear at the Continue Screen. Find GM_Continue and place this instruction after the ClearScreen instruction:

	clr.b	(f_levelstarted).w

Do the same exact thing at GM_Credits. Place it after the ClearScreen instruction. This prevents the HUD from appearing during Credits sequences.

That should do it, though there is an element of pop-in with the HUD (There always has been, to some extent). The Scrolling HUD mod works around this, and it actually makes use of that second RAM byte I alluded to when we added f_levelstarted. I recommend using this feature. It also looks nice!