@spud.gg/api v0.0.5
spud!
what is this?
Two things: (1) a library that improves the experience of working with the browser Gamepad API, and (2) a way for your game to communicate, optionally, with spud.gg, which is a platform for hosting gamepad-enabled browser games. But you don't need to host your game on spud.gg to use this library! It was designed so that However, if your game is hosted on spud, you'll get a few extra benefits such as player names and (soon) ways to support local high scores, achievements, online lobbies, and leaderboards.
installation
# with npm
npm i @spud.gg/api
# ... or the equivalent call to bun, pnpm, yarn, etc.
features
- Built-in controller deadzone handling with axis normalization
- Simplified wrappers for haptics
- Simplified gamepad button handling
- Utilities for snapping analog sticks to 4 or 8 directions
- Optional communication with the spud.gg parent window for player names, colors, and the pause menu
table of contents
basic usage
import { spud, clearInputs, Button, HapticIntensity } from '@spud.gg/api';
function gameLoop() {
// count the number of connected gamepads
console.log(`there are ${spud.playerCount} connected controllers`);
// loop through only the connected gamepads
spud.connectedPlayers.forEach((player) => {
console.log(player.name, player.gamepadInfo.vendor, player.gamepadInfo.model);
});
// or target just a specific player
const isPlayer3PressingSelect = spud.p3.isButtonDown(Button.Select);
// if p1 just pressed the left dpad button
if (spud.p1.buttonJustPressed(Button.DpadLeft)) {
// provide 50ms of haptic feedback
spud.p1.rumble(50);
// and read player-specific settings from the spud parent window (with defaults if not on spud.gg)
console.log(spud.p1.name, spud.p1.color);
}
// if all connected players are simultaneously pressing Start
if (spud.allPlayers.isButtonDown(Button.Start)) {
// ...
}
// read current axis values
const {
// normalized x and y to account for deadzones
x,
y,
// raw axis values from the gamepad
rawX,
rawY,
// normalized x and y values that snap to 4-way or 8-way directions
snap4,
snap8,
} = spud.p1.leftStick;
// simplified access to trigger axes
const { value, rawValue, rumble } = spud.p1.leftTrigger;
// start a 100ms Heavy haptic in p1's left trigger
rumble(100, HapticIntensity.Heavy);
// if any player just pressed the A (Xbox) or ✕ (PlayStation) button...
if (spud.anyPlayer.buttonJustPressed(Button.South)) {
// ...
}
// in addition, you always have access to the raw Gamepad, so this:
spud.p1.gamepad?.vibrationActuator.reset();
// ...is the same as this:
spud.p1.hapticReset();
// call this function once, at the end of your update cycle, in order to save
// a snapshot of the previous buttons down and other state. this is how we're
// able to provide utilities like `buttonJustReleased` which compare current
// and prior gamepad states.
clearInputs();
// loop infinitely
requestAnimationFrame(gameLoop);
}
gameLoop();
api reference
root level, core api
import { spud, clearInputs } from '@spud.gg/api';
import { yourGame } from './your-example-project';
// (optional)
// listen for pause/resume/other events from the parent spud.gg window
if (!spud.isListening) {
spud.listen();
}
function gameLoop() {
if (spud.isPaused) {
yourGame.drawPauseOverlay();
requestAnimationFrame(gameLoop);
return;
}
yourGame.update();
clearInputs();
yourGame.draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
spud
– The core part of this API with properties likep1
(player 1 controller),connectedPlayers
, etc.isListening
–true
if we're currently listening for pause/resume/other events from the spud.gg parent window. Not needed if you're not planning to host your game on the spud platform. This also adds automatic pausing when there's avisibilitychange
event causing your game to becomehidden
, such as changing tabs.listen()
– Call this function once to set up the above event listeners.
clearInputs()
- At the end of each game update cycle you must call this function once. Otherwise, utilities that rely on the prior gamepad state will not work.
player controllers
Each player (spud.p1
–spud.p4
) provides:
name
- Player name (only when in a spud.gg iframe, falling back toP1
/P2
/P3
/P4
)color
- Player color (blue, yellow, red, green by default)index
-0
|1
|2
|3
gamepad
- Raw Gamepad objectgamepadInfo
- Object with data about the player's gamepad, including:vendor
– AGamepadVendor
stringvendorId
– Unique identifier for the gamepad vendor from the gamepad'sid
fieldmodel
– AGamepadModel
stringmodelId
– Unique identifier for the gamepad model from the gamepad'sid
fieldid
,index
,mapping
,timestamp
– Properties that come straight from the browser gamepad fields
justConnected
-true
if the gamepad just connected this framejustDisconnected
-true
if the gamepad just disconnected this frameisButtonDown(button)
- Checks if a button is currently pressedwasButtonDown(button)
- Checks if a button was pressed in the previous framebuttonJustPressed(button)
- Checks if a button was just pressed (current frame && not previous)buttonJustReleased(button)
- Checks if a button was just released (previous frame && not current)buttonHeld(button)
- Checks if a button is being held (pressed in both current and previous frame)buttonsDown
- ASet
of all currently pressed buttonspreviousButtonsDown
- ASet
of all buttons pressed in the previous frameleftStick
/rightStick
- Analog stick accessors with properties:x
/y
- Normalized values that account for deadzonesrawX
/rawY
- Raw axis values directly from the gamepadsnap4
- Normalized values that snap to 4-way directions (up/down/left/right)snap8
- Normalized values that snap to 8-way directions (including diagonals)deadzone
- The amount of deadzone for the stick
leftTrigger
/rightTrigger
- Trigger accessors with properties:value
- Normalized value that accounts for deadzonerawValue
- Raw axis value directly from the gamepaddeadzone
- The amount of deadzone for the triggerrumble(duration?, intensity?)
- Method to provide haptic feedback to the triggerduration
: number - Duration in milliseconds (optional, default:30
)intensity
: HapticIntensity | number - Intensity of the rumble (optional, default:Balanced
)
rumble(duration?, intensity?)
- Easy vibration effectduration
: number - Duration in milliseconds (optional, default:30
)intensity
: HapticIntensity - Intensity of the rumble (optional, default:Balanced
)
haptic({ duration?, strong?, weak?, leftTrigger?, rightTrigger? })
- Fine-grained haptic controlduration
: number - Duration of haptic effect in milliseconds (optional, default:0
)strong
: number - Intensity of strong actuator (0
-1
) (optional, default:0
)weak
: number - Intensity of weak actuator (0
-1
) (optional, default:0
)leftTrigger
: number - Intensity of left trigger actuator (0
-1
) (optional, default:0
)rightTrigger
: number - Intensity of right trigger actuator (0
-1
) (optional, default:0
)
hapticReset()
- Cancels any ongoing haptic feedback
Examples
// check if a button is currently pressed
if (spud.p1.isButtonDown(Button.South)) {
console.log('A/✕ button is being pressed');
}
// check for buttons that were just pressed this frame
if (spud.p2.buttonJustPressed(Button.North)) {
console.log('Y/△ button was just pressed');
}
// using analog stick values with deadzone handling
const { x, y } = spud.p1.leftStick;
yourGame.moveCharacter(x, y); // values are normalized with deadzone applied
// using analog stick with 8-way directional snapping
const { x: snapX, y: snapY } = spud.p1.rightStick.snap8;
yourGame.aimWeapon(snapX, snapY);
// using trigger values
if (spud.p1.rightTrigger.value > 0.8) {
yourGame.fireWeapon();
// apply haptic feedback to the trigger
spud.p1.rightTrigger.rumble(100, HapticIntensity.Heavy);
}
// apply general haptic feedback
spud.p1.rumble(200, HapticIntensity.Balanced);
// custom haptic control
spud.p1.haptic({
duration: 300,
strong: 0.7,
weak: 0.3,
leftTrigger: 0.5,
rightTrigger: 0,
});
// cancel all haptic effects
spud.p1.hapticReset();
constants and types
Button
- Button constants that map to their index in the standard mapping. eg. DpadUp, Start, Select, LeftShoulder, North/South/East/West (maps to Xbox Y/A/X/B or PlayStation △/✕/□/○), etc.Axis
- Gamepad axes --Axis.LeftStickX
,Axis.LeftStickY
,Axis.LeftTrigger
, etc.Stick
- Joysticks --Stick.Left
,Stick.Right
Player
-Player.P1
,Player.P2
,Player.P3
,Player.P4
GamepadVendor
- Controller vendors --GamepadVendor.Microsoft
,GamepadVendor.Sony
,GamepadVendor.Nintendo
,GamepadVendor.Valve
,GamepadVendor.Generic
GamepadModel
- Controller models --DualShock 4
,DualSense Edge
,Xbox Wireless Controller (2020)
, etc.HapticIntensity
-HapticIntensity.Light
,HapticIntensity.Balanced
,HapticIntensity.Heavy
Examples
// button constants
if (spud.p1.buttonJustPressed(Button.South)) yourGame.jump();
if (spud.p1.isButtonDown(Button.DpadLeft)) yourGame.moveLeft();
// axis constants
const leftX = spud.p1.gamepad?.axes[Axis.LeftStickX] ?? 0;
const rightTriggerValue = spud.p1.gamepad?.axes[Axis.RightTrigger] ?? 0;
// haptic intensity levels
spud.p1.rumble(100, HapticIntensity.Light);
spud.p1.rumble(100, HapticIntensity.Balanced);
spud.p1.rumble(100, HapticIntensity.Heavy);
player groups
utilities for handling multiple players at once.
anyPlayer
isButtonDown(button)
- Checks if any connected player is pressing the buttonwasButtonDown(button)
- Checks if any connected player was pressing the button in the previous framebuttonJustPressed(button)
- Checks if any connected player just pressed the buttonbuttonJustReleased(button)
- Checks if any connected player just released the buttonbuttonHeld(button)
- Checks if any connected player is holding the button
allPlayers
If there are 3 connected gamepads, allPlayers
refers to P1, P2, and P3.
isButtonDown(button)
- Checks if all connected players are pressing the buttonbuttonHeld(button)
- Checks if all connected players are holding the buttonhaptic({ duration?, strong?, weak?, leftTrigger?, rightTrigger? })
- Apply haptic feedback to all connected playersduration
: number - Duration of haptic effect in milliseconds (optional, default:0
)strong
: number - Intensity of strong actuator (0
-1
) (optional, default:0
)weak
: number - Intensity of weak actuator (0
-1
) (optional, default:0
)leftTrigger
: number - Intensity of left trigger actuator (0
-1
) (optional, default:0
)rightTrigger
: number - Intensity of right trigger actuator (0
-1
) (optional, default:0
)
rumble(duration?, intensity?)
- Applies rumble effect to all connected playersduration
: number - Duration in milliseconds (optional, default:30
)intensity
: HapticIntensity - Intensity of the rumble (optional, default:Balanced
)
connectedPlayers
An array containing only the connected players, giving you all the player properties but only for gamepads that actually exist.
Examples
// start the game when all players press start
if (spud.allPlayers.isButtonDown(Button.Start)) {
yourGame.startGame();
}
// charge a special move when all players press a button simultaneously
if (spud.allPlayers.buttonHeld(Button.South)) {
yourGame.chargeTeamSpecialMove();
}
// apply haptic feedback to all connected players
spud.allPlayers.rumble(500, HapticIntensity.Heavy);
// or a custom haptic pattern for all players
spud.allPlayers.haptic({
duration: 200,
strong: 0.8,
weak: 0.4,
leftTrigger: 0.5,
rightTrigger: 0.25,
});
// pause the game when any player presses the Select/Back button
if (spud.anyPlayer.buttonJustPressed(Button.Select)) {
spud.pauseGame();
}
// loop through just the connected controllers
spud.connectedPlayers.forEach((player) => {
console.log(
`${player.name}: ${player.gamepadInfo.vendor} ${player.gamepadInfo.model} controller ${player.gamepadInfo.id}`,
);
// ^ is not nullish
});
There's also the playerCount
property showing the number of currently connected controllers. It's effectively the same as spud.connectedPlayers.length
.
console.log(`${spud.playerCount} controller(s) connected`);
players
This is the same exact thing as connectedPlayers
but also includes disconnected players. It will always return an array of length 4. And unlike in connectedPlayers
, the gamepad
property on each player can be null
.
compact
Sometimes you don't care which specific gamepads you're dealing with, you just want the first one or two connected players to be able to play against each other. For example, suppose 3 players were originally connected and then P2 disconnects. The navigator.getGamepads()
array now looks like this: [Gamepad, null, Gamepad, null]
. If you have a 2-player game, it would be convenient to alias P3 into the P2 slot so that your game only ever needs to think about P1 and P2. That's what you get with the compact
property. Use spud.compact.p1
to get the first connected player. In most cases it'll be equal to spud.p1
but if the original P1 disconnects during a session, spud.compact.p1
will instantly and automatically get remapped to the next connected player. Another way to think of spud.compact.p1
is that it's like doing this: spud.connectedPlayers[Player.P1]
rather than spud.p1
. (Internally, that's effectively what this is doing.)
While spud.compact.p1
has some niche use cases, for most games we recommend avoiding it in favor of a dedicated gamepad setup screen.
spud platform integration
The API provides built-in integration with the spud.gg platform:
spud.isPaused
- Boolean indicating if the game is currently paused. In addition to being set viapauseGame()
this can be set from outside your game by the spud.gg platform.spud.isDemoMode
- Boolean indicating if the game is running in demo mode. (Demo mode is used to provide a game preview on spud.gg's game grid and in the game detail modal.)spud.pauseGame()
- Sends a pause message to the parent spud.gg window, if applicable, and setsisPaused
totrue
In addition, spud.p1.name
will be the player's customized 1-3 character name rather than just "P1".
Examples
// check if the game is paused (set by the platform)
if (spud.isPaused) {
yourGame.showPauseScreen();
return;
}
// pause the game and notify the platform
pauseButton.addEventListener('click', () => {
spud.pauseGame();
});
// check if running in demo mode
if (spud.isDemoMode) {
yourGame.showDemoGameplay();
}
// if for some reason you want to remove event listeners for pause/resume/playerInfo:
spud.disconnect();
Credits
- Controller layouts and IDs https://github.com/electrovir/gamepad-type
- Gamepad axis normalization formulas and default controller deadzones https://github.com/ensemblejs/gamepad-api-mappings
Roadmap
(in no particular order)
- events:
- controller connected/disconnected
- controller battery alerts on state change (WebHID)
- improved joycon support:
- treat L+R controllers as one gamepad, if possible
- prompt user and ask if they want to treat as one or two players
- alternatively, use OS-level buddy controller support
- treat L+R controllers as one gamepad, if possible
- webHID:
- get controller battery levels
- alerts like "P1 L controller battery below 10%" or "P2 battery: 1%"
- tbd: not making it a huge pain for the user to add devices
- support dualshock touchpads/accelerometer/gyroscope
- wiimotes?
- LEDs (joycon, dualshock, others?)
- there's also the GamepadLightIndicator proposal that doesn't require HID
- get controller battery levels
- test and verify gamepad.touchEvents on PS controllers
- service worker for offlining large assets (and the entire game)
- communicate local high scores / leaderboard / achievements with spud.gg parent window
- working with godot, unity, and other engines