ZapStrasse: G-Con 2 Light Gun on a Modern TV

For me, one of the great forgotten peripherals of yesterworld gaming is the Light Gun… or the zapper… or whatever the heck you called it. Before Move controllers or the Wii-mote there were a collection of clunky plastic pistol shaped devices that let us have a taste of arcade-shooter action in the comfort of our own homes. For many of us, the golden age of these devices was sometime around the PS1/2 where Namco’s G-Con45 (PS1) and follow up G-Con 2 (PS2) let us play all manner of time crisis games along with a few other classics, one of which being Resident Evil: Dead Aim. Essentially the third in the poorly received ‘Survivor’ series, Dead Aim actually delivers a much better experience to it predecessors, largely through the addition of a D-Pad to the back of the G-Con 2. And it was a want to revisit this game a little while back that started this whole journey off…

… because as most retro-gamers know, these light guns don’t work on modern day (Non-CRT) TV’s which makes them pretty much unplayable for many of us, unless you’re committed enough (with adequate space) to have a CRT devoted to your retro, or you happened to back one of the few kickstarted campaigns for highly bespoke modern TV compatible light guns.

But, I had an idea; a spark of a concept really… What if I could build a doohickey.. a doodad… a thingum… whatever you want to call it, that I could strap to the front of my original G-Con 2 that would fool it in to working with a modern TV? … and you know what…I did… and the idea works… and I want to share it with all of you. So here it is from start to finish; The Hundstrasse ‘Zap-Strasse’ breakdown complete with shopping list, CAD files and code etc. so that you two can muddle one together and use it yourself if you want (Warning! – It is nowhere near perfect! This is proof of concept stuff!).

The Concept:

Before getting in to the technical stuff, it’s worth running over the basic idea, and to do that you need to know roughly how this particular breed of light gun works:

The G-Con (they both work the same as far as I can tell, but I’ve only tried the ZapStrasse on the G-Con 2) guns have two connections: one to the console (either controller or USB) and another to the composite video feed that’s going to the TV. The hardware itself is basically just a light sensor or some sort (no idea what sort, I’ve never taken it apart) with a plastic lens on the front so the that sensor is ‘looking’ at a fairly small patch of the screen.

CRT screens produce an image by scanning a single bright ‘dot’ across the entire screen in multiple horizontal lines. By changing the intensity of the dot as it scans each line, a picture can be built up. Once the dot reaches the bottom of the screen it returns to the top and the process starts all over again. Typically a full screen was scanned in 50Hz or 60Hz (time per second) depending on the TV standard in your geographic region. (Ok, before I get letters, that’s not quite right, but I’m keeping this to the key concepts ok!). The composite video signal controls the intensity and position of the dot; the start of each horizontal line is marked with a synchronisation pulse, as is the start of each new frame (or field).

When the ‘dot’ passes over the portion of the screen that the gun is ‘looking’ at, the light sensor detects it. As the gun also has a connection to the composite video feed, it can work out exactly where the dot was on the screen when this happened and therefore where on the screen the player was pointing the gun. If the player had pulled the trigger at this point then the gun relays this information back to the games console and the game would register a shot at the screen.

This works best when the screen is bright which is why most of this generation of light gun games tend to flash the screen white when the trigger is pulled as it gives the best response for that shot… but it’s generally interpreted as simulated muzzle flash.

Modern TV’s don’t scan which is why these guns don’t work.

Sooooo…. what does the ZapStrasse do?

The ZapStrasse fools your trusty ol’ G-Con in to thinking that it’s looking at a CRT by flashing an LED at it. I think this diagram explains it all pretty well…

… no?… ok, I’ll help you out. First up there is an infra-red camera on the front (just like in a Wii-mote) and an infra-red LED (or beacon) on top of the TV. By doing a little calibration routine when it’s switched on (just pointing at two corners of the play area and pushing a button) the ZapStrasse knows if it is pointed at the play area and where it is pointed on the play area from the position that it sees the IR beacon.

Using this information the ZapStrasse calculates which hypothetical horizontal scan line and where along it the corresponding location would be.

Finally, using the synchronisation pulses from the composite video feed it flashes an LED at the exact moment the signal reaches the calculated point. The G-Con looking at the LED is fooled in to thinking that it is looking at a CRT TV and returns the location of the shot back to the console.

… and this happens every frame…

Hardware: Shopping List

