NgtsKeyboardControls
NgtsKeyboardControls is a renderless attribute directive (bare selector [keyboardControls]) ported from Dreiβs KeyboardControls. It attaches keydown/keyup listeners to window (or a custom domElement) and distributes the pressed state of named actions to descendants via DI. It owns no gameplay semantics β consumers decide what actions do.
Types
NgtsKeyboardControlsEntry<TName extends string>
interface NgtsKeyboardControlsEntry<TName extends string = string> { name: TName; keys: string[]; up?: boolean; toggle?: boolean;}Multiple keys map to one action, matched against event.key or event.code. Prefer event.code values like 'KeyW' or 'Space' β event.key is layout/case-sensitive.
An entry supports three modes:
- default β the action is pressed while held: state is
truewhile any mapped key is down. up: falseβ fire-on-press (discrete action): state latchestrueafter the first press; onlykeyChangefires on subsequent press edges. Consume viakeyChangeedges. Ignored whentoggleistrue.toggle: true(defaultfalse) β stateful on/off: each press edge flips the state betweentrueandfalse; keyup is ignored. Takes precedence overup. Auto-repeat does not re-flip (held keys are reference-counted). Consume via the reactiveselect()signal β e.g.torchOn = controls.select('torch')flips on each press of the key.
NgtsKeyboardControlsState<TName>
type NgtsKeyboardControlsState<TName extends string = string> = { [K in TName]: boolean;};NgtsKeyboardControlsChangeEvent<TName>
interface NgtsKeyboardControlsChangeEvent<TName extends string = string> { name: TName; pressed: boolean; state: NgtsKeyboardControlsState<TName>;}Usage
Define the keyboard map at module level with createKeyboardControls β an identity factory that infers action names as literal types and returns { controlsMap, injectKeyboardControls } pre-typed so consumers never repeat the generic.
import { createKeyboardControls, NgtsKeyboardControls } from 'angular-three-soba/controls';
export const { controlsMap, injectKeyboardControls } = createKeyboardControls([ { name: 'forward', keys: ['ArrowUp', 'KeyW'] }, { name: 'back', keys: ['ArrowDown', 'KeyS'] }, { name: 'left', keys: ['ArrowLeft', 'KeyA'] }, { name: 'right', keys: ['ArrowRight', 'KeyD'] }, { name: 'jump', keys: ['Space'], up: false },]);Provide the directive via template:
<ngt-group [keyboardControls]="controlsMap"> <!-- descendants can inject the controls --></ngt-group>or via hostDirectives:
@Component({ hostDirectives: [NgtsKeyboardControls],})export class Scene { constructor() { inject(NgtsKeyboardControls).map.set(controlsMap); }}Consume in any descendant. There are three consumption forms: reactive per-action signals, non-reactive per-frame polling, and edge events.
export class Player { private keyboardControls = injectKeyboardControls(); // reactive: memoized Signal<boolean> per action protected jumping = this.keyboardControls.select('jump'); constructor() { // transient: poll non-reactive snapshot in the frame loop beforeRender(({ delta }) => { const { forward, back, left, right } = this.keyboardControls.snapshot; }); // edge events for discrete actions const sub = this.keyboardControls.keyChange.subscribe(({ name, pressed }) => { if (name === 'jump' && pressed) { /* jump */ } }); inject(DestroyRef).onDestroy(() => sub.unsubscribe()); }}Properties and Methods
| Property/Method | Description |
|---|---|
state | Readonly signal containing the full pressed state of all actions. |
select(name) | Memoized per-action Signal<boolean>. |
snapshot | Getter returning the current state for non-reactive per-frame polling. |
Standalone Functions
injectKeyboardControls<TName>()
Injects the nearest NgtsKeyboardControls, typed to the action names. Throws a friendly error when used outside of a NgtsKeyboardControls context.
createKeyboardControls([...entries])
Module-level identity factory. Infers action names as literal types and returns { controlsMap, injectKeyboardControls } pre-typed so consumers never repeat the generic.
Differences from Drei
keyChangeemits on action transitions only β no auto-repeat spam from held keys.- Held keys are reference-counted per action: releasing
KeyWwhileArrowUpis held keeps'forward'pressed. toggle: truemode β Drei has no toggle mode; itsup: falselatch forcesonChangeconsumption, whereastoggle+select()gives a live boolean signal.preventDefaultoption to stop default browser behavior (e.g. Space scrolling the page) for mapped keys.- The pressed state is not wiped when the
mapchanges.
Inputs
| name | type | description | required |
|---|---|---|---|
| map | NgtsKeyboardControlsEntry[] | The keyboard map defining named actions and their keys. A model() input aliased to the selector: [keyboardControls]="controlsMap". hostDirectives consumers set it programmatically via inject(NgtsKeyboardControls).map.set(controlsMap). | yes |
| domElement | HTMLElement | Window | The event source to attach keydown/keyup listeners to. Default: window | no |
| preventDefault | boolean | Whether to register non-passive listeners and call preventDefault() for mapped keys (e.g. stop Space scrolling the page). Default: false | no |
Outputs
| name | type | description |
|---|---|---|
| keyChange | NgtsKeyboardControlsChangeEvent | Emits { name, pressed, state } on action transitions only. Auto-repeat is deduplicated and held keys are reference-counted per action. |