Sprite Zero Detection

puppydrum64

Active member
Ok, I understand that this was a feature removed from NES Maker because it was considered too complicated for the average user to understand. I get that. But I really want to use it for my game and I just can't seem to get it to work. This is what I understand so far:

* The NES checks each pixel of the screen in an endless loop.
* In address $2002, the 6th bit is set at the first moment during the loop that a non-transparent sprite is touching a non-transparent background tile, and the above loop happens to be on that pixel.
* Super Mario Bros. uses this fact to partition the HUD from the rest of the screen, having it stay in place while the game scrolls.

I have read both this article on NESdev wiki and this explanation on stack exchange and I mostly get it.

Now with all that being said, I'm not sure about the implementation of splitting the screen. Using breakpoints in the Mesen debugger I can see just when sprite zero detection occurs, and it occurs mostly when I expect it to (except at the very beginning to lock the background HUD in place.)

My first attempt to make the screen split involved changing the NMI script. Originally, it looked like this:
Code:
	LDA camX
	STA $2005	;reset scroll values to zero
	LDA camY
	STA $2005	;reset scroll values to zero

Understanding that $2005 controls scrolling, my original plan was to do the following:
Code:
	LDA $2002
	AND $40
	BEQ SpriteZeroWait
	JMP skipNMIstuff

	SpriteZeroWait:
	LDA camX
	STA $2005	;reset scroll values to zero
	LDA camY
	STA $2005	;reset scroll values to zero

skipNMIstuff:

I was thinking I could gate the scroll reset behind sprite zero detection but all this did was make the camera halfway off to the right and messed everything up.

I'm hoping someone more experienced than me can explain how to code this sort of thing, I feel so close to getting it but I'm just not quite there and can't find a good tutorial that explains it (I've searched all over the internet but so far no luck)
 

CutterCross

Active member
You aren't in a spinning loop waiting for the sprite-0 flag to actually hit. You're just doing a sprite-0 hit check once right after vBlank starts. Your sprite-0 hit isn't in vBlank, so it never occurs.

Think about how the NMI and sprite-0 detection works. The NMI vector gets fired at the start of vBlank [the end of the frame], and the routine associated with the NMI runs. After that routine finishes, it's back to the main game loop where it waits for vBlank to end, then game logic starts over for the new frame. Your sprite-0 hit is going to occur DURING frame rendering. So you have to keep spinning during and after vBlank to wait for the sprite-0 hit. Waiting for the hit to occur like this is also why sprite-0 detection can make slowdown occur more often during gameplay.

So what you need is a spin loop to wait until the sprite-0 hit is detected, another loop to time your PPU register updates to occur during hBlank [because you can only safely write to PPU registers when rendering is disabled], and then you actually write to the PPU registers.

Code:
    LDA $0200               ;; OAM slot 0 [sprite-0]
    CMP sprite0_yPos        ;; check if sprite-0 is at a specific Y-coordinate
                            ;; [I have a variable for this, you can just use a static number]
    BNE skipSprite0Check
    ;;;; Do whatever exception conditionals for sprite-0 detection here:

    BIT $2002                ;; reset $2005 / $2006 latch if it's in an unknown state
    LDA #$00
    STA $2005                ;; set initial scroll positions
    STA $2005

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    LDA #%10010000           ;; enable NMI, sprites from Pattern Table 0, background from Pattern Table 1
    STA $2000               ;; start with nametable = 0 for status bar

    LDA #%00011110           ;; enable sprites, enable background, no clipping on left side
    STA $2001

WaitNotSprite0:
    BIT $2002
    BVS WaitNotSprite0        ;; wait until out of vBlank

WaitSprite0:
    BIT $2002
    BMI skipSprite0Check    ;; if vBlank is detected, we missed the sprite 0 hit somehow.
                            ;; jump out of the loop in that case. [safety net]
    BVC WaitSprite0            ;; wait until sprite 0 is hit

    LDX hBlankTimer         ;; I have a variable for my hBlank timer. You can also use a static number.
WaitScanline:               ;; wait until the electron beam is in hBlank
    DEX
    BNE WaitScanline

    LDA camX                ;; Do scroll updates
    STA $2005
    LDA camY                ;; This method cannot create a split y-axis scroll, but we'll set the Y position anyway.
    STA $2005

    LDA soft2001
    STA $2001
skipSprite0Check:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ;;;; rest of NMI...
 
Last edited:

puppydrum64

Active member
Perhaps I don't understand this as well as I thought. I assumed that anything in the NMI script was part of vBlank. I'm still trying to understand where exactly your code example should go to make this work.
 

puppydrum64

Active member
Ok, so I tried something a little different and unfortunately got this result.

I'll go ahead and paste my entire NMI script below.

