OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
Controller Input Tutorial

This tutorial covers reading SNES controllers including button detection and multi-player support.

SNES Controller Layout

L R
┌───────────────────────────────────────┐
│ ┌───┐ ┌───┐ │
│ │ ↑ │ (X) │ X │ │
│ ┌───┼───┼───┐ (Y) (A)├───┤ │
│ │ ← │ │ → │ [SEL][STA] │ Y │ A │ │
│ └───┼───┼───┘ ├───┼───┤ │
│ │ ↓ │ │ B │ │
│ └───┘ └───┘ │
└───────────────────────────────────────┘

Button Constants

OpenSNES defines these button masks in input.h:

// D-Pad
#define KEY_UP 0x0800
#define KEY_DOWN 0x0400
#define KEY_LEFT 0x0200
#define KEY_RIGHT 0x0100
// Face Buttons
#define KEY_A 0x0080
#define KEY_B 0x8000
#define KEY_X 0x0040
#define KEY_Y 0x4000
// Shoulder Buttons
#define KEY_L 0x0020
#define KEY_R 0x0010
// Start/Select
#define KEY_START 0x1000
#define KEY_SELECT 0x2000

Reading Controllers

Direct Register Access (Recommended)

#include <snes.h>
u16 pad;
while (1) {
// Wait for auto-joypad read to complete
while (REG_HVBJOY & 0x01) {}
// Read controller 1
pad = REG_JOY1L | (REG_JOY1H << 8);
// Check for disconnected controller
if (pad == 0xFFFF) {
// No controller connected
continue;
}
// Check buttons
if (pad & KEY_A) {
// A button pressed
}
if (pad & KEY_UP) {
// D-pad up pressed
}
}
void WaitForVBlank(void)
Wait for next VBlank period.
#define REG_JOY1H
Joypad 1 data high (R).
Definition registers.h:334
#define REG_JOY1L
Joypad 1 data low (R).
Definition registers.h:331
#define REG_HVBJOY
H/V blank and joypad status (R).
Definition registers.h:307
#define KEY_A
Definition input.h:84
#define KEY_UP
Definition input.h:78
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
OpenSNES common-case master header.

Reading Controller 2

u16 pad1, pad2;
// Wait for auto-joypad
while (REG_HVBJOY & 0x01) {}
// Read both controllers
pad1 = REG_JOY1L | (REG_JOY1H << 8);
pad2 = REG_JOY2L | (REG_JOY2H << 8);
#define REG_JOY2H
Joypad 2 data high (R).
Definition registers.h:340
#define REG_JOY2L
Joypad 2 data low (R).
Definition registers.h:337

Edge Detection

Detect button presses (not just held state):

u16 pad_current;
u16 pad_previous = 0;
u16 pad_pressed;
while (1) {
while (REG_HVBJOY & 0x01) {}
// Read current state
pad_current = REG_JOY1L | (REG_JOY1H << 8);
// Detect new presses (down this frame, not last frame)
pad_pressed = pad_current & ~pad_previous;
// Save for next frame
pad_previous = pad_current;
// Skip if disconnected
if (pad_current == 0xFFFF) {
pad_pressed = 0;
}
// Now use pad_pressed for one-shot actions
if (pad_pressed & KEY_A) {
// A was just pressed (fires once)
player_jump();
}
// Use pad_current for continuous actions
if (pad_current & KEY_RIGHT) {
// Right is held (fires every frame)
}
}
static s16 player_x
Player X position in screen coordinates.
Definition main.c:56
#define KEY_RIGHT
Definition input.h:81

Movement Example

u16 player_x = 128;
u16 player_y = 112;
void update_player(u16 pad) {
// Movement with boundary checking
if (pad & KEY_UP) {
if (player_y > 0) player_y--;
}
if (pad & KEY_DOWN) {
if (player_y < 224) player_y++;
}
if (pad & KEY_LEFT) {
if (player_x > 0) player_x--;
}
if (pad & KEY_RIGHT) {
if (player_x < 248) player_x++;
}
}
static s16 player_y
Player Y position in screen coordinates.
Definition main.c:58
#define KEY_DOWN
Definition input.h:79
#define KEY_LEFT
Definition input.h:80

