Skip to content

Gamepad and Peripherals

Alekmaul edited this page Jul 19, 2026 · 1 revision

Input

PVSneslib is able to manage the SNES joypad, the multitap (MultiPlayer 5), the mouse and the SuperScope.

Joypad

    __--L--_________________--R--__           Button Colors:
   /    _                          \   PAL and Japan    North America
  |   _| |_                  (X)    |   X = Blue         X = Gray
  |  |_   _|  SLCT STRT   (Y)   (A) |   Y = Green        Y = Gray
  |    |_|                   (B)    |   A = Red          A = Purple
   \_________.-----------._________/    B = Yellow       B = Purple

PVSnesLib can handle the two SNES pads, which are identified with number 0 or 1 (up to 5 pads if a MultiPlayer 5 adapter is connected, see the MultiTap chapter below).

To get the values for internal pad management, the pads are automatically read for you every frame by the VBlank ISR — you no longer need to call a scan function yourself.

Then, let's use the padsCurrent function with the pad number you want to check. The default one is with number 0.

For example, use a short type variable to do that :

short pad0;
pad0 = padsCurrent(0);

To test is a specific button is pressed, just use the corresponding name :

  KEY_A      for pad A button.  
  KEY_B      for pad B button.  
  KEY_X      for pad X button.  
  KEY_Y      for pad Y button.  
  KEY_SELECT for pad SELECT button.  
  KEY_START  for pad START button.  
  KEY_RIGHT  for pad RIGHT button.  
  KEY_LEFT   for pad LEFT button.  
  KEY_DOWN   for pad DOWN button.  
  KEY_UP     for pad UP button.  
  KEY_R      for Right shoulder button.  
  KEY_L      for Left shoulder button.  

So, you can use if (pad0 & KEY_A) to know if button A is pressed or if (pad0 & (KEY_A | KEY_X)) to know if button A or button X are pressed. Notice that we used the | operator to add different buttons, not the & operator, this is because | is like the adding operation.

