[4.5.6] Combating Sprite Limits - Forcing Blank Sprites to Not Draw

CutterCross

Active member
We all (hopefully) know that the NES can only draw 8 hardware sprites per scanline, and only 64 hardware sprites total. But working with NESmaker's Game Objects or Monster Objects can kind of suck because all sprites, even if they're completely blank and are mainly used to help with 16x16 offsets when placing them on a screen, will draw. And blank sprites still eat up one of those precious slots in OAM, and on our 8 sprites per scanline limitation.

Here's an example:

leS07cY.gif



Really only 7 sprites display actual graphics at any given time (not counting the player object), but those blank sprites for positioning are still taking up a lot of space!



So let's change that.



In doDrawSprites.asm, there's this section with the label doDrawSpritesLoop. (Should be around line 253.) It looks like this:

Code:
	doDrawSpritesLoop:
	
			LDA (temp16),y
			clc
			ADC temp3
			STA tempC ;; the calculated table position, with offest.
						;; tempC becomes the "tile to draw".
			INY
			LDA (temp16),y 
			STA tempD ;; the next value is the attribute to draw.
			INY 		;; increasing again sets us up for the next sprite.
						;; now we can use tempA-D to draw our sprite using the macro.
						
								
			DrawSprite tempA, tempB, tempC, tempD


What we're going to do is add a conditional check so if the current sprite to be drawn is a certain tile index (which would be our blank tile), we skip drawing the sprite entirely.

So let's say our blank tile is tile number #$7F in our Game Objects tileset. All we need to do is check against tempC, which holds the tile index, and see if it's #$7F. If it is #$7F, branch around the DrawSprite macro.

Code:
	doDrawSpritesLoop:
	
			LDA (temp16),y
			clc
			ADC temp3
			STA tempC ;; the calculated table position, with offest.
						;; tempC becomes the "tile to draw".
			INY
			LDA (temp16),y 
			STA tempD ;; the next value is the attribute to draw.
			INY 		;; increasing again sets us up for the next sprite.
						;; now we can use tempA-D to draw our sprite using the macro.
			
			LDA tempC
			CMP #$7F
			BEQ +
								
			DrawSprite tempA, tempB, tempC, tempD
			+


And voila, now those blank sprites don't render anymore! And our OAM table is looking much cleaner now!

BgKWht5.gif



Just a bit of extra info: If your blank tile index is #$00 you don't even need the CMP. Hope you'll find this easy modification useful!
 

mouse spirit

Well-known member
I cant wait to start using 4.5. Especially for stuff like this and
everything else people are helping create for it.Well done cutter.
 

AllDarnDavey

Active member
Thanks for this, this is such a great idea!
I just helped mouse turn off weapon spites that go off-screen to avoid them wrapping around to the other side, and the idea of doing something similar to skip blank sprites never even occurred to me, genius.
 

PasseGaming

Active member
Okay, quick question just to make sure I have this right. The nesmaker still draws all the blank spaces on our game object tiles. By using this code for each 8x8 blank space it will clean up the game some allowing it to run more smoothly?

or is this to remove the transparent pixels around the sprite to speed it up some?
 

CutterCross

Active member
PasseGaming said:
Okay, quick question just to make sure I have this right. The nesmaker still draws all the blank spaces on our game object tiles. By using this code for each 8x8 blank space it will clean up the game some allowing it to run more smoothly?

or is this to remove the transparent pixels around the sprite to speed it up some?

This is less about making the game run more smoothly and more about making room for additional hardware sprites to be drawn when using NESmaker's object system. The NES can only render 64 individual hardware sprites on screen, and only 8 hardware sprites can be rendered per scanline. One of the problems with using NESmaker's object system is that you may have a bunch of blank tiles (IE tiles with no actual graphics on them) to get around positioning of object placement on the screen painter, or just otherwise have a bunch of blank tiles in your object animations. Those all still draw by default, which cuts into your sprite limits. With this modification, blank sprites will never draw, thus saving you more sprites to use on-screen and per scanline.
 

PasseGaming

Active member
Oh, so this pertains to used player object sprites that have blank 8x8 spaces. Okay, makes sense. I was confused there thinking that unused ties or player objects were somehow being drawn. Luckily I don't have any blank spaces within my player object and monster sprites.
 

vanderblade

Active member
I know it's a hassle, Cutter, but like Mouse Spirit asked, any chance you could provide instructions on how to do this in 4.1.5? Thanks!
 

AllDarnDavey

Active member
vanderblade said:
I know it's a hassle, Cutter, but like Mouse Spirit asked, any chance you could provide instructions on how to do this in 4.1.5? Thanks!

Because I thought it would be a lot easier to convert then it ended up being, and because I apparently like avoiding working on my own project, I got a version of this working in 4.1.5 Just give a name like DrawSprites_blankCull.asm and assign it in place of your original DrawSprites:
Code:
HandleDrawingSprites:
	;;; HANDLE HURT FLICKER
	;;;;; if you don't want your objects to flicker when hurt, delete this block
	;;; also look down for another block when drawing y value
	LDA Object_status,x
	BNE objectNotDeactivated
	RTS