Two-Player Example

// Player positions
u16 p1_x = 64, p1_y = 112;
u16 p2_x = 192, p2_y = 112;
while (1) {
while (REG_HVBJOY & 0x01) {}
// Read both controllers
u16 pad1 = REG_JOY1L | (REG_JOY1H << 8);
u16 pad2 = REG_JOY2L | (REG_JOY2H << 8);
// Player 1 movement
if (pad1 != 0xFFFF) {
if (pad1 & KEY_UP) { if (p1_y > 0) p1_y--; }
if (pad1 & KEY_DOWN) { if (p1_y < 224) p1_y++; }
if (pad1 & KEY_LEFT) { if (p1_x > 0) p1_x--; }
if (pad1 & KEY_RIGHT) { if (p1_x < 248) p1_x++; }
}
// Player 2 movement
if (pad2 != 0xFFFF) {
if (pad2 & KEY_UP) { if (p2_y > 0) p2_y--; }
if (pad2 & KEY_DOWN) { if (p2_y < 224) p2_y++; }
if (pad2 & KEY_LEFT) { if (p2_x > 0) p2_x--; }
if (pad2 & KEY_RIGHT) { if (p2_x < 248) p2_x++; }
}
// Update sprites
oamSet(0, p1_x, p1_y, 0, 0, 0, 0); // Player 1
oamSet(1, p2_x, p2_y, 0, 1, 0, 0); // Player 2 (palette 1)
}
void oamSet(u16 id, u16 x, u16 y, u16 tile, u16 palette, u16 priority, u16 flags)
Set sprite properties.
void oamUpdate(void)
Copy OAM buffer to hardware.

Button Combinations

// Check for multiple buttons
if ((pad & KEY_A) && (pad & KEY_B)) {
// A + B pressed together
}
// Check for button + direction
if ((pad & KEY_A) && (pad & KEY_DOWN)) {
// Down + A (e.g., slide attack)
}
// Create combo masks
#define COMBO_HADOUKEN (KEY_DOWN | KEY_RIGHT | KEY_A)
if ((pad & COMBO_HADOUKEN) == COMBO_HADOUKEN) {
// All combo buttons pressed
}
#define KEY_B
Definition input.h:74

Menu Navigation

u8 menu_selection = 0;
const u8 MENU_ITEMS = 4;
if (pad_pressed & KEY_UP) {
if (menu_selection > 0) {
menu_selection--;
}
}
if (pad_pressed & KEY_DOWN) {
if (menu_selection < MENU_ITEMS - 1) {
menu_selection++;
}
}
if (pad_pressed & KEY_A) {
// Confirm selection
activate_menu_item(menu_selection);
}
if (pad_pressed & KEY_B) {
// Cancel / go back
close_menu();
}
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47

Is a Pad Plugged In?

An empty port and an idle pad both read as "no button pressed". To tell them apart, padIsConnected(pad) returns 1 when a standard pad answered on that port during the last VBlank:

if (!padIsConnected(1)) {
textPrintAt(4, 20, "PLUG IN CONTROLLER 2");
}
u8 padIsConnected(u8 pad)
Check if controller is connected.
void textPrintAt(u8 x, u8 y, const char *str)
Print a string at specific position.

Pads are 0 and 1; a mouse or a Super Scope on the port reads 0 (it is not a pad). The answer is valid from the first VBlank after consoleInit().

The SNES Mouse

mouseInit(port) looks for a mouse on port 0 or 1 and, if it finds one, turns on mouse reading in the NMI handler. It reads the result of the automatic joypad read, so call it after a WaitForVBlank(), as examples/input/mouse does:

s16 cursor_x = 128, cursor_y = 112;
if (!mouseInit(0)) {
textPrintAt(2, 5, "No mouse detected.");
}
while (1) {
cursor_x += mouseGetX(0); /* signed motion since last frame */
cursor_y += mouseGetY(0); /* positive = down */
/* click: pressed this frame */
}
/* right button is down */
}
}
s16 mouseGetY(u8 port)
Get Y displacement since last frame.
u8 mouseButtonsHeld(u8 port)
Get currently held mouse buttons.
#define MOUSE_BUTTON_LEFT
Definition input.h:224
u8 mouseInit(u8 port)
Initialize mouse on given port.
s16 mouseGetX(u8 port)
Get X displacement since last frame.
#define MOUSE_BUTTON_RIGHT
Definition input.h:225
u8 mouseButtonsPressed(u8 port)
Get newly pressed mouse buttons this frame.
signed short s16
16-bit signed integer (-32768 to 32767)
Definition types.h:50

