The ICARUS Sprite HUD

chronosv2

New member
Hey there folks!
A couple days ago I started work on a sprite HUD for NESMaker. When 4.1.0 hits, if you choose to use scrolling you'll need to use some advanced tricks to make use of the tile HUD, and I have no familiarity with some of those topics so I decided to design a HUD inspired by Kid Icarus -- a "score" or "currency" panel and an expandable HP Bar.
The first version was simple enough -- it worked and had expandable max HP, but it was a little code-bulky with the score display.
The second version cleaned up the score display code to make it a little smaller, though perhaps a little slower since it's now done in a loop with comparisons -- this method lets you have a score value up to 7 digits (8 if you turn off the icon), though obviously a display that large will preclude other sprites from drawing on that line. Given the dynamic nature of the script I'm not sure how I'd implement a good flicker, so it's best to keep your numbers small. Kid Icarus used 3 digits for hearts and 5 HP Bar segments as a maximum for a reason. :)

Then we're on to this, the current version. I made more modifications, and also designed the health bar to allow different levels of granularity. The bar can now be designed to work with 8 HP per sprite, 4 HP per sprite or 2 HP per sprite. I've also used some of the ASM6 assembler's features to implement conditional compiling. Want the HP bar but no currency display? Change a 1 to a 0 and it's gone. Want the currency display without the life bar? Just as easy.

You can also set the X,Y coordinates of both components so you aren't stuck with the "Kid Icarus" format I chose to use for inspiration if you don't want to.
If you have any questions or criticisms let me know!
 

Attachments

  • ICARUS_SpriteHUD.zip
    6 KB · Views: 101
  • 2step_000.png
    2step_000.png
    1.9 KB · Views: 1,562
  • 4step_000.png
    4step_000.png
    1.8 KB · Views: 1,561
  • 8step_001.png
    8step_001.png
    1.8 KB · Views: 1,561

chronosv2

New member
Here's the code in raw ASM form for those that would rather not touch the zip file:
Code:
;; ICARUS Sprite HUD
;; version 1.0.2
;; by Kevin Skeen (chronosv2)

;;Constants -- These will eventually be modifiable in-engine via a plugin.
;;Configuration Constants -- These control conditional assembly. Anything turned off will NOT be part of your assembled code.
NUM_HPBAR_STEPS = 		$08		;;The number of steps in each HP sprite Valid values are #$08, #$04 and #$02.
SPRITE_HUD_DRAW_HPBAR = 1		;;Set this to 0 to skip drawing HP Bar.
SPRITE_HUD_DRAW_SCORE = 1		;;Set this to 0 to skip drawing Score.
SPRITE_HUD_DRAW_ICON = 	1		;;Set this to 0 to skip drawing Score Icon. (Automatically skipped if SPRITE_HUD_DRAW_SCORE = 0)

;;Set these to the variable names you want the Sprite HUD to use. Be sure to leave the '#' because these are pointing to variable addresses!
HP_VARIABLE = #playerHP
HP_MAX_VARIABLE = #playerHPMax
CURRENCY_VARIABLE = #playerScore

;;Conditional Assembly -- these IF statements are evaluated DURING the assembly process. Only the code corresponding to a
;;  condition that returns true will be assembled into your final code.
;;This code handles how many steps your HP Bar has.
IF NUM_HPBAR_STEPS == #$08
	SPRITE_HUD_HPBAR_EMPTY = $67	;;The empty HP bar on your GameObject Sprite Sheet (each following sprite is filled 1 pixel more)
ELSEIF NUM_HPBAR_STEPS == #$04
	SPRITE_HUD_HPBAR_EMPTY = $6B	;;The empty HP bar on your GameObject Sprite Sheet (each following sprite is filled 2 pixel more)
ELSEIF NUM_HPBAR_STEPS == #$02
	SPRITE_HUD_HPBAR_EMPTY = $6D	;;The empty HP bar on your GameObject Sprite Sheet (each following sprite is filled 4 pixel more)
ELSEIF NUM_HPBAR_STEPS == #$01
	SPRITE_HUD_HPBAR_EMPTY = $6E	;;The empty HP bar on your GameObject Sprite Sheet (each following sprite is filled 8 pixel more)
ELSE
	ERROR "Invalid NUM_HPBAR_STEPS value. Valid values are #$02, #$04, #$08."