objectNotDeactivated:
	
	LDA Object_status,x
	AND #HURT_STATUS_MASK
	BEQ ignoreHurtFlicker
	LDA vBlankTimer
	AND #%00000001
	BEQ ignoreHurtFlicker
	LDA #$01
	STA DrawFlags ;; if DrawFlags is 1, do flicker.  If 0, do not.
	JMP gotFlickerValue
ignoreHurtFlicker:
	LDA #$00
	STA DrawFlags
gotFlickerValue:
	;;;;;;;;;;;;;;;;;;;;;;;;;;;
	;;; END HANDLE HURT FLICKER
	
	
	;; still at the *object* level right now.
	LDA Object_type,x
	CMP #$10
	BCC dontAddTilesetOffset
	;; this offset is added for monsters.
	LDA #$80
	STA tileset_offset
	JMP gotTilesetOffset
dontAddTilesetOffset:
	LDA #$00
	STA tileset_offset
gotTilesetOffset:
	LDA gameHandler
	AND #%10000000 ;; is updating sprites turned on
	BNE skipGettingStartingOffset
	RTS ;; draw no sprites
skipGettingStartingOffset:
	LDY Object_type,x
	LDA ObjectSize,y
	AND #%00000111
	STA tempy ;; is good for height
	LDA ObjectSize,y
	LSR
	LSR
	LSR 
	STA tempx ;; is good for width.
	STA tempz ;; double up on this one, because we'll need to restore it
			;; and won't have access to tempx anymore
			
	;; we also need to know how far beyond our offset we should be.
	;; this can be calculated by the number of ((tiles x 2) x animation offset).  We can find it with a quick loop.	
	LDA #$00
	STA sprite_tileOffset
	LDA Object_animation_frame,x
	BEQ noNeedToFactorForOffset ;; if it is zero, no need for an offset.
	STA temp2

	LDY Object_type,x
GetFrameOffset:
	LDA Object_total_sprites,y
	ASL
	CLC
	ADC sprite_tileOffset
	STA sprite_tileOffset	
	DEC temp2
	LDA temp2
	BNE GetFrameOffset

;;;now temp should contain the offset.
noNeedToFactorForOffset:

	
	
	
	;; we want to cycle forward through the sprites if odd frame, backwards through the sprites if even frame?
	;; first, we need to determine where our starting position is:
	LDA Object_movement,x
	AND #%00000111 ;; this is where we'll store direction
	STA temp
	;;;;;multiply that by 8 (directions) to get the starting point of pointers.
	;; so we have to do a quick look up of the anim speed table.
	;; bits 7654 represent the offset, while 3210 represent the animation speed.
	LDA Object_animation_offset_speed,x
	LSR
	AND #%11111000
	CLC 
	ADC temp
	;;; effectively, we're shifting the offset 4 bits,
	;; but then multiplying it by 8 since each "action offset" is 8 values (one for each direction)
	;; so this is a shortcut.  
	
	TAY
	
	
	
	
	LDA Object_x_hi,x
	SEC
	SBC xScroll
	STA spriteDrawLeft
	
	LDA Object_table_lo_lo,x
	STA temp16
	LDA Object_table_lo_hi,x
	STA temp16+1
	LDA (temp16),y

	STA sprite_pointer
	
	LDA Object_table_hi_lo,x
	STA temp16
	LDA Object_table_hi_hi,x
	STA temp16+1
	LDA (temp16),y
	STA sprite_pointer+1
	

	
	;;;now we have the right grouping for the right direction for the right object.

	;; now, should be able to run right through the values pretty easily.
	;; we're going to pick up wherever the last object left off drawing
	LDA Object_x_hi,x
	SEC
	SBC xScroll
	STA temp1 ;; this will be used to keep track of horizontal placement, increasing by 8 and 
				;; decreaseing tempx to see if we're out of horizontal spacing.
	LDA Object_y_hi,x
	sec
	sbc #$01
	STA temp2	;; this will be used to keep track of vertical placement, increasing by 8 and
				;; decreasing tempy to see if we're out of vertical spacing (ie-done).
	
;;;; FIND OUT IF THIS SPRITE SHOULD BE DRAWN BEHIND THE BACKGROUND	
	 LDA Object_state_flags,x
	AND #%00100000
	 STA temp3  ;; holds whether or not this object's sprite is drawn behind backgrounds.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	TXA
	PHA

	LDY #$00
	LDA (sprite_pointer),y
	STA objectFrameCount
	
	LDX spriteOffset 
	LDA sprite_tileOffset 	
	CLC 
	ADC #$01
	TAY
DrawSpritesForThisObjectLoop:
	LDA (sprite_pointer),y
	CLC
	ADC tileset_offset
	CMP #$7F
	BEQ BlankCull
	CMP #$FF
	BNE NoBlankCull
	BlankCull:
		LDA #$FE
		JMP drawTileOffScreen_flicker
	NoBlankCull:
	LDA DrawFlags ;; are we flickering?
	BEQ noFlicker
	LDA #$fe
	JMP drawTileOffScreen_flicker
