SEGASGDK

Sega Genesis Light Gun Programming with SGDK

2024/02/16

Introduction

SGDK is a free and open-source software development kit for the Sega Genesis/Mega Drive. I like it a lot. It makes it very easy to create games for the Sega Genesis/Mega Drive. I especially like the controller support. SGDK supports a wide range of controllers for the Genesis and Master System. A quick look at joy.h shows that it supports the Sega Menacer, Sega Phaser, and Konami Justifier light guns.

 * This unit provides methods to read controller state.<br>
 *<br>
 * Here is the list of supported controller device:<br>
 * - 3 buttons joypad<br>
 * - 6 buttons joypad<br>
 * - Sega Mouse<br>
 * - Sega Team Player adapter<br>
 * - EA 4-Way Play<br>
 * - the Menacer<br>
 * - the Justifier<br>
 * - Sega Master System pad<br>
 * - Sega Trackball<br>
 * - Sega Phaser<br>

NOTE: The code in this post has been built with SGDK 1.80. It may not work with older versions. In the past I’ve had serveral link errors with SGDK 1.60. If you’re using an older version of SGDK and run into problems, upgrade to v1.80.

out/sega.o: In function `_EXTINT':
(.text.keepboot+0x44e): undefined reference to `internalExtIntCB'
out/sega.o: In function `_HINT':
(.text.keepboot+0x460): undefined reference to `internalHIntCB'
out/sega.o: In function `_VINT':
(.text.keepboot+0x472): undefined reference to `internalVIntCB'

I am not going to cover installing SGDK or sprite programming. I assume you have a basic understanding of how to install and compile with SGDK and can add a sprite with the basic sprite engine. If you don’t know how to do these things, there are a number of tutorials available on YouTube. Pigsy’s Retro Game Dev Tutorials is a good place to start. There are also tutorials on the web: ohsat daniweb. They’re may be a little out of date, but it shouldn’t be difficult to figure out.

Special thanks to Chilly Willy for developing most of the controller code and providing advice over at Sprites Mind.

SGDK Light Gun Programming

To program a light gun game with SGDK, you use the controller functions in joy.h. I’ll cover all three supported light guns in this post (they’re all pretty similar), but I’ll spend most of my time on the Sega Menacer.

Sega Menacer Programming

Basics

The basic steps to using light guns with SGDK are: 1. Check your controller ports for the light gun ID if your games uses the Menacer or Justifier. The Phaser has no ID to look up. 2. Tell SGDK which light gun you want to use. 3. Read X and Y values from SGDK’s controller functions. 4. Read the light gun buttons.

Checking the Controller Ports

The first thing to do is check if a Sega Menacer is attached to the controller port. This is done with the JOY_getPortType() function. You can use the Menacer with either controller port, but all games I’m aware of use the second port. Use the predefined PORT_1 and PORT_2 to tell SGDK which you want to query. THe following code checks if PORT_2 has a Menacer attached:

u8 portType = JOY_getPortType(PORT_2);
if(portType == PORT_TYPE_MENACER )
{
  // ... Do stuff here
}    
Enable Menacer Support

Once you’ve determined a Menacer is attached to the port, call JOY_setSupport() with the port you want to use (PORT_2) and JOY_SUPPORT_MENACER to tell SGDK to start reading Menacer values.

u8 portType = JOY_getPortType(PORT_2);
if(portType == PORT_TYPE_MENACER )
{
  JOY_setSupport(PORT_2, JOY_SUPPORT_MENACER);
}
Read X and Y values

One SGDK knows which light gun to support you can start reading the light gun X and Y values. SGDK has two functions for getting these values: JOY_readJoypadX() and JOY_readJoypadY(). Use these functions in your main loop to get the raw X and Y values of your Menacer.

  s16 xVal = JOY_readJoypadX(JOY_2);
  s16 yVal = JOY_readJoypadY(JOY_2);

Easy, right? Well, it’s a bit more complicated than this. I’ll go into more detail later.

Read Trigger/Buttons

Button states are read the same way you read a 3 or 6 button controller.

u16 value = JOY_readJoypad(JOY_2);

Where value can be BUTTON_A, BUTTON_B or BUTTON_C, or BUTTON_START.

The basic code looks like this:

#include <genesis.h>