The mouse reports motion, not a position: keep your own cursor and clamp it to the screen. mouseGetX() / mouseGetY() return -127..+127. mouseIsConnected(port) says whether the NMI handler still sees the mouse this frame (it can be unplugged mid-game).

Sensitivity has three steps (MOUSE_SENS_LOW, MOUSE_SENS_MEDIUM, MOUSE_SENS_HIGH). mouseSetSensitivity(port, level) queues the change, which the NMI handler applies; mouseGetSensitivity(port) returns the level in effect, so it may lag one frame behind the request.

The Super Scope

The Super Scope is a light gun on port 2 only. When it sees the screen's light it makes the PPU latch its horizontal and vertical counters (a transition on pin 6 of the second controller connector — fullsnes, PPU timers and status; anomie's register doc, $213C/$213D). The NMI handler reads that latch and the gun's buttons every frame once scopeInit() has found it:

if (scopeInit()) {
/* ask the player to shoot the centre of the screen, then: */
}
while (1) {
u16 x = scopeGetX(); /* 0-255, calibrated */
u16 y = scopeGetY(); /* 0-223 */
/* hit test at (x, y) */
}
}
#define SSC_FIRE
Definition input.h:347
void scopeCalibrate(void)
Calibrate aim from a center-screen shot.
u16 scopeButtonsPressed(void)
Get newly pressed buttons this frame.
u16 scopeGetX(void)
Get calibration-adjusted H position.
u8 scopeInit(void)
Detect Super Scope on port 2.
u16 scopeGetY(void)
Get calibration-adjusted V position.

The raw counter values (scopeGetRawX(), scopeGetRawY()) are not screen coordinates: the visible picture starts part-way along the horizontal counter, and every gun aims a little differently. scopeCalibrate() takes the last shot as the screen centre (128, 112) and offsets every later reading; scopeGetX() / scopeGetY() return the corrected position. examples/input/superscope shows the whole detect / calibrate / fire loop.

Buttons come as a mask of SSC_FIRE, SSC_CURSOR, SSC_TURBO, SSC_PAUSE, plus two status flags, SSC_OFFSCREEN (aimed off the TV) and SSC_NOISE:

Call Returns
scopeButtonsHeld() buttons down now (like padHeld())
scopeButtonsPressed() buttons that went down this frame
scopeButtonsRepeat() a held button re-triggering: first after the hold delay, then every repeat delay
scopeSinceShot() frames since the gun last latched a position

scopeSetHoldDelay(frames) (default 60) and scopeSetRepeatDelay(frames) (default 20) tune the auto-repeat. scopeIsConnected() returns whether scopeInit() found the gun, and drops to 0 if the gun is unplugged; a gun plugged in later is not detected on its own, so call scopeInit() again (for example once per frame on a "connect the Super Scope" screen) to notice a gun plugged in later.

Hardware Registers

For reference, the joypad registers:

Register Address Description
REG_JOY1L $4218 Controller 1, low byte
REG_JOY1H $4219 Controller 1, high byte
REG_JOY2L $421A Controller 2, low byte
REG_JOY2H $421B Controller 2, high byte
REG_JOY3L $421C Controller 3 (multitap)
REG_JOY3H $421D Controller 3 (multitap)
REG_JOY4L $421E Controller 4 (multitap)
REG_JOY4H $421F Controller 4 (multitap)
REG_HVBJOY $4212 Status (bit 0 = auto-read busy)

Tips

  1. Always wait for auto-joypad - Check REG_HVBJOY & 0x01 before reading
  2. Check for disconnected controllers - 0xFFFF means no controller
  3. Use edge detection for menus - Prevents rapid-fire selection
  4. Use held state for movement - Smoother continuous motion

Example

See examples/input/two_players/ for a complete two-player demo.

Next Steps