Files
ue-toolchain/plugins/ue-design-skills/skills/ue-input-architecture/SKILL.md
T
ue-toolchain dab3f35079 feat(skills): ship ue-design-skills bundle, licensing and delivery gate
Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:48:55 +07:00

388 lines
14 KiB
Markdown

---
name: ue-input-architecture
description: >-
Design or review Enhanced Input architecture in Unreal Engine: physical
mapping contexts, input actions, semantic gameplay tags, input config data,
native versus ability input paths, per-frame buffering on the ability system
component, player-mappable settings, device switching, and runtime injection
and removal of input by feature plugins. Use when adding controls, rebinding,
gamepad or touch support, ability input, mode-specific input, or debugging
actions that bind correctly and never fire.
---
# UE input architecture
The invariant:
> Physical controls, semantic commands and gameplay implementations are three
> separate layers joined by validated data.
```text
key / gamepad / touch
→ input mapping context
→ input action
→ input config: input action → input tag
→ native handler OR ability system component
→ ability set: input tag → ability
```
Measured patterns from a reference product: [patterns](references/patterns.md).
Detection recipes for silent input failures: [failure modes](references/failure-modes.md).
**The characteristic failure of this architecture is a button that does nothing
and logs nothing.** Read the failure modes before adopting it — most of them are
variations on that one observable.
Related skills: `ue-gameplay-tag-governance`, `ue-gas-architecture`,
`ue-modular-gameplay`, `ue-ui-architecture`.
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
---
## 1. Keep the three layers independent
### Physical — the mapping context
Owns key, button and axis to input action; modifiers and triggers; priority
relative to other contexts; player-mappable metadata.
Changing a keyboard layout or a gamepad binding should stop here.
### Semantic — the input config
An immutable asset owning pairs:
```text
IA_Move → InputTag.Move
IA_WeaponFire → InputTag.Weapon.Fire
IA_Dash → InputTag.Ability.Dash
```
Gameplay depends on tags, not on asset paths or physical keys.
### Capability — handlers and ability sets
Native tags bind to explicit handlers. Ability tags travel to the ability system
component as payload. Granted ability specs carry the matching semantic tag.
Changing an ability implementation should not require touching a mapping context.
---
## 2. Split native and ability actions
Make the distinction explicit in the config, with two separate lists.
**Native** — deterministic controller and pawn functions: move, look, crouch,
autorun. Each costs one explicit binding line, and the tag is a **lookup key at
bind time only**. The handler receives an input value; the tag does not exist at
runtime.
**Ability** — bound through one pressed/released pair with the row's tag passed
as a payload argument. One pair of functions serves any number of abilities, and
the count of new abilities you can add without writing code is unbounded.
### Classification test
Native when the action controls continuous motion, must execute immediately and
deterministically, and needs no ability lifecycle.
Ability when the action activates a grantable capability, can be blocked or
cancelled by tags, has cost or cooldown or prediction, or changes with equipment
or mode.
### Watch what the ability path fixes at bind time
If ability binding is hardcoded to two trigger events, then hold, tap and
double-tap variants must be expressed through triggers and modifiers in the
mapping context rather than through trigger-event selection. That is a
reasonable constraint — but it is a constraint, and it is invisible until
someone needs a third event.
---
## 3. Join semantic tags at grant time
When an ability set grants an ability, add the configured input tag to the
spec's dynamic source tags. Lookup then becomes:
```text
pressed input tag
→ scan activatable specs
→ exact tag match
→ buffer the spec handle
```
Do not store an input action pointer on the ability. That couples a capability to
one content asset and blocks per-mode and per-device remapping.
### The match is exact, not hierarchical
A spec tagged `InputTag.Weapon` is **not** matched by a press of
`InputTag.Weapon.Fire`. The tag hierarchy organises the vocabulary and filters
the editor picker; it does not participate in lookup. Assuming otherwise
produces a binding that is correct in every visible respect and never fires.
Recipe: IN-01.
### Validate the join
The input config and the ability set are independent assets. Their tag equality
is a foreign key with no database behind it, so validation is the only compiler
it will ever have:
- every input-driven ability has a non-empty tag;
- every intended ability tag is reachable from at least one active config;
- every config ability tag has a granted consumer in the intended composition;
- duplicates are intentional;
- category constraints hold on both sides.
---
## 4. Buffer ability input, process once per frame
Forwarding a tag must not activate immediately. Record held handles, pressed this
frame, released this frame; process from the controller's post-input step in
three passes — held with the while-active policy, then pressed with the
on-triggered policy, then activate everything in one batch, then releases.
This removes event-order dependence and makes simultaneous input resolvable.
See `ue-gas-architecture` for the activation policies and the global block tag.
**Accept the consequence:** the ability path is one input-processing step behind
the native path within the same frame. For most games that is invisible. It is
still a real difference between the two paths, and it belongs in the
classification decision rather than being discovered later.
---
## 5. Initialize input at a named readiness boundary
Binding depends on several independently arriving pieces: a local controller, the
input subsystem, the pawn data and its config, the project's input component
subclass, and any contexts added by features.
Do not treat `BeginPlay` as ready. Use init states and emit a semantic event —
"bind inputs now" — once the dependencies hold.
Feature actions must respond to **both** the generic extension-added event and
the semantic ready event, because either can arrive first. Set the readiness flag
*before* broadcasting, or a handler that checks it during the broadcast will see
false.
### Base initialization checklist
- [ ] the component is the expected input subclass, checked with a diagnostic
that names the fix;
- [ ] clearing existing mappings is deliberate and its consequences are handled;
- [ ] default contexts added to the local player subsystem;
- [ ] settings registration separated from runtime activation (§6);
- [ ] native actions bound explicitly;
- [ ] ability actions bound **and their handles retained** (§7);
- [ ] the ready event is sent exactly once per initialization;
- [ ] late feature handlers can bind safely.
### Clearing all mappings is a broadcast dependency
If initialization clears every mapping on the subsystem, it also removes contexts
added by already-active features. Recovery then depends on those features hearing
the ready event and re-adding their contexts — which makes correctness a property
of event ordering rather than of stored state. It works; it is fragile; and it
must be a documented decision rather than a side effect. Recipe: IN-08.
---
## 6. Separate "register with settings" from "activate now"
These are two different operations:
```text
RegisterInputMappingContext(IMC) // discoverable and rebindable
AddMappingContext(IMC, priority) // actually active
```
A context may be active but not remappable, or registered but not currently
active. All four combinations are meaningful.
**One flag must not gate both.** If it does, unchecking "expose this for
rebinding" also silently removes the input — a designer changes a settings
checkbox and the character stops responding, with nothing in the log. Recipe:
IN-06.
---
## 7. Modular input needs ownership receipts
### Adding a mapping context
Data: context, priority, register-with-settings flag. Retain per activation
context: the extension and delegate handles, the exact players touched, and the
exact contexts and priorities activated.
### Adding ability bindings
Data: a list of input configs. Retain per pawn: **every returned bind handle**,
which config produced it, and the readiness subscription.
### Symmetric removal
```text
remove the exact bind handles this config created
remove the exact contexts this action added
unregister from settings only if this action registered them
release extension and delegate handles
```
**Never implement removal by clearing all bindings or all mappings on the pawn.**
That destroys other features' ownership, and it will look like it works.
The measured reference composes this well and tears it down badly: bind handles
are declared as locals and discarded, so the removal function *cannot* be
implemented without changing the component that binds. Copy the composition;
write the teardown yourself. Recipes: IN-03, IN-04.
---
## 8. Device switching belongs above gameplay semantics
Gameplay should not branch on keyboard versus gamepad for the same command. The
UI layer owns the active input type, glyphs, controller brand and style,
change notifications, and disconnected-device handling.
Gameplay may still use **distinct actions and tags** where the semantics genuinely
differ — mouse delta and analog stick look need different modifiers and
sensitivity, so:
```text
InputTag.Look.Mouse
InputTag.Look.Stick
```
That is semantic distinction, not device sniffing. The test is whether the two
paths do different arithmetic; if they do, they are different commands.
Per-player sensitivity, dead zones and inversion belong in input modifiers that
read settings — not in handler code. Note that a modifier which resolves a setting
by property **name** through reflection will break silently on a rename, with no
compiler help. Recipe: IN-10.
---
## 9. Context priority and consumption
A handler can be perfectly bound and never fire because an earlier context
consumes the key.
Before adding a mapping: list every active context in priority order; find all
mappings for the physical key; inspect triggers, modifiers and consume-input
behaviour; establish whether the contexts can be active simultaneously; and
confirm no raw key path bypasses the system.
Priority is meaningful only between concurrently active contexts. Document why a
feature needs to outrank the baseline.
### Avoid raw key fallback
Raw key handling bypasses rebinding, device parity, triggers and modifiers,
context priority, and input profiles. Use it only for editor and debug controls,
with a stated reason.
---
## 10. The config-file boundary
With Enhanced Input, the project input config file should contain the
player-input and input-component classes, user-settings class registration, debug
bindings, UI input defaults, and legacy axis properties only where the engine
still requires them.
Gameplay mappings live in mapping context assets; semantic mappings live in input
config assets. **Legacy axis configuration that sits beside Enhanced Input is
actively misleading** — it looks like the place sensitivity is set, and it is not.
Recipe: IN-09.
---
## 11. Test matrix
### Composition
Baseline pawn data and config; a mode action set adding a second config;
equipment granting and removing matching abilities; a feature activating both
before and after pawn readiness; two features adding distinct configs at once.
### Lifecycle
```text
spawn → bind → activate feature → press / hold / release
→ deactivate feature → verify no response
→ reactivate → verify exactly one response
```
### Devices
Keyboard and mouse; gamepad; touch; hot-swap while UI is open; a saved and
reloaded remap; two local players with separate profiles.
### Network and abilities
Owning-client predicted activation; server rejection with failure feedback; input
held across a possession or avatar change; the block tag clearing buffered state;
an ability removed while its input is held.
---
## 12. Review checklist
- [ ] Physical key knowledge stops at the mapping context.
- [ ] Gameplay uses semantic tags, not asset references.
- [ ] Native versus ability classification is intentional.
- [ ] Ability input is buffered, not activated inside the event callback.
- [ ] Config and ability-set tags are validated as a join, in both directions.
- [ ] Tag matching semantics are known to be exact, and the data suits that.
- [ ] Binding waits for a named readiness state, not for begin play.
- [ ] Base and feature bindings both retain handles.
- [ ] Runtime activation and settings registration are independent.
- [ ] Deactivation removes only what this owner added.
- [ ] Priority and consumption conflicts have been audited for shared keys.
- [ ] Device icons and method switching stay in the UI layer.
- [ ] Activate, deactivate, reactivate produces no duplicate callbacks.
---
## 13. When this architecture is too much
For a single-mode project with one device family, no dynamic abilities and no
rebinding, direct bindings are honest and this indirection is overhead.
Introduce semantic tags when at least one becomes true: abilities are granted
dynamically; several pawn archetypes reuse controls; features add input at
runtime; rebinding and device parity matter; gameplay must be independent of
content asset paths.
The architecture is paid for by **modularity of content** and **many archetypes
with different action sets**. Without either, the cost is real and the benefit is
not: to answer "what does the left mouse button do?" a reader must traverse
mapping context, action, config, tag, pawn data, ability set and spec tags —
seven assets instead of one call stack.
---
## Provenance
The findings behind the failure modes come from a line-by-line source audit of
the input layer of Epic's Lyra Starter Game on Unreal Engine 5.6 — about 870
lines across seven file pairs, plus its integration points in the hero component
and two feature actions — read as source rather than run.
Source addresses stay in the research archive that produced this skill; what
ships is the detection recipe. Each entry carries a stable identifier (`IN-06`
and up) that resolves back to the audited location.
## Evidence boundary
One project, one engine version, one workspace. Mapping contexts and input
actions are binary assets and were not read, so every claim about which keys map
to which actions is out of scope rather than concluded. Re-run the recipes
against your own tree before acting on any specific claim.