Zapper hit detection

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Zapper hit detection
by on (#108656)
In my game, I'm only going to have 1 target at any time, so I just want to know whether I found a hit or not.

If I've understood this correctly, it's not enough to just read from the zapper - it needs to be done while the frame is being drawn, because the light sensor turns off after a very short time compared to my eyes, which perceive the white area on the screen as being constantly white.

So how do I do this? One idea was to poll $2002 for vblank to end and then have a loop that polls $2002 for vblank to start and $4017 for a hit, or that polls just $4017 while waiting for nmi... how would you more experienced people do it?

Also, are there other light sources I could use, for debugging and such? What kind of light, specifically, does the zapper detect?
Re: Zapper hit detection
by on (#108657)
Dafydd wrote:
One idea was to poll $2002 for vblank to end and then have a loop that polls $2002 for vblank to start and $4017 for a hit, or that polls just $4017 while waiting for nmi... how would you more experienced people do it?

What I did in Zap Ruder was set up sprite 0 at the top of the screen, wait for the sprite 0 hit (0 then 1 on $2002 bit 6), and use the "zapkernels" (subroutines that I wrote using timed code to read $4017 once each scanline, whose source code is included with Zap Ruder) to wait up to 192 scanlines for light to be detected. If you just want a Y coordinate (as used in ZapPing), you can leave the whole screen bright and count scanlines.

Otherwise, darken the screen except for the targets and use the Y coordinate to narrow down which target was hit. If this Y coordinate is close to more than one target (not the case in 1 DUCK, but possible for 2 DUCKS or CLAY SHOOTING), turn each on individually. The Zap Ruder menu has two target groups: the boxes on the left and the boxes on the right. When the photodiode turns on, it darkens the groups in sequence over the course of the next two frames and moves the cursor to that group if darkening the group caused the photodiode to turn off.

In my experience, the Zapper's photodiode will turn on reliably when the color in the target area is $10 (light gray), $20-$2C (white and bright colors), or $30-$3C (white and pale colors). It's still unclear whether or not the IC in the Zapper includes a resonator at 15.7 kHz (SDTV horizontal scan rate) to distinguish TV light from other light sources.
Re: Zapper hit detection
by on (#108677)
tepples wrote:
It's still unclear whether or not the IC in the Zapper includes a resonator at 15.7 kHz (SDTV horizontal scan rate) to distinguish TV light from other light sources.
The IC used in the zapper is identical in functionality to a standard infrared remote demodulator IC; the selective frequency has been changed and the normally-IR photodiode has been replaced with a visible light one. I suppose I should sit down and build a testing rig to characterize the behavior.
Re: Zapper hit detection
by on (#108735)
So... if I point the thing at the sun, it should register a hit, right? Why does a lightbulb, or anything that shines a white light, not do the trick? It doesn't react to just ANY visible light, does it?

(I should clarify I do know about the blank screens drawn before and after the frames that draw white hitboxes to prevent cheating)
Re: Zapper hit detection
by on (#108736)
Light from a CRT SDTV turns on and off about 15,700 times a second. Light from a light bulb or the sun does not. The Zapper's demodulator IC detects the difference.
Re: Zapper hit detection
by on (#108761)
Ah, so it's not the frequency of the light, but the frequency at which it flashes. Got it.

Well, I made a screen where the upper quarter is in all white and the rest in all black, save for a message that is printed differently depending on whether you are aiming at white or not, and depending on whether the trigger is pulled. It does, however, not react to ANYTHING I do to it - pulling the trigger, aiming the zapper at the TV or elsewhere, into a pillow while pressing the trigger... nothing happens. It's just stuck displaying HIT. What am I doing wrong?

Code:
wait_for_hit_and_vblank:
   lda #$08
   cmp $4017 ; set Z if A == $08, i.e. if $4017.3 is 1,
           ; i.e. light sense: not detected
   bne wait_for_just_vblank ; if Z is not set: light detected!
   lda #$10
   cmp $4017 ; set Z if A == $10, i.e. if $4017.4 is 1,
           ; i.e. trigger is pulled
   beq wait_for_just_vblank ; if Z is set: trigger pulled!
   
   bit $2002
   bpl wait_for_hit_and_vblank ; still no hit registered, wait some more
   
   ; we're now in vblank with no hit or trigger pull registered. Display MISS
   lda $2002
   lda #$22
   sta $2006
   lda #$2E
   sta $2006
   lda #$4D ; M
   sta $2007
   lda #$49 ; I
   sta $2007
   lda #$53 ; S
   sta $2007
   sta $2007
   jmp set_scroll

; we're still not in vblank, so wait for it...   
wait_for_just_vblank:
   bit $2002
   bpl wait_for_just_vblank
   
   ; we're now in vblank with either a hit or a pulled trigger. Display HIT
   lda $2002
   lda #$22
   sta $2006
   lda #$2E
   sta $2006
   lda #$48 ; H
   sta $2007
   lda #$49 ; I
   sta $2007
   lda #$54 ; T
   sta $2007

set_scroll:
   lda #$00
   sta $2000
   sta $2005
   sta $2005

   jmp wait_for_hit_and_vblank
Re: Zapper hit detection
by on (#108762)
You use bad programming, which is why. Here's the explaination:

Code:
   lda #$08
   cmp $4017 ; set Z if A == $08, i.e. if $4017.3 is 1,
           ; i.e. light sense: not detected
   bne wait_for_just_vblank ; if Z is not set: light detected!
   lda #$10
   cmp $4017 ; set Z if A == $10, i.e. if $4017.4 is 1,
           ; i.e. trigger is pulled
   bne wait_for_just_vblank ; if Z is set: trigger is pulled!


$4017 return what it's supposed to OR'd with $40 because of open bus. AND other bits can be set, too. To get the bits you need the right way, use LDA+BIT or LDA+AND.

Code:
   lda #$08
   bit $4017 ; set Z if A == $08, i.e. if $4017.3 is 1,
           ; i.e. light sense: not detected
   bne wait_for_just_vblank ; if Z is not set: light detected!
   lda #$10
   bit $4017 ; set Z if A == $10, i.e. if $4017.4 is 1,
           ; i.e. trigger is pulled
   bne wait_for_just_vblank ; if Z is set: trigger is pulled!


Code:
   lda $4017
   and #$08 ; set Z if A == $08, i.e. if $4017.3 is 1,
           ; i.e. light sense: not detected
   bne wait_for_just_vblank ; if Z is not set: light detected!
   lda $4017
   and #$10 ; set Z if A == $10, i.e. if $4017.4 is 1,
           ; i.e. trigger is pulled
   bne wait_for_just_vblank ; if Z is set: trigger is pulled!


And on a side note, if you are not using pre-defined labels for the NES hardware, I'd say start doing that as looking at text is easier then looking at a bunch of numbers, especially when you do a ton of hardware interaction as remembering the registers is a waste of time to just having exactly what you're modifying in the name.
Re: Zapper hit detection
by on (#108768)
3gengames wrote:
You use bad programming, which is why.

Fair enough...

Unfortunately, your code doesn't work either.
Re: Zapper hit detection
by on (#108776)
In case you missed it the first time: Zap Ruder includes working code for Zapper polling that you are free to use in your own game.
Re: Zapper hit detection
by on (#108778)
I should have thanked you the first time :) I had a look at it, and I don't really see what I'm doing differently from you (besides the obvious, what with the timed code and all). I have to say, I'm very impressed by the accuracy you've achieved. However, copy+pasting someone else's code wholesale kind of defeats the whole point of why I'm doing this in the first place, and this is way too advanced for me right now (it's also NTSC specific). I only have 1 target on the screen, so the code would be very simple, but I want to understand what I'm doing.

... um. Ok, I changed
Code:
  lda $4017
  and #$08
  beq wait_for_just_vblank
for
Code:
  lda #$08
  and $4017
  beq wait_for_just_vblank
(as per zapkernel's example) and now it works. Also
Code:
  lda #$10
  and $4017
  bne wait_for_just_vblank
works for trigger detection.

So close, 3gengames! :lol: I still don't know why this works and the other way didn't, but I'm going to find out.

Thanks again, tepples! Or, um, both of you, actually :)
Re: Zapper hit detection
by on (#108781)
Umm... nope. Changed it back, still works. I think it was a misuse of beq and bne that did it. ...somehow. Here's some code that works, anyway. Maybe something similar should be on the wiki for noobs like me?

Code:
lda #$08; 00001000
and $4017; ????1??? <- miss
; result: A=00001000, Z=0
beq hit_reported; results in no branch because Z=0

lda #$08; 00001000
and $4017; ????0??? <- hit
; result: A=00000000, Z=1
beq hit_reported; results in branch because Z=1

lda #$10; 00010000
and $4017; ???1???? <- trigger
; result: A=00001000, Z=0
bne trigger_pulled; results in branch because Z=0

lda #$10; 00010000
and $4017; ???0???? <- no trigger
; result: A=00000000, Z=1
bne trigger_pulled; results in no branch because Z=1
Re: Zapper hit detection
by on (#108782)
tepples wrote:
It's still unclear whether or not the IC in the Zapper includes a resonator at 15.7 kHz (SDTV horizontal scan rate) to distinguish TV light from other light sources.

It's my recollection that it's not the IC itself but a capacitor or resistor elsewhere in the circuit - I recall somebody replacing it in order to make their zapper sensitive to 31.5kHz and work properly with scan-doubled NTSC (in fact, I think it may have been kevtris back when he first made his FPGA console).
Re: Zapper hit detection
by on (#108791)
The RC pair off pin 3 (22Ω + 1µF) has a corner frequency of hsync rate÷2. This probably determines the bandwidth of the filter, i.e. the number of scanlines necessary to count.
Assuming that the mechanism of the IR3T07 is similar to the contemporary CX20106 or its equivalent GL3274, the 390KΩ resistor should be what determines the scanrate.

Amusing piece of trivia: The reason that the trigger is "true" for ≈6 vblanks is because that's approximately 10kΩ (the pullup inside the console) × 10µF (the capacitor inside the zapper) = 0.1s
Re: Zapper hit detection
by on (#108796)
lidnariq wrote:
Amusing piece of trivia: The reason that the trigger is "true" for ≈6 vblanks is because that's approximately 10kΩ (the pullup inside the console) × 10µF (the capacitor inside the zapper) = 0.1s

On an unmodified Zapper, I can hold the trigger halfway in and keep it true indefinitely. One activity in Zap Ruder relies on this to change the instrument's timbre.
Re: Zapper hit detection
by on (#108815)
Hmm,
A known glitch about the zapper is you can get a perfect hit score every time in some poorly programmed games by simply pointing the gun right next to and into a light bulb.

This didn't work for me, anyway. Did they just make that up, or are there light bulbs that flash with the same frequency as CRT monitors?

But then again,
Image
Re: Zapper hit detection
by on (#108819)
You never had it happen because you never tried a zapper game in which it allowed you to do that.

Do we really need a "Is there light?" zapper test? If so, who's gonna pony up and write it? :P I will if somebody wants it.
Re: Zapper hit detection
by on (#108828)
A Zapper seeing constant light will look like an unplugged Zapper. In Zap Ruder's test screens, it'll leave the arrows at the top of the screen. ZapPing, also in Zap Ruder, does a more sophisticated check: try to detect light a few lines into vblank. Any light detected in that case represents a Zapper pointed at something other than the TV, at which point it assumes the Zapper is unplugged and falls back to controller play.
Re: Zapper hit detection
by on (#108829)
The code I asked you to help me with earlier in this thread shows a constant, white area that, now that the code works, makes the game display "HIT!" whenever the zapper is aimed at said white area, and "MISS!" whenever aiming anywhere else. No light I've tried other than that coming from the TV makes the game display "HIT!".
Re: Zapper hit detection
by on (#108830)
Well, a halogen lamp did the job for a single frame when moving the zapper past it quickly. It only stays displaying "HIT!", however, when aiming at the white area of the TV, or aiming at the black area of the TV from within 20 cm or so away. I guess even the "black" (which is really a dark grey) is registered by the Zapper as the cathode ray sweeps past. Still, I really doubt my quick sweep of the Zapper in front of a halogen lamp could have fooled the however-many-kHz filter in the IC, so it was probably just the trigger firing despite not being pulled (it's a little loose).

EDIT: Nope, it wasn't the trigger (I removed that code to make sure). Also tested with an LED - the Zapper registers for one frame on other bright light if sufficiently brief in time. I don't think anyone could use this for cheating in a game though, unless they could move their arm at 50 or 60 Hz (or however fast you'd need to be), as you'd need pretty precise timing. I suppose a stroboscope might do the job though.

This MIGHT all be happening, of course, because my code counts the first occurence of $4017.3 being 0 outside of vblank as a hit... but since the Zapper does report bit 3 as being 0, this means the IC has been tricked to think it's seeing light at some 15.7 kHz, while in fact, it didn't. Perhaps someone brighter than I could... illuminate me on this subject?