Actions and Action Bindings

Understand actions, the input pipeline, and how to bind inputs to gameplay.

Actions allow to decouple specific user inputs from game/business logic. Inputs (keyboard, mouse/touch, controllers, on‑screen UI) are converted by action bindings into actions that game systems consume on every frame or whenever an event happens to act upon the action.

  • Input → Action Binding → Action → Effect
    • Input: hardware or UI event (key, mouse move, touch, thumbstick, button)
    • Action Binding: translates that input into a domain signal
    • Action: a shared signal (event or state) consumed by systems
    • Effect: the game changes on frame (e.g., camera rotates, character moves, jumps)

State vs. Event actions

  • StateAction<T>

    • Represents a continuous state that persists until changed (e.g., movement axes, “is running”).
    • Merges multiple writers (e.g., keyboard + joystick) into one value each frame.
    • Read anywhere via .get().
  • EventAction<T>

    • Represents instantaneous events (e.g., “jump pressed”, “rotate delta”, “zoom delta”).
    • Produces values per frame via a reader; values are combined (sum, etc.) before consumption.

For example, the CharacterCameraBehavior consumes rotation and zoom event actions, which are StateActions, every frame. The StateAction returns the final value as an accumulation of all the inputs since the last frame.

Built-in actions:

  • Movement: MoveForwardAction, MoveBackwardAction, MoveLeftAction, MoveRightAction, RunAction (State)
  • Jumping: JumpAction (Event)
  • Camera: RotateYawAction, RotatePitchAction, ZoomAction (Event)

Built-in action bindings

  • Keyboard locomotion: useKeyboardLocomotionActionBindings(...)
  • Mouse/touch camera (pointer capture): usePointerCaptureRotateZoomActionBindings(...)
  • Mouse camera (pointer lock): usePointerLockRotateZoomActionBindings(...)
  • Single key/button bindings: useKeyboardActionBinding(...), usePointerButtonActionBinding(...)
  • Mobile UI: useScreenJoystickLocomotionActionBindings(...), useScreenButton(...)

These hooks connect hardware inputs to actions. Multiple bindings can feed the same action; values are safely merged.

Example: Rotate the camera with the mouse (Pointer Lock)

This shows the full pipeline: mouse movement → pointer-lock binding → rotation actions → camera rotates on frame.

import { Canvas } from '@react-three/fiber'
import { Sky } from '@react-three/drei'
import {
Viverse,
SimpleCharacter,
BvhPhysicsBody,
PrototypeBox,
usePointerLockRotateZoomActionBindings,
} from '@react-three/viverse'

function Bindings() {
// Binds mouse movement (while pointer is locked) to RotateYawAction/RotatePitchAction,
// and mouse wheel to ZoomAction. The camera behavior consumes these every frame.
usePointerLockRotateZoomActionBindings({ lockOnClick: true })
return null
}

export default function App() {
return (
  <Canvas
    style={{ position: 'absolute', inset: '0', touchAction: 'none' }}
    camera={{ fov: 90, position: [0, 2, 2] }}
    shadows
  >
    <Viverse>
      <Sky />
      <directionalLight intensity={1.2} position={[5, 10, 10]} castShadow />
      <ambientLight intensity={1} />

      {/* SimpleCharacter includes a default camera behavior that reads rotation + zoom actions */}
      <SimpleCharacter actionBindings={[]} />
      <Bindings />

      <BvhPhysicsBody>
        <PrototypeBox color="#ffffff" scale={[10, 0.5, 10]} position={[0, -2, 0]} />
        <PrototypeBox color="#cccccc" scale={[2, 1, 3]} position={[4, 0, 0]} />
        <PrototypeBox color="#ffccff" scale={[3, 1, 3]} position={[3, 1.5, -1]} />
      </BvhPhysicsBody>
    </Viverse>
  </Canvas>
)
}

Click the canvas once to lock the pointer. Move the mouse to rotate the camera; use the mouse wheel to zoom. That’s the actions pipeline in action.

Example: Map Q/E keys to camera yaw rotation

You can also route keyboard inputs into camera rotation by mapping KeyboardEvent to yaw deltas and binding them to the same RotateYawAction.

import { useKeyboardActionBinding } from '@react-three/viverse'
import { RotateYawAction } from '@react-three/viverse'

function KeyboardYawBindings() {
  // Map KeyboardEvent → number (negative = left, positive = right)
  const rotateFromKeyboard = RotateYawAction.mapFrom((e: KeyboardEvent) => {
    if (e.code === 'KeyQ') return -0.02
    if (e.code === 'KeyE') return  0.02
    return 0
  })
  // Bind Q/E key presses to the mapped action
  useKeyboardActionBinding(rotateFromKeyboard, { keys: ['KeyQ', 'KeyE'] })
  return null
}

Place <KeyboardYawBindings /> alongside your other bindings. Now both mouse and keys contribute to RotateYawAction; their values are combined before the camera consumes them each frame.

Example: Keyboard locomotion and jump

To bind classic WASD + Shift + Space to movement and jump, use the built-in locomotion bindings:

import { useKeyboardLocomotionActionBindings } from '@react-three/viverse'

function LocomotionBindings() {
  useKeyboardLocomotionActionBindings({
    requiresPointerLock: false, // set to true if you want movement only when pointer is locked
  })
  return null
}

These bindings write to:

  • MoveForwardAction, MoveBackwardAction, MoveLeftAction, MoveRightAction (State: 0..1)
  • RunAction (State: boolean)
  • JumpAction (Event)

Your character controller (e.g., SimpleCharacter) reads these to move and animate on frame, regardless of the input device that produced them.

Takeaways

  • Decouple input from gameplay: actions provide a stable interface your systems consume.
  • Compose inputs: multiple bindings can feed the same action; values merge predictably.
  • Think “signals,” not devices: code against actions like “move forward” or “yaw delta,” not specific keys or hardware.