⌨️🖱🎮 use-control is an elegant, typesafe input management system for React supporting keyboard, mouse and gamepad.
👁 Live Demo (source in packages/example
)
npm i use-control
yarn add use-control
First, we set up an input mapping
. Inputs come in two flavours:
buttons
: discrete inputs like keyboard presses, mouse clicks and gamepad buttonsaxes
: continuous inputs like mouse position, gamepad joysticks and triggers
const inputMap = {
buttons: {
left: [
keycode(KEYS.left_arrow),
mouseButton('left'),
gamepadButton(0, GAMEPADS.XBOX_ONE.D_LEFT),
],
right: [
keycode(KEYS.right_arrow),
mouseButton('right'),
gamepadButton(0, GAMEPADS.XBOX_ONE.D_RIGHT),
],
},
axes: {
x: [mouseAxis('x'), gamepadAxis(0, GAMEPADS.XBOX_ONE.STICK_R_X)],
y: [mouseAxis('y'), gamepadAxis(0, GAMEPADS.XBOX_ONE.STICK_R_Y)],
},
}
Then we can wire up our input listeners within a component using various hooks.
const MyComponent = () => {
const [count, setCount] = useState(0)
useButtonPressed(inputMap, "left", () => {
setCount(count - 1)
})
useButtonPressed(inputMap, "right", () => {
setCount(count + 1)
})
useAxis(inputMap, "x", v => {
console.log("x-axis", v)
})
return <div>{count}</div>
}
Check out the full example for more details.
Note: if you want to use gamepad as an input source you need to call gamepadInit()
in the entry point of your app to set up the listeners
If you want to use gamepad input you'll need to attach the listeners when your app starts up, this probably means you want to call gamepadInit
once in index.js
or App.js
but you can turn the feature on and off at your leisure.
gamepadInit()
gamepadTeardown()
useButtonPressed(inputMap, actionName, callback)
useButtonReleased(inputMap, actionName, callback)
useButtonHeld(inputMap, actionName, throttleInterval, callback)
useAxis(inputMap, axisName, callback)
These functions can be use to construct bindings for input maps:
mouseButton('left' | 'right' | 'middle')
mouseAxis('x' | 'y')
keycode(code)
gamepadButton(controllerIndex, buttonIndex)
gamepadAxis(controllerIndex, buttonIndex)
If you need to dig down and specifically target one form of input it might be more useful to pick from this list:
import { useKeyDown, useKeyUp, useKeyHeld } from 'use-control/lib/input/keyboard'
useKeyDown(keyCode, callback)
useKeyUp(keyCode, callback)
useKeyHeld(keyCode, callback)
import { useMouseMove, useMouseMoveNormalised, useMouseDelta } from 'use-control/lib/input/keyboard'
useMouseMove(callback)
useMouseMoveNormalised(callback)
useMouseDelta(callback)
import { useGamepadButtonPressed, useGamepadAxis } from 'use-control/lib/input/keyboard'
useGamepadButtonPressed(controllerIndex, buttonIndex, callback)
useGamepadAxis(controllerIndex, axisIndex, callback)
- Virtual joystick support
- Accelerometer input support
- Controller button mappings for
- PS4
- Xbox 360
- Xbox One
- (and any others contributed)
Personally, I'm just tired of writing useEffect
with document.addEventListener('keydown', ...)
.
use-control
is the API I've always dreamed of for dealing with input events, it's heavily inspired by my experience with input systems in game development. It's a tiny, batteries-included library for focusing on the actual user interactions rather than boilerplate and ochestration.
use-control
relies on the core concept of an Input Mapping
of keycodes, mouse buttons and gamepad buttons into Input Actions
(i.e. "left", "right", "jump", "select"), declared as a JS object:
const inputMap = {
buttons: {
left: [
keycode(KEYS.left_arrow),
gamepadButton(0, GAMEPADS.XBOX_ONE.D_LEFT),
],
right: [
keycode(KEYS.right_arrow),
gamepadButton(0, GAMEPADS.XBOX_ONE.D_RIGHT),
],
jump: [
keycode(KEYS.space)
]
},
axes: {
x: [mouseAxis('x'), gamepadAxis(0, GAMEPADS.XBOX_ONE.STICK_R_X)],
y: [mouseAxis('y'), gamepadAxis(0, GAMEPADS.XBOX_ONE.STICK_R_Y)],
},
}
You should consider declaring this statically and sharing the mapping across your app but it can be dynamically updated at runtime and different mappings can be used in different components as needed.
These mappings allow us to think at a higher level when consuming input, instead of asking "what events do I need to bind to?" or "what keycode am I listening for?" we can simply ask "what happens when the user presses the jump
button?"
useButtonPressed(inputMap, "jump", () => {
player.addForce(0, -10)
})
yarn
yarn bootstrap
cd packages/use-control
yarn build
cd ../example
yarn start