Code:
NMI:
	;first push whatever is in the accumulator to the stack
	
	PHA
	LDA doNMI
	BEQ dontSkipNMI
	JMP skipWholeNMI
dontSkipNMI:
	LDA #$01
	STA doNMI
	TXA
	PHA
	TYA
	PHA
	PHP
	
	LDA temp
	PHA 
	LDA temp1
	PHA 
	LDA temp2
	PHA 
	LDA temp3
	PHA 
	LDA tempx
	PHA 
	LDA tempy
	PHA 
	LDA tempz
	PHA 
	
	LDA tempA
	PHA
	LDA tempB
	PHA
	LDA tempC
	PHA
	LDA tempD
	PHA


	LDA arg0_hold
	PHA
	LDA arg1_hold
	PHA
	LDA arg2_hold
	PHA
	LDA arg3_hold
	PHA
	LDA arg4_hold
	PHA
	LDA arg5_hold
	PHA
	LDA arg6_hold
	PHA
	LDA arg7_hold
	PHA

	
	LDA temp16
	PHA 
	LDA temp16+1
	PHA 
	
	LDA pointer
	PHA
	LDA pointer+1
	PHA
	
	LDA pointer2
	PHA
	LDA pointer2+1
	PHA
	
	LDA pointer3
	PHA
	LDA pointer3+1
	PHA
	
	LDA pointer6
	PHA
	LDA pointer6+1
	PHA
	
	LDA currentBank
	PHA 
	LDA prevBank
	PHA 
	LDA tempBank
	PHA 
	LDA chrRamBank
	PHA 
	

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ;;;; rest of NMI...
	LDA skipNMI
	BEQ dontSkipNMI2
	JMP skipNMIstuff
dontSkipNMI2:
	LDA #$00
	STA $2000
	LDA soft2001
	STA $2001
	
	;;;Set OAL DMA
	LDA #$00
	STA $2003
	LDA #$02
	STA $4014
	;; Load the Palette




;;;;;;;;;;;;;;;;;;;SPRITE ZERO STUFF STARTS HERE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	LDA $0200               ;; OAM slot 0 [sprite-0]
    CMP #$50       ;; check if sprite-0 is at a specific Y-coordinate
                            ;; [I have a variable for this, you can just use a static number]
    BNE skipSprite0Check
    ;;;; Do whatever exception conditionals for sprite-0 detection here:

    BIT $2002                ;; reset $2005 / $2006 latch if it's in an unknown state
    LDA #$00
    STA $2005                ;; set initial scroll positions
    STA $2005

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    LDA #%10010000           ;; enable NMI, sprites from Pattern Table 0, background from Pattern Table 1
    STA $2000               ;; start with nametable = 0 for status bar

    LDA #%00011110           ;; enable sprites, enable background, no clipping on left side
    STA $2001

WaitNotSprite0:
    BIT $2002
    BVS WaitNotSprite0        ;; wait until out of vBlank

WaitSprite0:
    BIT $2002
    BMI skipSprite0Check    ;; if vBlank is detected, we missed the sprite 0 hit somehow.
                            ;; jump out of the loop in that case. [safety net]
    BVC WaitSprite0            ;; wait until sprite 0 is hit

    LDX #$00         ;; I have a variable for my hBlank timer. You can also use a static number.
WaitScanline:               ;; wait until the electron beam is in hBlank
    DEX
    BNE WaitScanline

    LDA camX                ;; Do scroll updates
    STA $2005
    ; LDA camY                ;; This method cannot create a split y-axis scroll, but we'll set the Y position anyway.
    ; STA $2005

    LDA soft2001
    STA $2001
skipSprite0Check:

;;;;;;;;; SPRITE ZERO STUFF ENDS HERE ;;;;;;;;;;;;;;;;;;;;;;;
	LDA soft2001
	BNE doScreenUpdates
	JMP skipScreenUpdates



 doScreenUpdates:

	 bit $2002
	 ;;; VBLANK ENDS HERE
	 LDA updateScreenData
	 AND #%00000001
	 BNE doPaletteUpdates
	 JMP +
 doPaletteUpdates:
	 .include SCR_LOAD_PALETTES
	 JMP skipScreenUpdates
 +
	 LDA updateScreenData
	 AND #%00000010
	 BNE doSpritePaletteUpdates:
	 JMP +
doSpritePaletteUpdates:
	.include SCR_LOAD_SPRITE_PALETTES
	JMP skipScreenUpdates
+
	LDA updateScreenData
	AND #%00000100
	BNE doColumnUpdate
	JMP +
doColumnUpdate:
	.include SCR_UPDATE_SCROLL_COLUMN
	JMP skipScreenUpdates
+