Want to build your own? First up, my prototype version is just that; a prototype. I’m not intending on developing this much further other than possibly some updates to the code, so you’ll need a bit of electronic knowledge and possibly some DIY maker-skillz to get your own version working, but here are all the key bits of information you need, starting with the important bits of hardware:

  • Arduino Nano Every (You can buy from the PiHut): This small form factor Arduino is the brains of the operation and performs all the necessary calculations and timing for the ZapStrasse. You’ll need to upload the sketch provided below and I recommend clocking it up to the full 20MHz rather than that standard Nano 16MHz. If you’ve never coded for/used an Arduino before then the official site offers some great information and links to download the necessary IDE software.
  • IR Positioning Camera (you can buy from the PiHut): This device is essentially what powers the Wii remote and does a lot of the heavy lifting onboard so that it simply transmits the sensor position of up to 4 tracked IR sources (although at the moment ZapStrasse only uses one.
  • Wii Style Sensor Bar: Either plug in the old Wii, or better yet, buy a battery powered one. The Wii sensor bar is basically just two banks of IR LEDs at either end of the bar. For the ZapStrasse currently I only use one side of the bar, so cover up the one set of the LED’s and position so that the remaining set of LED’s sits on top of your TV as centrally as possible. If you don’t now what I’m talking about try switching on the Wii sensor bar then use your mobile phone camera to look at it; you should be able to see the IR LED’s glowing in either end.
  • Texas Instruments LM1881 IC.: This little ‘chip’ is a sync stripper; essentially it just extracts the scan synchronisation signals from the composite video signal. If you want more information about it you can check out the datasheet here, I also found this page on PAL composite video signals useful for background information.
  • You’ll also need:
    • 2 x 0.1 microfarad capacitors
    • Standard 5V LED (I’ve gone with Red as it apparently has a good response time) and associated protective resistor
    • RCA Composite video Terminal block (or similar)
    • Simple push-to-make button
    • A few resistors (see circuit diagram)
    • Something to prototype on (breadboard, stripboard etc.)
    • wires, solder, etc.
    • Some kind of case to hold it to the front of the G-Con (CAD for my very rough attempt provided if you want to 3D print something)

Electronics: Circuit Diagram

I’d suggest building the circuit on a breadboard and testing it out before soldering anything, but hey, that’s up to you. (you’ll likely need the full Nano Every pinout available here)

So, I prototyped mine on a breadboard and then soldered it up on a piece of standard stripboard (with some of my terrible soldering skills). If you’re planning on using the same case as me then the stripboard should be 70 x 38 mm and avoid putting connections in the corners. Also remember that the LED needs to have leads long enough to so that it can sit facing the light gun sensor (on the other side of the board). I soldered the Arduino directly to the strip-board (heatsink the pins if possible during soldering), but you could use an IC holder if you want, but be aware that space in the case is quite tight.

All Breadboarded up…
… and then some terrible soldering… there exists no photographic evidence of the soldered side…

Arduino Code

This is my current prototype code; don’t laugh! I wanted to keep it as simple as possible so I’ve made a lot of assumptions and approximations. There are also still some bugs (see later section) so don’t expect this to be anywhere near flawless.

// ZapStrasse by @Hundstrasse
// www.Hundstrasse.com
// Last Edit 18/5/21
//
//Copyright (c) 2021 L. Carter <@Hundstrasse>
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
//
//
//Designed to run on Arduino Nano Every Clocked to 20MHz
//Code uses pin registry for fast digital switching so may not transfer between Arduino models
//Change appropriate lines in boards.txt for 20MHz to:
//nona4809.build.f_cpu=20000000L
//nona4809.bootloader.OSCCFG=0x02
//
//Use ATMEGA328 Register Emulation
//
// IR Code adapted from:
// Wii Remote IR sensor  test sample code  by kako http://www.kako.com
// modified for https://dfrobot.com by Lumi, Jan. 2014
//

#include <Wire.h>

//IR sensor variables
int IRsensorAddress = 0xB0;
int slaveAddress;
byte data_buf[16];
int i;

//Calibration Button Digital Pin
int c_switch = 10;

//IR return value variables
long Ix[4];
long Iy[4];
int s;

//Calculated play area
long min_x;
long min_y;
long max_x;
long max_y;
long x_range;
long y_range;
bool on_screen = false;
int j = 0;

//Sync Strip Variables
volatile long h_lines = 0;
volatile bool old_frame = false;
int h_target = 26;          //Nothing apecial about these numbers, they're just inoffensive initial values to allow the first program loop to run
int v_target = 163;
int v_target_a = 158;
int v_target_b = 159;
int v_target_c = 160;
int v_target_d = 161;
int v_target_e = 162;
int v_target_f = 164;
int v_target_g = 165;
int v_target_h = 166;
int v_target_i = 167;
int v_target_j = 168;

// Following variables may need to be changed for different video modes, resolutions etc.
long h_range = 50;          //micro-seconds for the visual data part of each horizontal scanline (rem:52)
long v_range = 288;         //Number of horizontal lines visible on screen. (could be 288 or 305 for PAL *shrugs)
int led_d = 3;              //Micro second delay for each LED flash
int full_line_delay = 44;

void Write_2bytes(byte d1, byte d2)
{
  Wire.beginTransmission(slaveAddress);
  Wire.write(d1); Wire.write(d2);
  Wire.endTransmission();
}

void Sensor_Read()
{
  //IR sensor read
  //Note: Sensor misbehaves when requesting or reading fewer than 16 bytes; I've not properly investigated
  //Seems to work OK with current code, but could cause problems in future as data read is slow.
  Wire.beginTransmission(slaveAddress);
  Wire.write(0x36);
  Wire.endTransmission();
  Wire.requestFrom(slaveAddress, 16);
  i = 0;
  while (Wire.available() && i < 16) {
    data_buf[i] = Wire.read();
    i++;
  }
  Ix[0] = data_buf[1];
  Iy[0] = data_buf[2];
  s   = data_buf[3];
  Ix[0] += (s & 0x30) << 4;
  Iy[0] += (s & 0xC0) << 2;
}

void h_count() {
  //ISR to increment Horizontal line count with each sync pulse
  h_lines++;
}

void h_reset() {
  // ISR to reset horizontal line count at V-Sync pulse and report next frame
  h_lines = 0;
  old_frame = false;
}

void setup()
{
  //Setup for IR Sensor (Adapted from Example Code)
  slaveAddress = IRsensorAddress >> 1;
  Wire.begin();
  // IR sensor initialize
  Write_2bytes(0x30, 0x01); delay(10);
  Write_2bytes(0x30, 0x08); delay(10);
  Write_2bytes(0x06, 0x90); delay(10);
  Write_2bytes(0x08, 0xC0); delay(10);
  Write_2bytes(0x1A, 0x40); delay(10);
  Write_2bytes(0x33, 0x33); delay(10);
  delay(100);

  //Calibrate Position
  //Point gun at lower left corner of play area and press calibration button, then upper right and press calibration button
  //Sets the screen play area; if you need to repeat then either rest Arduino, or power cycle with cable.
  //You'll need to stay roughly in the same position during play, otherwise recalibrate
  pinMode(c_switch, INPUT);
  while (digitalRead(c_switch) == HIGH) {}    //Stops Accidental Calibration
  while (digitalRead(c_switch) == LOW) {}     //Lower LHS play area calibration
  Sensor_Read();
  min_x = Ix[0];
  min_y = Iy[0];
  delay(100);
  while (digitalRead(c_switch) == HIGH) {}
  while (digitalRead(c_switch) == LOW) {}     //Upper RHS play are calibration
  Sensor_Read();
  max_x = Ix[0];
  max_y = Iy[0];
  x_range = max_x - min_x;
  y_range = max_y - min_y;

  //Interrupts and LED output
  DDRD |= B00000100;            //Don't Edit the DDRD binary values unless you're sure you know what you're doing!
  PORTD &= B11111011;           //Don't Edit the PORTD binary values unless you're sure you know what you're doing!
  attachInterrupt(digitalPinToInterrupt(7), h_count, RISING);
  attachInterrupt(digitalPinToInterrupt(8), h_reset, RISING);

}

void loop()                           //Flash LED at correct 'position', read IR position, calcuate new target position, rinse, repeat.
{
  while (old_frame) {}                //Wait for next frame (ensures no repeat or second flash during a single frame)
  old_frame = true;
  if (on_screen) {

    if (h_target < 39) {
      while (h_lines < (v_target_a)) {} //You're probably wondering why I don't just have a loop here? It was all an effort to reduce operations between pulses
      delayMicroseconds(h_target);
      PORTD |= B00000100;               //Don't Edit the PORTD binary values unless you're sure you know what you're doing!
      delayMicroseconds(led_d);        
      PORTD &= B11111011;               //Don't Edit the PORTD binary values unless you're sure you know what you're doing!

      while (h_lines < (v_target_b)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;              
      delayMicroseconds(led_d);         
      PORTD &= B11111011;               

      while (h_lines < (v_target_c)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;              

      while (h_lines < (v_target_d)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;              
      delayMicroseconds(led_d);       
      PORTD &= B11111011;           

      while (h_lines < (v_target_e)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;             
      delayMicroseconds(led_d);       
      PORTD &= B11111011;          

      while (h_lines < (v_target)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;             
      delayMicroseconds(led_d);     
      PORTD &= B11111011;              

      while (h_lines < (v_target_f)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;               
      delayMicroseconds(led_d);        
      PORTD &= B11111011;            

      while (h_lines < (v_target_g)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;         
      delayMicroseconds(led_d);       
      PORTD &= B11111011;              

      while (h_lines < (v_target_h)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;            
      delayMicroseconds(led_d);    
      PORTD &= B11111011;            

      while (h_lines < (v_target_i)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;               
      delayMicroseconds(led_d);       
      PORTD &= B11111011;              

      while (h_lines < (v_target_j)) {}
      delayMicroseconds(h_target);
      PORTD |= B00000100;             
      delayMicroseconds(led_d);      
      PORTD &= B11111011;             
    }
    else {
      while (h_lines < (v_target_a)) {} //For points to the right the above doesn't work so you need to apply a full lie delay.   
      delayMicroseconds(h_target);      //This still isn't quite right, but it's better.
      PORTD |= B00000100;              
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;            
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;              
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;              
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;               
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
      delayMicroseconds(full_line_delay);
       PORTD |= B00000100;           
      delayMicroseconds(led_d);         
      PORTD &= B11111011;
    }

  }

  if (digitalRead(c_switch) == HIGH) {  //Centre Shoot Mode: useful for in-game calibration; hold button and gun targets screen centre
    on_screen = true;
    h_target = 25;
    v_target = 154;
  }

  else {
    Sensor_Read();                      //Read sensor as soon as possible after LED flash to give maximum time for data transfer

    if ((Ix[0] < min_x) || (Iy[0] < min_y) || (Ix[0] > max_x) || (Iy[0] > max_y)) {
      on_screen = false;                //If not onscreen then don't pulse LED next frame
    }

    else {                              //Calcuate new horizontal line target and distance along line in microseconds
      on_screen = true;
      h_target = int(((Ix[0] - min_x) * h_range) / x_range);
      v_target = int((((y_range - (Iy[0] - min_y) ) * v_range) / y_range) + 10) ; //+10 for blank lines pre-display area
    }
  }

  if (on_screen) {                      //Pre-prepare all scanlines to trigger
    v_target_a = v_target - 5;
    v_target_b = v_target - 4;
    v_target_c = v_target - 3;
    v_target_d = v_target - 2;
    v_target_e = v_target - 1;
    v_target_f = v_target + 1;
    v_target_g = v_target + 2;
    v_target_h = v_target + 3;
    v_target_i = v_target + 4;
    v_target_j = v_target + 5;
  }

}

The Case

Hand on heart… I whipped up this CAD very quickly and due to a slightly shonky 3D printer it didn’t go together great and despite putting in provision for it to be held together with an M4 nut and bolt, my prototype needs cableties to keep it clamped to the front of the G-con as well as a bit of clever ‘editing’ with a craft knife… but hey, if you want a starting point you can download the STL files here.

This is how it looks mostly in the case. Note: I had to put a dot of glue on the side of the LED so that it stayed in position. The gun needs to be looking directly ‘in-to’ the light.

One thing to note about the case is that the IR sensor is intentionally angled upwards slightly (9.5 Deg from horizontal). This maximises play area when the IR beacon is on top of the TV, but would be detrimental if you put the IR sensor under the screen.

Using the ZapStrasse

All closed up and ready to go!

So you’ve soldered, printed, uploaded the code and strapped the whole thing to the front of your G-Con2 and you want to know how to use it?

  • You’ll need to be running your PS2 in RGB mode with access to the composite video cable (I think originally this was connected to the TV via a SCART adaptor).
  • Setup the G-con as normal (USB & Composite video), but you’ll need another composite video T-connected out in addition to the one going to the Gun.
  • Plug the composite video in to the ZapStrasse and a USB power supply (via Arduino micrconnection). I advise a few cable ties to stop things pulling out of sockets etc.
  • Make sure that the half (see earlier section) Wii sensor bar is in position centrally on top of your TV and switched on
  • I advise setting TV to 4:3 (normal) mode so you’ll be playing with black bars
  • Stand 1.5m to 2m from screen when playing (unfortunately the field of view for the IR camera isn’t great so you need to be a fair distance away)
  • Calibrate by aiming at the lower left hand corner of the screen and pressing the ZapStrasse button then the same for the Upper Right hand corner of the screen.
  • You should now be good to go. If the gun is registering as not pointing at screen in your game’s calibration screen then power-cycle the ZapStrasse and try again.

Does it work?? …. Yes…. Sort of…but not great… Current Known Issues:

The ZapStrasse in action on its first ingame test

As I said, this is really a proof of concept prototype, so don’t expect great things. The following are known issues:

  • Target point dances around quite a but, specifically tends to jump to the right for a fraction of a second every second or so. I have no idea why this is, but could be to do with the response times of the Arduino or the LED itself. This can make ingame calibration a bit awkward (you may need to try several times)
  • Shooting at the far right of the screen instead shoots at the left of the screen; again this is a limit on the hardware response time. I’ve programmed in a few hack-ey improvements to this, but it’s still not ideal
  • You need to play holding the gun level (ie. don’t rotate around the barrel at all). ZapStrasse currently only uses a single IR beacon so the relative movement will be way out if you try and hold the gun sideways.
  • This has only been tested on a PAL (UK) tv, G-Con 2, and PS2. Theoretically you could make this work on a PS1 G-con or NTSC TV, but it would need changes to the code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.