int main(bool hard)
{

  ///////////////////////////////////////////////////////////////////////////////////
  // Menacer Setup
  //  1. check Port 2 for the Sega Menacer
  bool menacerFound = FALSE;
  u8 portType = JOY_getPortType(PORT_2);
  if (portType == PORT_TYPE_MENACER)
  {
    // 2. Turn on Menacer support
    JOY_setSupport(PORT_2, JOY_SUPPORT_MENACER);
    menacerFound = TRUE;
    VDP_drawText("Menacer FOUND!", 13, 1);
  }
  else
  {
    VDP_drawText("Menacer NOT found.", 11, 1);
  }


  ///////////////////////////////////////////////////////////////////////////////////
  // Main Loop!
  while (TRUE)
  {
    if (menacerFound)
    {
      // 3. Read X and Y values
      s16 xVal = JOY_readJoypadX(JOY_2);
      s16 yVal = JOY_readJoypadY(JOY_2);
      char message[40];
      sprintf(message, "Menacer Values x:%d, y:%d      ", xVal, yVal);
      VDP_drawText(message, 8, 7);

      // 4. Read the button states
      u16 value = JOY_readJoypad(JOY_2);
      if (value & BUTTON_A)
      {
        VDP_drawText("A", 18, 9);
      }
      else
      {
        VDP_drawText(" ", 18, 9);
      }

      if (value & BUTTON_B)
      {
        VDP_drawText("B", 20, 9);
      }
      else
      {
        VDP_drawText(" ", 20, 9);
      }

      if (value & BUTTON_C)
      {
        VDP_drawText("C", 22, 9);
      }
      else
      {
        VDP_drawText(" ", 22, 9);
      }

      if (value & BUTTON_START)
      {
        VDP_drawText("S", 24, 9);
      }
      else {
        VDP_drawText(" ", 24, 9);
      }

    }
    SYS_doVBlankProcess();
  }
}

 

This code can also be found on GitHub here under ‘basic’.

Things to note:

  • The values returned from these functions do not correspond to exact pixel locations. The values returned range from -1 to 255. -1 is returned if the menacer is not pointed at the screen. If its pointed at the screen you’ll get a value between 0 and 255. The Genesis’ screen resolution is 320x224.
  • The X value for the Menacer is NOT continuous from 0 to 255. As you move left to right the numbers have a gap around 182 to 228 and may wrap around to 0.
  • The Y value for the Menacer is continuous from 0 to 255, but the X value is not.
  • I use Kega Fusion 3.64 because it has light gun support. This lets me write and test software fairly quickly. Fusion is a good emulator but nothing beats real hardware. If you try this out on real hardware it may not go so well.
Real World Testing

A menacer, like most light guns of its time, has a photo sensor that detects light from the electron gun of a CRT television. Unfortunately different TVs will have different brightness levels and the sensitivy of the light guns can also vary. So some light gun and TV combinations may have problems getting a good reading. If you take a look at this video:

 

You’ll see that no values are returned. So what do we do about this? We can turn the TV brightness up, which can help. Unfortunately, the black background was still too dark to get a good reading. So color choice is also important. In my testing, I’ve noticed my light guns are more sensitive to greens and blues than reds. For example, I originally had different look to my game, red trucks and dark blue uniforms on the enemies.

 

Notice the jumps When I aim at the red truck. THe target snaps to the brighter colors and avoids the mid-level red on the side of the truck. ONe way to work around this is to change the colors your using. Instead of using any color I want, I can limit myself to colors that are more readily detected up by the light gun.

 

Menacer Programming for the real world

As is, there are a number of problems in the basic code to fix. * Brightness/Colors Matter on real hardware. * Non-continuous X Coordinates. * Calibration.

Brightness/Color Choice

If you’re planning on running on emulators only, the color choice doesn’t really matter. The emulator probably won’t be checking for screen brightness. I want my games to work on real hardware. When creating graphics it’s a good idea to choose colors that can be picked up more easily than others.

An easy way to fix the basic demo is to simply set the background to a brighter (preferably not red) color:

PAL_setColor(0, 0x0844);

This helps immediately.

 

Another thing that can be done is to flash the screen white when the player pulls the trigger. This is something Konami did in Lethal Enforcers:

 

Flashing the screen a bit more work than choosing bright colors but is easy to do. When the player pulls the trigger, you can call PAL_setColor() with set of bright colors for a few frames.