skipScreenUpdates:	
	;; always do hud update, otherwise
	;; it's possible that updates to tiles, attributes, or palettes
	;; will cause a skip in hud update.
	;.include ROOT\DataLoadScripts\LoadHudData.asm
	
			LDA camY_hi
			ASL
			ASL
			ASL
			ASL
			CLC
			ADC camX_hi
			STA camScreen
	;LDA camScreen ;; bit 0, determines which nametable we are looking at.
	
	
	
	AND #%00000001
	ORA #%10010000
	
	STA $2000
	

	LDA camX
	STA $2005	;reset scroll values to zero
	LDA camY
	STA $2005	;reset scroll values to zero
skipNMIstuff:		


	
	INC vBlankTimer
	INC randomSeed

	;; music player things


	SwitchBank #$1B ;; music bank
	 JSR doSoundEngineUpdate 
	 ReturnBank

	
	PLA
	STA chrRamBank
	PLA 
	STA tempBank
	PLA 
	STA prevBank
	PLA
	STA currentBank
	
	PLA
	STA pointer6+1
	PLA
	STA pointer6
	PLA
	STA pointer3+1
	PLA
	STA pointer3
	
	PLA
	STA pointer2+1
	PLA
	STA pointer2
	
	PLA
	STA pointer+1
	PLA
	STA pointer
	
	PLA
	STA temp16+1
	PLA
	STA temp16

	PLA
	STA arg7_hold
	PLA
	STA arg6_hold
	PLA
	STA arg5_hold
	PLA
	STA arg4_hold
	PLA
	STA arg3_hold
	PLA
	STA arg2_hold
	PLA
	STA arg1_hold
	PLA
	STA arg0_hold
	
	PLA 
	STA tempD
	PLA
	STA tempC
	PLA
	STA tempB
	PLA 
	STA tempA
	
	PLA 
	STA tempz
	PLA 
	STA tempy
	PLA
	STA tempx
	PLA 
	STA temp3
	PLA 
	STA temp2
	PLA 
	STA temp1
	PLA 
	STA temp
	
	LDA #$00
	STA doNMI
	
	PLP
	PLA
	TAY
	PLA
	TAX
skipWholeNMI:	
	LDA #$00
	STA waiting
	PLA

	
	RTI
 

CutterCross

Active member
Perhaps I don't understand this as well as I thought. I assumed that anything in the NMI script was part of vBlank. I'm still trying to understand where exactly your code example should go to make this work.
The NMI routine gets fired when vBlank starts. Before doing anything related to sprite-0 hit detection, you want to first update anything else PPU related. This is because PPU registers can only be safely written to when rendering is disabled. Aside from manually disabling sprite and background rendering, the only times you can safely update the PPU registers are in hBlank [which occurs at the end of every scanline, but is VERY short], and in vBlank [at the end of the frame]. So when vBlank starts, update everything PPU related first, [tile updates, palette updates, etc.] THEN carry on with everything related to the sprite-0 hit.
 

puppydrum64

Active member
The NMI routine gets fired when vBlank starts. Before doing anything related to sprite-0 hit detection, you want to first update anything else PPU related. This is because PPU registers can only be safely written to when rendering is disabled. Aside from manually disabling sprite and background rendering, the only times you can safely update the PPU registers are in hBlank [which occurs at the end of every scanline, but is VERY short], and in vBlank [at the end of the frame]. So when vBlank starts, update everything PPU related first, [tile updates, palette updates, etc.] THEN carry on with everything related to the sprite-0 hit.
I've almost got it working now that I've seen this reply. Consider my video outdated. There are only two problems at the moment.

* On screens where sprite zero isn't drawn, the camera is halfway between the screen it's supposed to be centered on and the screen directly to the right of that screen. (I think I can fix this by skipping the sprite zero check code when not on a screen that's supposed to scroll.)

* When the game starts, the HUD works as expected. However, after a while the HUD's tiles get overwritten with the next screen's background tiles. Thankfully the HUD is still static as the rest of the screen scrolls.
 

CutterCross

Active member
The scroll routine is still updating tiles in the HUD area when the loading seam wraps around to the 1st nametable again. Building a game that uses sprite-0 detection for an X scroll split, your loading seam would be designed with it in mind, where it doesn't update tiles in the HUD area. But since NESmaker 4.5.X's scroll routines weren't designed with sprite-0 detection in mind, and updates the entire column, this is what results.
 

puppydrum64

Active member
The scroll routine is still updating tiles in the HUD area when the loading seam wraps around to the 1st nametable again. Building a game that uses sprite-0 detection for an X scroll split, your loading seam would be designed with it in mind, where it doesn't update tiles in the HUD area. But since NESmaker 4.5.X's scroll routines weren't designed with sprite-0 detection in mind, and updates the entire column, this is what results.
So I'd have to alter how the loading seam works. I'm guessing that's what gets written to $4014? I could be wrong
 
Top Bottom