ENDIF
SPRITE_HUD_SCORE_ICON = $66		;;The icon for "hearts" (currency) on your sprite sheet.
SPRITE_HUD_NUMBER_0 = $76		;;The number 0 on your GameObject Sprite Sheet - all numbers must be in order (012..789)
SPRITE_HUD_START_X = $08		;;The starting X coordinate (in pixels) for the HUD
SPRITE_HUD_HPBAR_START_Y = $10	;;The starting Y coordinate (in pixels) for HP
SPRITE_HUD_SCORE_START_X = $08  ;;The starting X coordinate (in pixels) for "hearts"
SPRITE_HUD_SCORE_START_Y = $08	;;The starting Y coordinate (in pixels) for "hearts"
SPRITE_HUD_PALETTE = %00000001	;;The sprite attributes -- the last 2 bits represent the palette to use
									;;	XXXXXX00 = Player Palette 1
									;;	XXXXXX01 = Player Palette 2
									;;	XXXXXX10 = Monster Palette 1
									;;	XXXXXX11 = Monster Palette 2
CURRENCY_NUMBER_OF_DIGITS = $03 ;;The number of digits in the Currency display

IF SPRITE_HUD_DRAW_HPBAR == 1
	LDA HP_MAX_VARIABLE
	IF NUM_HPBAR_STEPS == #$08
		LSR ;;\
		LSR ;;-- Divide by 8
		LSR ;;/
	ELSEIF NUM_HPBAR_STEPS == #$04
		LSR ;;\
		LSR ;;-- Divide by 4
	ELSEIF NUM_HPBAR_STEPS == #$02
		LSR ;;-- Divide by 2
	ELSE
		ERROR "Invalid NUM_HPBAR_STEPS value. Valid values are #$02, #$04, #$08."
	ENDIF
	TAX ;;Store in X

	LDA HP_VARIABLE	;;We need the player's current HP stored in a temp variable so we can do math on it.
	STA temp 		;;So we store it in temp1
	LDA #SPRITE_HUD_START_X	;;We need the starting X-coordinate stored in a variable too.
	STA temp2 		;;So let's use temp2 for that.

	;; This is the main loop.
	SpriteHUDDrawLoop:
		CPX #$00							;;We have the number of bars we need to draw in X. If this is 0, we're done.
		BEQ endPredraw				;;So BRANCH to endPredraw
		LDA temp 							;;Load what's left of player HP into Accumulator
		BMI emptyBar 					;;If the number is negative then we need an empty bar.
		CMP #NUM_HPBAR_STEPS 	;;If we have NUM_HPBAR_STEPS HP or more left, we need a full bar.
		BCS fullBar 					;;So BRANCH to fullBar
		JSR drawPartialBar 		;;If none of these are true, jump to the drawPartialBar subroutine
		JMP afterSpriteCalc 	;;Jump to the end of the loop where we do some housekeeping on HP temp and X-coord temp2.
		fullBar:							;;We need to draw a full bar, so
		JSR drawFullBar				;;Jump to drawFullBar Subroutine
		JMP afterSpriteCalc 	;;Jump to housekeeping
		emptyBar:							;;We need to draw an empty bar, so
		JSR drawEmptyBar			;;Jump to drawEmptyBar Subroutine
		afterSpriteCalc: 			;;Housekeeping starts here
		;;We just drew NUM_HPBAR_STEPS HP, so we need to subtract NUM_HPBAR_STEPS from temp so we can calculate for the next bar.
		LDA temp 							;; Load temp into Accumulator for this.
		SEC 									;;Don't forget to set Carry.
		SBC #NUM_HPBAR_STEPS	;;Now we do the subtraction
		STA temp 							;;And store it back in temp.
		LDA temp2 						;;Now we load the X-coordinate
		CLC 									;;Clear carry (I forgot to do this once and got a weird 1px space between bar 1 and 2! This is why you CLC!)
		ADC #$08 							;;Add the width of a bar (8)
		STA temp2 						;;Store the number back in temp2.
		DEX 									;;We just drew a bar so subtract 1 from X.
	JMP SpriteHUDDrawLoop		;;And start again.
	endPredraw:

	JMP afterHPPredraw 			;;We need to declare these subroutines but we don't want to run them yet, so jump over them.
		drawEmptyBar: 				;;Draws an empty bar using the usual Predraw sprite macro template.
		DrawSprite temp2, #SPRITE_HUD_HPBAR_START_Y, #SPRITE_HUD_HPBAR_EMPTY, #SPRITE_HUD_PALETTE, spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
	RTS	;;This is a SUBROUTINE so RTS sends us back to where we came from -- one command after JSR drawEmptyBar

	drawFullBar: ;;Draws an empty bar using the usual Predraw sprite macro template.
		LDA #SPRITE_HUD_HPBAR_EMPTY ;;We need the address of the empty bar sprite
		CLC 						;;Don't forget to clear carry like I always do!
		ADC #NUM_HPBAR_STEPS		;;Add NUM_HPBAR_STEPS (the number of increments in the bar)
		STA temp3 					;;Store it in a variable so we can feed it to the sprite draw macro.
		DrawSprite temp2, #SPRITE_HUD_HPBAR_START_Y, temp3, #SPRITE_HUD_PALETTE, spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
	RTS	;;This is a SUBROUTINE so RTS sends us back to where we came from -- one command after JSR drawFullBar

	drawPartialBar:	;;Draws a partial bar
		LDA HP_VARIABLE 	;;We need the player's actual HP, not the number we're playing with in temp.
		IF NUM_HPBAR_STEPS == #$08
			AND #%00000111  ;;We only care if the number is between 0 and 7 so and out the rest.
		ELSEIF NUM_HPBAR_STEPS == #$04
			AND #%00000011  ;;We only care if the number is between 0 and 7 so and out the rest.
		ELSEIF NUM_HPBAR_STEPS == #$02
			AND #%00000001  ;;We only care if the number is between 0 and 7 so and out the rest.
		ELSE
			ERROR "Invalid NUM_HPBAR_STEPS value. Valid values are #$02, #$04, #$08."
		ENDIF
		;;D #%00000111  ;;We only care if the number is between 0 and 7 so and out the rest.
		CLC 			;;You know the drill. Clear carry before an add!
		ADC #SPRITE_HUD_HPBAR_EMPTY ;;Add the memory address of the empty bar.
		STA temp3 					;;Store it somewhere so we can use it in the sprite draw macro.
		DrawSprite temp2, #SPRITE_HUD_HPBAR_START_Y, temp3, #SPRITE_HUD_PALETTE, spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
	RTS ;;This is a SUBROUTINE so RTS sends us back to where we came from -- one command after JSR drawPartialBar