noFlicker:
	LDA temp2 ;; LDA the y value, not drawn off screen
drawTileOffScreen_flicker:
	STA SpriteRam,x ;; store it to the y value byte for this sprite
	INX ;; increase the index to draw to
	LDA (sprite_pointer),y  ;; load the table pointer to the tile number
	CLC
	ADC tileset_offset

	STA SpriteRam,x ;; store to the tile index		
	INX ;; increase index
	INY ;; increase the position to read in the table (which alternates tile/attribute)
	LDA temp3	
	ORA (sprite_pointer),y ;; load the table pointer to the attribute
	STA SpriteRam,x ;; store it to the attribute index
	INY ;; increase the position to read in the table...this next value is for the next sprite.
	INX ;; increase index
	LdA temp1 ;; load the x value
	STA SpriteRam,x ;; store to the x index
	;;=======================================
	;;;; DONE WITH THIS SPRITE.
	INX  ;; increase index to get ready to draw the next sprite.
	
	DEC tempx ;; decrease the variable holding the width.
	BEQ doneWithSpriteColumns ;; if it is at zero, that means the column is over
	;;; more sprites to draw in this column.
	
	;;;;;;;;;; increase the x value to the next 'column' to draw the next sprite.
		;;;; conceivably, this is where we could put a horizontal offset. 
	LDA temp1 
	CLC
	ADC #$08   ;; each tile is 8 wide.
	STA temp1
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	JMP DrawSpritesForThisObjectLoop
doneWithSpriteColumns:
	DEC tempy ;; decrease the variable holding height
	BEQ doneDrawingThisObjectsSprites ;; if there is no horizontal, and no vertical tiles left to count
										; then this sprite is done being drawn
	;;; there are still sprites to draw here
	;;; so now, we must:
	LDA tempz
	STA tempx ;; restore the temp size of the column
	;;======= increase the y value by 8px / one tile
	LDA temp2 
	CLC
	ADC #$08
	STA temp2 
	;;==============================================
	;and
	;;======= move horizontal position back all the way to the left
	LDA spriteDrawLeft
	STA temp1
	JMP DrawSpritesForThisObjectLoop
	
doneDrawingThisObjectsSprites:
	TXA
	STA spriteOffset
	;; restore x so we're talking about the right object
	PLA
	TAX
	
	
	JSR getNewEndType
	;;;Handle This object's animation:
	;;; fist, count down the animation timer.
	DEC Object_animation_timer,x
	LDA Object_animation_timer,x
	BNE notAtEndOfFrame
	;; is at end of frame.
	LDA Object_animation_frame,x
	CLC
	ADC #$01
	CMP objectFrameCount
	BNE notTheLastFrameYet
	;; this is the last frame
	;;=========================== check for end animation
	LDA Object_end_action,x
	
	LSR
	LSR
	LSR
	LSR

	BNE checkForEndAnimType

	;;; looping type animation - just reset the frame and continue on.
	LDA #$00
	JMP notTheLastFrameYet 
checkForEndAnimType:
	JSR doMoreThanResetTimer ;; this is in handle update objects
								;;uses the same table as 
								;;action timer end
	LDA #$00 ;; to reset frames.
	;;===================================================
notTheLastFrameYet:
	STA Object_animation_frame,x
		LDA Object_animation_offset_speed,x
		;;; set the initial animation timer
		AND #%00001111 ;; now we have the animation speed value displayed in the tool
						;; are 16 values enough for the slowest?  Or do we want to have multiples?  Or maybe a table read?	
	
		;ASL
		;ASL
		ASL
		STA Object_animation_timer,x

	
notAtEndOfFrame:
	

	
	
	RTS

4_1_5BlankSpriteCull.gif
I set it up to cull any sprite using the last gameObject tile or Monster graphic tile. #$7F or #$FF
 

AllDarnDavey

Active member
4_1_5BlankSpriteCull2.gif

Just make sure you are not using one of the blank culled sprites for your sprite0, or you'll mess up the HUD if you're using scrolling.
 

vanderblade

Active member
Thanks, Davey! I applied this to my project, and so far no errors or issues. I have to play through the rest of the game to make sure.

I aggressively modified my monsters to not have unnecessary blank sprites, but this may help with those few that were stubborn.

Edit: it may just be in my head, but I think this is impacting my sprite cycling.
 

Jonny

Well-known member
Another example here of the @CutterCross tutorial above and how useful it can be...
I'm offseting basically (as mentioned in the tut.) to get the 4 numbers to line up. They're all 16x8, with alternating sides blank. Then, with the acutal cursor having 3 blank sprites we're upto 7 so the rest don't draw in the first example.

SPRITE2.gifSPRITE1.gif

The number are supposed to flicker btw. It's a 2 frame animation.
 

TolerantX

Active member
I tried using the code and got branch out of range errors on the Dodrawsprites_arcade platformerbase

LDA tempC
CMP #$7F
BEQ +


Also tried

LDA tempC
BEQ +
 
Top Bottom