Type-safe, composables-first input plugin for the GWEN game engine.
Handles keyboard, mouse, gamepad, touch, and gyroscope input through a unified action system with stackable input contexts, runtime rebinding, processor pipelines, and multi-player support.
pnpm add @gwenjs/inputnpm install @gwenjs/input// gwen.config.ts
import { defineConfig } from '@gwenjs/app'
export default defineConfig({
modules: [['@gwenjs/input', { players: 1 }]],
})import { createEngine } from '@gwenjs/core'
import { InputPlugin } from '@gwenjs/input'
import '@gwenjs/input/augment'
const engine = createEngine({ ... })
await engine.use(InputPlugin({ players: 1 }))Co-locates actions and their default bindings in a single declaration. Returns fully-typed ActionRef<T> values.
import { defineInputSchema, Keys, GamepadButtons, GamepadStick,
Composite2D, DeadZone, Smooth, Scale, Hold } from '@gwenjs/input'
export const { actions, context: GameplayContext } = defineInputSchema('gameplay', {
priority: 0,
actions: {
Jump: { type: 'button', bindings: [Keys.Space, GamepadButtons.South] },
Move: { type: 'axis2d', bindings: [
Composite2D({ up: Keys.W, down: Keys.S, left: Keys.A, right: Keys.D }),
{ source: GamepadStick.Left, processors: [DeadZone(0.15), Smooth(0.08)] },
]},
Sprint: { type: 'button', bindings: [
{ source: Keys.ShiftLeft, interactions: [Hold({ holdTime: 0.1 })] },
]},
},
})import { defineAction, defineInputContext, bind, Keys, GamepadButtons } from '@gwenjs/input'
const Jump = defineAction('Jump', { type: 'button' })
const Move = defineAction('Move', { type: 'axis2d' })
export const GameplayContext = defineInputContext('gameplay', {
priority: 0,
bindings: [
bind(Jump, Keys.Space),
bind(Jump, GamepadButtons.South),
],
})import { usePlayer, useAction } from '@gwenjs/input'
import { actions } from './input'
// Via composable — player 0 by default
const jump = useAction(actions.Jump) // ButtonActionState
const move = useAction(actions.Move) // Axis2DActionState
onUpdate((dt) => {
if (jump.isJustTriggered) player.jump()
entity.velocity.x = move.value.x * speed
entity.velocity.y = move.value.y * speed
})
// Per-player access
const p2 = usePlayer(1)
const p2Jump = p2.action(actions.Jump)import { forPlayers, usePlayer } from '@gwenjs/input'
// Create one system instance per player
defineConfig({
systems: [...forPlayers(2, movementSystem)],
})
function movementSystem(playerIndex: number) {
return defineSystem(() => {
const player = usePlayer(playerIndex)
const move = player.action(actions.Move)
// ...
})
}Applied in order to raw input values:
| Processor | Description |
|---|---|
DeadZone(threshold) |
Set to 0 if |value| < threshold |
Scale(factor) |
Multiply by factor |
Invert() |
Multiply by −1 |
InvertX() / InvertY() |
Invert single axis (axis2d only) |
Clamp(min, max) |
Clamp to range |
Normalize() |
Normalise axis2d to unit vector |
Smooth(factor) |
Lerp toward target per frame |
SwizzleXY() |
Swap x and y |
Applied to button bindings to control when isJustTriggered fires:
| Interaction | Description |
|---|---|
Press() |
Default — fires on first down frame |
Release() |
Fires on release frame |
Tap({ maxDuration? }) |
Press + release within maxDuration |
Hold({ holdTime }) |
Held for holdTime seconds |
DoubleTap({ maxGap? }) |
Two taps within maxGap |
AllOf(...keys) |
All specified keys held simultaneously |
ChordedWith(actionRef, condition) |
Only fires if action is in given state |
Toggle() |
Press once = on, press again = off |
Repeat({ initialDelay?, rate }) |
Auto-repeat while held |
const p1 = usePlayer(0)
// Interactive — resolves with the next input received
const source = await p1.captureNextInput({ timeout: 5000 })
if (source !== null) p1.rebind(actions.Jump, 0, source)
// Programmatic
p1.rebind(actions.Jump, 0, 'Enter')
p1.resetBinding(actions.Jump, 0)
p1.resetBindings()
// Serialise / restore
const snapshot = p1.exportBindings()
localStorage.setItem('bindings', JSON.stringify(snapshot))
p1.importBindings(JSON.parse(localStorage.getItem('bindings')!))import { builtInProfiles } from '@gwenjs/input'
// Register profiles
InputPlugin({
accessibilityProfiles: {
'one-handed-left': builtInProfiles.oneHandedLeft,
'custom': myCustomProfile,
}
})
// Activate from settings UI
const input = useInput()
input.player(0).activateAccessibilityProfile('one-handed-left')
// List available profiles
input.getAccessibilityProfiles() // ['one-handed-left', 'custom']
// Get remappable actions for UI
input.player(0).getRemappableActions()
// [{ name: 'Jump', type: 'button', bindings: [{ index: 0, displayName: 'Space', isOverridden: false }] }]Escape hatches for direct device reading without the action system:
import { useKeyboard, useMouse, useGamepad, useTouch, useGyroscope, usePointer } from '@gwenjs/input'
const kb = useKeyboard()
const mouse = useMouse()
const pad = useGamepad(0) // slot 0
const touch = useTouch()
const gyro = useGyroscope()
const ptr = usePointer() // unified mouse + touch
// Pointer state
ptr.position // { x, y } canvas-relative
ptr.isPressed
ptr.type // 'mouse' | 'touch'
ptr.delta // { x, y } movement this frame
// Gyroscope
gyro.orientation // { roll, pitch, yaw } in degrees
gyro.rotationRate // { alpha, beta, gamma } in deg/s
gyro.acceleration // { x, y, z } in m/s²
gyro.isAvailable
gyro.isPermitted
gyro.calibrate() // set current orientation as zero
gyro.resetCalibration()button.onclick = async () => {
const result = await useInput().requestMotionPermission()
// 'granted' | 'denied' | 'unavailable'
}const recorder = useInputRecorder()
const playback = useInputPlayback()
recorder.start()
// ... run scenario ...
recorder.stop()
const rec = recorder.export()
// Playback
playback.load(rec)
playback.play()
playback.loop = true // loop for demo screens
playback.speed = 0.5 // half speed
playback.onComplete(() => backToMenu())| Path | Contents |
|---|---|
@gwenjs/input |
Full public API |
@gwenjs/input/module |
Build-time module definition |
@gwenjs/input/processors |
Processor functions only |
@gwenjs/input/interactions |
Interaction functions only |
@gwenjs/input/constants |
Keys, GamepadButtons, GamepadStick, etc. |
@gwenjs/input/devices |
Device classes (KeyboardDevice, etc.) |
@gwenjs/input/augment |
Side-effect: extends GwenProvides types |
InputPlugin({
players: 2,
contexts: [GameplayContext, MenuContext],
defaultActiveContexts: ['gameplay'],
canvas: document.getElementById('game') as HTMLCanvasElement,
touch: {
enabled: true,
virtualJoysticks: [{ id: 'move-stick', side: 'left', size: 120 }],
virtualButtons: [{ id: 'jump-btn', label: '↑', position: { x: 80, y: 85 } }],
},
gyro: { smoothing: 0.1, deadZone: 0.02 },
devOverlay: { position: 'bottom-right', opacity: 0.85 },
onBindingsChanged(playerIndex, snapshot) {
localStorage.setItem(`bindings-p${playerIndex}`, JSON.stringify(snapshot))
},
initialBindings: [
JSON.parse(localStorage.getItem('bindings-p0') ?? 'null'),
],
})