This tutorial covers reading SNES controllers including button detection and multi-player support.
OpenSNES defines these button masks in input.h:
Detect button presses (not just held state):
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:
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().
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:
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 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:
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.
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) |
See examples/input/two_players/ for a complete two-player demo.