ENDIF

afterHPPredraw:	;;Done doing the health bar, now for the score/currency.
IF SPRITE_HUD_DRAW_SCORE == 1				
	LDA #SPRITE_HUD_SCORE_START_X ;;We need the starting X-coordinate for currency.
	STA temp2 ;;Store it in Temp2.
	;DEX
	IF SPRITE_HUD_DRAW_ICON == 1
		DrawSprite temp2, #SPRITE_HUD_SCORE_START_Y, #SPRITE_HUD_SCORE_ICON, #SPRITE_HUD_PALETTE, spriteOffset ;;Draw the ICON.
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
		inc spriteOffset
		LDA temp2 ;;We need to add the width of a number to temp2
		CLC 	  ;;So clear carry
		ADC #$08  ;;Add 8
		STA temp2 ;;Store the number.
	ENDIF
	LDX #CURRENCY_NUMBER_OF_DIGITS
	currencyLoop:
	DEX
	LDA CURRENCY_VARIABLE,x ;;We need to draw a digit. Grab each one in descending order (Thousands, hundreds, tens, etc)
	CLC 			  ;;Clear carry
	ADC #SPRITE_HUD_NUMBER_0 ;;Add the address of our "0" number and we have the number ready to draw!
	STA temp 				 ;;Just gotta' store it in a variable for the DrawSprite macro.
	DrawSprite temp2, #SPRITE_HUD_SCORE_START_Y, temp, #SPRITE_HUD_PALETTE, spriteOffset
	inc spriteOffset
	inc spriteOffset
	inc spriteOffset
	inc spriteOffset
	LDA temp2 ;;Increasing X by 8 again.
	CLC
	ADC #$08
	STA temp2
	CPX #$00
	BEQ doneCurrency
	JMP currencyLoop
	doneCurrency:
ENDIF
 

dale_coop

Moderator
Staff member
Oh wow! That is a complex code. Never thought there was conditional compilimg functionalities in the ASM.
But that makes sense.
Thank you, chronosv2 <3
 

chronosv2

New member
Thank you!
It was fun to work on. Technically the first set of IF/ELSEIF/ENDIF is unnecessary for end users (I was using one script to compile 3 ROMs so I needed the conditional setting of SPRITE_HUD_HPBAR_EMPTY) but I left it in because it's not adding any bloat. Besides, the error check can still come in handy if users choose a step that wouldn't work well. Though I guess technically you could do strange numbers of HP per block so long as you used the sprite space for it.

Oh well, advanced users can remove the check and set up strange sprite values like 7 HP per block. :p

Edit well after the fact: You can't actually use strange numbers like 3 or 5 HP per block in this engine -- the division I use to figure out how many bars to draw wouldn't work very well with numbers like that!
 
Top Bottom