Skip to content

(⌨️ /🖱 / 🎮 ) keyboard, mouse and gamepad input via react hooks

License

Notifications You must be signed in to change notification settings

bfollington/use-control

Repository files navigation


Version Twitter ETH Language License Bundle Size Build

⌨️🖱🎮 use-control is an elegant, typesafe input management system for React supporting keyboard, mouse and gamepad.

👁  Live Demo (source in packages/example)

Installation

npm i use-control
yarn add use-control

Example

First, we set up an input mapping. Inputs come in two flavours:

  • buttons: discrete inputs like keyboard presses, mouse clicks and gamepad buttons
  • axes: 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

API Overview

Bootstrap

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()

Hooks

  • useButtonPressed(inputMap, actionName, callback)
  • useButtonReleased(inputMap, actionName, callback)
  • useButtonHeld(inputMap, actionName, throttleInterval, callback)
  • useAxis(inputMap, axisName, callback)

Input Sources

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)

Primitive Hooks

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)

Roadmap

  • Virtual joystick support
  • Accelerometer input support
  • Controller button mappings for
    • PS4
    • Xbox 360
    • Xbox One
    • (and any others contributed)

Why use-control?

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.

Usage

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)
})

Running this repo

Bootstrap

yarn
yarn bootstrap

Running the examples

cd packages/use-control
yarn build
cd ../example
yarn start