Also, if ( (pad0 & KEY_A) && (pad0 & KEY_X) ) (or if ( (pad0 & (KEY_A | KEY_X) == (KEY_A | KEY_X) )) is used to know if button A and button X are pressed.

At least, the pad is refreshed during VBL (thanks to the VBlank ISR), so it is no more needed to call any scan function to refresh pad values.

     // Get current #0 pad
     pad0 = padsCurrent(0);
		
     // Update display with current pad
     if (pad0 & KEY_A) {
       consoleDrawText(12,10,"A PRESSED");
     }

MultiTap

The MultiPlayer 5 (multitap) adapter lets you plug up to 4 extra pads on controller port 2, giving a total of 5 pads (numbered 0 to 4) that you can read with the very same padsCurrent / padsDown / padsUp functions used for the standard joypads.

First, call detectMPlay5() once (typically on boot) to detect the adapter and populate the snes_mplay5 variable (0 if not connected, 1 if connected):

extern u8 snes_mplay5; /*!< \brief 1 if MultiPlay5 is connected */

// Check Multiplayer 5
detectMPlay5();

CAUTION: REG_WRIO ($4201) must not be written to while MultiPlayer5 is active (bit 7 of REG_WRIO must be set when Auto Joy reads the controllers, shortly after the VBlank period starts).

Once snes_mplay5 is set, the VBlank ISR automatically reads all 5 pads for you, exactly like it does for the standard 2 pads. You can then simply loop over pad indexes 0 to 4 with padsCurrent:

u8 i;
unsigned short pad0;

if (snes_mplay5)
{
    // Get all pads values
    for (i = 0; i < 5; i++)
    {
        pad0 = padsCurrent(i);

        // Update display with current pad
        if (pad0 & KEY_A) {
            consoleDrawText(i * 6, 8, "A");
        }
    }
}
else
{
    consoleDrawText(3, 7, "NO MULTIPLAYER 5 CONNECTED!");
}

To get a better idea on how to read the MultiPlayer 5 pads, please see the following example:

  • snes-examples/input/multiplay5: prints, for each of the 5 possible pads, which button is currently pressed on screen (columns P1 to P5).

Mouse

snes_mouse has to be turned on (snes_mouse = 1). This is set to 0 by default after consoleInit().

You can set snes_mouse to 1 by calling initMouse() or detectMouse().

  • initMouse() will also clear the mouse variables and set the initial mouse sensitivity.
  • detectMouse() will only set snes_mouse to 1 if it detects a mouse on one of the console ports.

When snes_mouse is non-zero, the VBlank ISR will read mouse data from the controller ports and update the following mouse state variables (the array index specifies the controller port [0 or 1]):

extern u8 mouseConnect[2];        /*!< \brief 1 if Mouse present */
extern u8 mouseButton[2];         /*!< \brief Mouse buttons that are pressed on this frame (Click mode). */
extern u8 mousePressed[2];        /*!< \brief Mouse buttons that are currently pressed, stays until is unpressed (Turbo mode). */
extern u8 mousePreviousPressed[2];/*!< \brief Mouse buttons held or pressed in the previous frame */
extern u8 mouse_x[2];             /*!< \brief Mouse horizontal displacement. daaaaaaa, d = direction (1: left, 0: right), a = acceleration. */
extern u8 mouse_y[2];             /*!< \brief Mouse vertical displacement. daaaaaaa, d = direction (1: up, 0: down), a = acceleration. */
extern u8 mouseSensitivity[2];    /*!< \brief Mouse sensitivity (0-2) */

To use the mouse, we first call initMouse() or detectMouse() to populate snes_mouse to 1. It can be called at boot and like this:

// Enable mouse reading and set the initial mouse sensitivity to medium
initMouse(MOUSE_SLOW);
WaitForVBlank(); // Let's make sure we read the mouse for the first time after initMouse()

if (mouseConnect[0] == false)
    consoleDrawText(3,5,"NO MOUSE PLUGGED ON PORT 0");
else
    consoleDrawText(5,5,"MOUSE PLUGGED ON PORT 0");

Reading the two mouse buttons is done by reading the mouseButton or mousePressed variables in the same manner a detecting joypad buttons. mouseButton will contain buttons that are newly pressed on this frame (good for click-style actions such as pressing a UI button once) and mousePressed contains the buttons that are currently depressed / held down (good for dragging or drawing):

// mouseButton works as a one frame value, so it gets released shortly after pressing the button.
// Good for clicking stuff that need to be called once, like buttons. mouseButton is 0 if there is no mouse connected.
if (mouseButton[0] & mouse_L) {
    // do something once, like clicking on a UI button
}

// mousePressed works as a turbo switch, it stays 1 until it gets released, then it goes back to 0.
// Good for dragging or writing.
switch (mousePressed[0]) {
    case mouse_L:
        consoleDrawText(7,10,"LEFT BUTTON PRESSED ");
        break;
    case mouse_R:
        consoleDrawText(7,10,"RIGHT BUTTON PRESSED");
        break;
    case mouse_L + mouse_R:
        consoleDrawText(7,10,"BOTH BUTTONS PRESSED");
        break;
}

I recommend using this code structure to convert raw displacement data into usable values:

u16 p1_mouse_x = 0x80;
u16 p1_mouse_y = 0x70;
u16 p2_mouse_x = 0x80;
u16 p2_mouse_y = 0x70;

if (mouse_x[0] & 0x80)
    p1_mouse_x -= mouse_x[0] & 0x7F;
else
    p1_mouse_x += mouse_x[0] & 0x7F;

if (mouse_y[0] & 0x80)
    p1_mouse_y -= mouse_y[0] & 0x7F;
else
    p1_mouse_y += mouse_y[0] & 0x7F;

// And set some boundaries
if (p1_mouse_x > 0xFF00) p1_mouse_x = 0;
if (p1_mouse_x > 0xFF)   p1_mouse_x = 0xFF;
if (p1_mouse_y > 0xFF00) p1_mouse_y = 0;
if (p1_mouse_y > 0xEF)   p1_mouse_y = 0xEF;

// You can then feed p1_mouse_x / p1_mouse_y straight into oamSet() to move a cursor sprite
oamSet(0, p1_mouse_x, p1_mouse_y, 3, 0, 0, 0, mouseConnect[1]);

To get a better idea on how to read the mouse, please see the following examples:

  • snes-examples/input/mouse: a mouse demo that can read 2 mice at the same time (one on each controller port). It draws a sprite cursor that follows each connected mouse, shows the current X/Y position on screen, reports which button(s) are pressed, and lets you click on-screen SLOW/NORMAL/FAST buttons (drawn on the background) to change the mouse sensitivity of each port with mouseSetSensitivity.
  • snes-examples/input/mouse-data-test: prints the mouse data from controller port 2 to the user.

Sensitivity

The Nintendo mouse has 3 sensitivity settings (0=low, 1=medium, 2=high) that can be adjusted by sending cycle-sensitivity commands to the mouse through the controller port.

The Hyperkin clone mouse will always report 0 sensitity and will ignore cycle sensitivity commands.

The u8 mouseRequestChangeSensitivity[2] variable is used to signal to the VBlank ISR that the MainLoop wants to change the Nintendo Mouse's sensitivity setting. Cycling the sensitity is done in the VBlank ISR to prevent a timing conflict with the SNES's Automatic Joypad read.

To adjust the mouse sensitity, either modify mouseRequestChangeSensitivity (see input.h) or call one of these functions:

  • void mouseCycleSensitivity(u16 port); - cycles the mouse sensitivity once (incrementing the sensitivity)
  • void mouseCycleSensitivityTwice(u16 port); - cycles the mouse sensitity twice (decrementing the sensitivity)
  • void mouseSetSensitivity(u16 port, u8 sensitivity); - changes the mouse sensitivity to sensitivity & 3. (A sensitivity value of 3 is invalid)

Please be aware that the reported sensitivity in the mouseSensitivity variable will be delayed one frame.

SuperScope

First, we might use detectSuperScope() on boot to detect Super Scope presence. Other way is to force detection by populating snes_sscope to 1 manually, but we dont need to do that if we call this function. We need to call this function everytime Scope gets disconnected from the system, a usefull way to do it is inside this conditional:

if (snes_sscope == false)
{
  detectSuperScope();
  // some other code you might need in your program, like displaying warning messages and stopping your game.
}

Here is a brief explanation of every variable we might be using:

extern u16 scope_holddelay; /*! \brief Hold delay. */
extern u16 scope_repdelay;  /*! \brief Repeat rate. */
extern u16 scope_shothraw;  /*! \brief Horizontal shot position, not adjusted. */
extern u16 scope_shotvraw;  /*! \brief Vertical shot position, not adjusted. */
extern u16 scope_shoth;     /*! \brief Horizontal shot position, adjusted for aim. */
extern u16 scope_shotv;     /*! \brief Vertical shot position, adjusted for aim. */
extern u16 scope_centerh;   /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */
extern u16 scope_centerv;   /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */
extern u16 scope_down;      /*! \brief flags that are currently true.*/
extern u16 scope_now;       /*! \brief flags that have become true this frame.*/
extern u16 scope_held;      /*! \brief flagsthat have been true for a certain length of time.*/
extern u16 scope_last;      /*! \brief flags that were true on the previous frame.*/
extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot was fired.*/
for scope_down, scope_now, scope_held, scope_last, we need to mask our bits with this usefull bits:

typedef enum SUPERSCOPE_BITS
{
    SSC_FIRE = BIT(15),     //!< superscope FIRE button.
    SSC_CURSOR = BIT(14),   //!< superscope CURSOR button.
    SSC_PAUSE = BIT(12),    //!< superscope PAUSE button.
    SSC_TURBO = BIT(13),    //!< superscope TURBO flag.
    SSC_OFFSCREEN = BIT(9), //!< superscope OFFSCREEN flag.
    SSC_NOISE = BIT(8),     //!< superscope NOISE flag.
} SUPERSCOPE_BITS;

Aim calibration

Because every SuperScope unit and every TV can be aimed slightly differently, games generally start with an aim adjustment step: the player is asked to shoot the physical center of the screen once, and the game stores the offset between the raw shot position and the screen center so every following shot can be corrected.

// Wait for a first shot (fire pressed then released) to calibrate...
if (scope_down & SSC_FIRE) { /* ... */ }

// Start aim adjust process: store the raw shot position relative to the center of screen
// so that following shots match the SuperScope's physical aim.
scope_centerh = 0x80 - scope_shothraw;
scope_centerv = 0x70 - scope_shotvraw;

// From now on, scope_shoth / scope_shotv give you the corrected (aim-adjusted) shot position,
// ready to be used directly as sprite coordinates, e.g.:
oamSetXY(0, scope_shoth - 0x08, scope_shotv - 0x08);

scope_down tells you which SuperScope buttons/flags are currently held, scope_now tells you which ones just became true this frame, and scope_held tells you which ones have been held for a while — this last one is handy to implement an auto/turbo-fire style behaviour once the FIRE button is held down long enough:

// Fire rate tuning, usually set once before entering the main game loop
scope_holddelay = 60;          // frames to wait before FIRE is considered "held"
scope_repdelay  = slow_fire_rate; // repeat rate once FIRE is held

if (scope_held & SSC_FIRE) {
    // the trigger has been held long enough: apply a slower, sustained fire rate
}
if (scope_down & SSC_FIRE) {
    // the trigger was just pulled: apply the normal fire rate
}

And that's most of it. You can look inside the example file snes-examples/input/superscope to have an idea of how you can program Super Scope games. That example implements a small shooting-gallery style demo that:

  • waits for a SuperScope to be plugged into controller port 2 (detectSuperScope() is polled every frame until snes_sscope becomes true);
  • walks the player through the aim-calibration step described above (shoot the center of the screen, then a second shot to verify the aim);
  • reads SSC_FIRE to spawn bullet sprites at the corrected scope_shoth / scope_shotv position, with a bit of random spread and simulated gravity;
  • reads SSC_PAUSE to pause the game and go back to aim calibration, and SSC_CURSOR to resume/restart, moving between game states with goto labels (ADJUST_AIM, CONTINUE_GAME, PLAY_GAME, START_OVER);
  • detects bullet/target collisions by comparing OAM sprite coordinates, and animates targets being destroyed.

And that's most of it. You can look inside the example file (snes-examples/input folder) to have an idea of how you can program Super Scope games.

Clone this wiki locally