First, I define an palette with bright colors.

const u16 palette_flash[32] =
{
  0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888,
  0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888,
  0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888,
  0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888, 0x0888
};

Second, when the player pulls the trigger, I set a variable (flashScreen) to flash the screen.

static void joypadHandler( u16 joypadId, u16 changed, u16 joypadState ) {
  if( joypadId == JOY_2  ) {

    // A
    if( changed == BUTTON_A && joypadState == BUTTON_A) {
      flashScreen = 3;
 ...
  

Third, the main loop changes the palette with PAL_setColors() when flashScreen is set and changes it back after a few frames.

  ///////////////////////////////////////////////////////////////////////////////////
  // Main Loop!
  while (TRUE)
  {
    if ( useFlash && flashScreen > 0)
    {
      if (flashScreen == 3)
      {
        PAL_setColors(0, palette_flash, 32, CPU);
      }
      else if (flashScreen == 1)
      {
        PAL_setColors(0, palette, 32, CPU);
      }
      --flashScreen;
    }
    ...
  }

While the screen colors are set to palette_flash values read from JOY_readJoypadX() and JOY_readJoypadY() be good values.

Color choice still matters. If you try to quickly switch a screen from full black (0x0000) to full white (0x0EEE) some TV’s may not react well. My TV doesn’t handle it well.

 

A less extreme change in brightness works better.  

Mapping JOY_readJoypadX Values to Pixel Coordinates

As seen above, the X values from JOY_readJoypadX() are not continuous.  

Kega Fusion’s Menacer gives me approximately: * 77 through 182 * 228 thorugh 255 * 0 to 22

I see similar numbers with a real Sega Menacer. When I pan my menacer from the left side of my TV to the right I see the following values (approximately): * 65 through 182 * 229 thorugh 255 * 0 to 3

I also have a modified Radica Menacer. When I move it across my TV from left to right I see * 79 through 182 * 229 thorugh 255 * 0 to 28

The values reported don’t use the entire range of 0 to 255. They start in the 60s/70s on the left and there’s a big jump from 182 to 228. Even worse, the values reported by the gun on the far right side of the screen (0 through 28) correspond to pixels on the left. So we clearly can’t use the raw X values in a game. Y values OTOH are continuous. They aren’t too far off from where I’m pointing but can still be improved. I’ll write more about this later in the calibration section.

Some searching brought me to this PDF file at spritesmind.net

It points out that : 1. The Vertical values can be directly converted to a Y pixels. 2. The Horizontal values reported are equivalent to two screen pixels (this doesn’t seem to be exactly true, but for this article I’ll assume it is.) 3. Commercial games use a lookup table to map Horizontal values to screen pixels

So I made lookup table that can hold the full range of positive (+0) values reported by JOY_readJoypadX(). The table is just a 256 element array of s16 values. I start populating the table with a position of ‘0’ at element 60 of the array. This is to get the lookup value close to the X position on the left side of a Genesis screen. As I move up through the array to index 182, I increase the position by 2. Atfter I reach index 182, I jump to index 228 and continue increasing the position until I reach index 255. At 255 I jump down to index 0 of the array and continue increasing the position until I reach index 59. The code looks like this:

static s16 xLookup[ 256 ];  // full rangeA for JOY_readJoypadX()

static void calculateXLookup() {
  // I'll start populating the table left to right with a value of 0
  s16 pos = 0;
  // I"m guessin the left side won't be lower than 60.
  for( int i=60; i < 183; ++i ) {
    xLookup[i] =  pos;
    pos += 2;
  }
  // handle the gap in X values 
  for( int i=228; i < 256; ++i ) {
    xLookup[i] =  pos;
    pos += 2;
  }
  // handle the wrap around to 0 and run up to 60.
  for( int i=0; i < 60; ++i ) {
    xLookup[i] =  pos;
    pos += 2;
  }

}

Once the lookup table is ready, it maps reasonable X positions to the xVals from JOY_readJoypadX()

  crosshairsPosX = xLookup[ xVal ];  // lookup the screen coordinate based on Horizontal Value
  crosshairsPosY = yVal;             // direct conversion of Vertical value

Depending on what you’re displaying, you may want to factor in an offset. In my case I want to center a 16 x 16 sprite. So I want to shift the sprite position to the left and up by 8 pixels. I do this by subtracting 8 from the X and Y values.

  crosshairsPosX = xLookup[ xVal ] - 8;
  crosshairsPosY = yVal  - 8;

This looks pretty good, but it’s not exaclty right. When I look down the sites of my menacers the target seems a bit off.

Calibration

So the lookup table gives pretty good results, but the positions don’t completely line up with the sights of my Menacer. Also, different Sega and Radica Menacers may have different offsets from mine, so just adding an arbitrary offset to line things up may not work for every light gun out there.

To compensate for this I can compute a final offset with code that:

  1. Has the player aim at a target at the center of the screen
  2. Has the player fire several shots with the trigger. The code saves the lookup values for each shot.
  3. When enough shots are saved, the code calculates an average X and Y position from the shots fired. This average is then used to calculate a X and Y offsets.
  4. During game play, the code uses the offsets from 3 to adjust the final screen position used.

To save the shots fired by the player I defined an array to store the X and Y positiions.

#define MAX_VALS 10
static s16 xValues[MAX_VALS];
static s16 yValues[MAX_VALS];
static u16 currentValue = 0;

The Joypad handler stores values when the trigger BUTTON_A is pulled

static void joypadHandler( u16 joypadId, u16 changed, u16 joypadState ) {
  if( joypadId == JOY_2  ) {

    // A
    if( changed == BUTTON_A && joypadState == BUTTON_A) {
      flashScreen = 3;
      if( calibrateMode ) {
        // get reading
        s16 xVal = JOY_readJoypadX(JOY_2);
        s16 yVal = JOY_readJoypadY(JOY_2);
        // store values for calculation
        if( currentValue < MAX_VALS ) {
          xValues[currentValue] = xLookup[xVal];
          yValues[currentValue] = yVal;
          ++currentValue;
        }
        if( currentValue == MAX_VALS ){
          calculateOffset();
          currentValue = 0;
          calibrateMode = FALSE;
        }
      }
    }

Once enough values have been collected, I calculate the X and Y offsets using a simple average and subtracting that from the center of the target.

static void calculateOffset() {
  s16 xTemp = 0;
  s16 yTemp = 0;
  // get average X and Y
  for( int i=0; i < currentValue; ++i ) {
    xTemp = xTemp + xValues[i];
    yTemp = yTemp + yValues[i];
  }
  //
  xTemp = xTemp / currentValue;
  yTemp = yTemp / currentValue;

  // center of target is at 160, 112
  xOffset = 160 - xTemp;
  yOffset = 112 - yTemp;

}

The crosshair positions can now now calculated with the lookup table and offset values

crosshairsPosX = xLookup[xVal] + xOffset - 8;
crosshairsPosY = yVal + yOffset - 8;
Hit Detection

Now that we have a reasonable X and Y location we can check for hits with game objects. As long as you know where your game objects are, it’s a simple matter to check if the X/Y coordinate overlaps your game objects. How you want to deal with hit detection is erally up to you.

I wrote a simple demo that just checks if the X/Y location of the gun is within a hit-box for several sprites. It’s avaiable on GitHub under collision

Konami Justifier

The other light guns are pretty similar to the Sega Menacer. I’ll just cover a few differences here. The most obvious differences are the calls to JOY_getPortType() and JOY_setSupport(). In this case you look for PORT_TYPE_JUSTIFIER and set SGDK’s light gun support to JOY_SUPPORT_JUSTIFIER_BLUE.

THe code looks very similar to the Menacer code.

    ///////////////////////////////////////////////////////////////////////////////////
    // Justifier Setup
    //
    calculateXLookup();

    // Asynchronous joystick handler. 
    JOY_setEventHandler (joypadHandler );


    // check Port 2 for the Konami Justifier
    bool justifierFound = FALSE;
    u8 portType = JOY_getPortType(PORT_2);
    if(portType == PORT_TYPE_JUSTIFIER )
    {
        JOY_setSupport(PORT_2, JOY_SUPPORT_JUSTIFIER_BLUE );
        justifierFound = TRUE;
        VDP_drawText("Justifier FOUND!", 11, 1);
    } else {
        VDP_drawText("Justifier NOT found.", 10, 1);
    }

Reading the Justifier is the same as reading the Menacer. You call JOY_readJoypadX() and JOY_readJoypadY() to get the X and Y positions.

        if( justifierFound ) {  
            // get the button states        
            u16 value = JOY_readJoypad(JOY_2);
            if( value & BUTTON_A ) {
                VDP_drawText("A", 18, 9);
            } else {
                VDP_drawText(" ", 18, 9);
            }


            // My blue justifier appears to return 34 through 176 when I use it on 
            // H32 mode.  
            // 
            // if both values are -1, the gun is aiming off screen. 
            s16 xVal = JOY_readJoypadX(JOY_2);
            s16 yVal = JOY_readJoypadY(JOY_2);
            char message[40];
            sprintf( message, "Justifier Values x:%d, y:%d      ", xVal, yVal );
            VDP_drawText(message, 7, 7 );

The actual values read from the Justifier are different from the Menacer. I have two blue Justifiers (sadly I’ve lost out on every pink Justifier auction I’ve tried). Unlike the Menacer, the X values from JOY_readJoypadX() are continuous.
As I move from the left side of my CRT to the right the values range from 31 through 180. My other justifier runs from 33 to 176. Both stop short of the discontinuity seen with the Menacer around 182. Interestingly, Kega Fusion’s Justifier crosses past the discontinuity at 182. When I scan from left to right the values are 35 through 182 then 228 to 235. As a result, the Justifier lookup is simpler than the menacer:

static void calculateXLookup() {
  // My blue justifiers return approximately 
  //  * 31 through 180 ( 149 values ) when I pan from left to right
  //  * 33 through 176 ( 143 values ) when I pan from left to right
  // 
  //  Kega fusion
  //  35 through 182, 228 to 235 from left to right
  //      ( 182-35) + (235- 228)  = 154 
  s16 pos = 0;
  for( int i=20; i < 183; ++i ) {
    xLookup[i] = pos;
    pos += 2;
  }
  for( int i=228; i < 255; ++i ) {
    xLookup[i] = pos;
    pos += 2;
  }

}

A full example can be found on GitHub

Sega Phaser

The Sega Master System Light Phaser works with the Sega Genesis/Mega Drive and SGDK provides support for it. Unlike the Menacer and Justifier, there’s no ID to detect, so setup code is even easier

    // Can't check for phaser with JOY_getPortType().  Just assume we've got a Phaser attached
    JOY_setSupport(PORT_2, JOY_SUPPORT_PHASER);

Reading values are exactly the same as the Menacer and Justifier.

        // if both values are -1, the gun is aiming off screen.
        s16 xVal = JOY_readJoypadX(JOY_2);
        s16 yVal = JOY_readJoypadY(JOY_2);

I also have a couple of Phasers. Like the Menacer, the X values from JOY_readJoypadX() are disjoint. As I move from the left side of my CRT to the right I see the following values (approximately): * 35 through 182 * 228 thorugh 235

My second phaser shows * 37 through 182 * 228 thorugh 237

So the X lookup table is identical to the Justifier version.

static void calculateXLookup()
{
  // My SMS Phasers appears to return
  //   30 through 182 then 229 through 235 when I pan left to right accross the screen
  //   37 through 182 then 229 through 237 when I pan left to right accross the screen
  
  s16 pos = 0;
  for (int i = 20; i < 183; ++i)
  {
    xLookup[i] = pos;
    pos += 2;
  }
  for (int i = 228; i < 256; ++i)
  {
    xLookup[i] = pos;
    pos += 2;
  }
}

A full example can be found on GitHub

Conclusion

SGDK’s controller functions provide a fairly easy and fun way to create light gun games. SGDK is a great tool for Sega Genesis development. It includes a variety of tools and resources that make it easy to create games for the Sega Genesis/Mega Drive.

About Me

Greg Gallardo

I'm a software developer and sys-admin in Iowa. I use C++, C#, Java, Swift, Python, JavaScript and TypeScript in various projects. I also maintain Windows and Linux systems on-premise and in the cloud ( Linode, AWS, and Azure )

Github

Mastodon

YouTube

About you

IP Address: 3.129.70.157

User Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)

Language:

Latest Posts

Iowa City Weather

Today

-- ˚F / 61 ˚F

Sunday

71 ˚F / 54 ˚F

Monday

64 ˚F / 46 ˚F

Tuesday

76 ˚F / 54 ˚F

Wednesday

76 ˚F / 56 ˚F

Thursday

72 ˚F / 51 ˚F

Friday

67 ˚F / 47 ˚F