How-To: Wire Combo Input
Goal: route player input into the combo controller and declare, in Project Settings, exactly which actions count as combo inputs, which count as chord modifiers, and where directional intent comes from. Nothing about combo input is implicit - every action and modifier the system reacts to is named in an asset you can open.
1. Declare the input catalog
Three settings under Project Settings -> Crimson -> Crimson Ability System declare the combo system's entire input surface.
| Setting | What it does |
|---|---|
| Combo Input Mapping Context | The UInputMappingContext whose mappings define every UInputAction a combo may use. The graph editor's input picker reads its catalog from here, and at runtime the controller ignores a press for any action that is not in it - a sprint or interact press routed to the controller is dropped instead of being buffered and re-tested against every transition until it ages out. Leave it unset and the gate is off: every routed action is accepted. |
| Modifier Input Mapping Context | The UInputMappingContext whose mappings define which UInputActions act as chord modifiers (Shift, Ctrl, a trigger axis). An action declared here is a modifier - it is never treated as a combo input in its own right, so pressing it cannot buffer an entry that no transition can match. Unset is valid: chords then rely purely on the modifier registry. |
| Move Input Action | Your IA_Move. The controller samples its raw axis at press time to fill in ECrimsonComboDirection. Leave it unset and every input resolves to None, so only direction-agnostic transitions can fire. |
UCrimsonInputTrigger_Combo, shipped here) or the stock Down trigger.Do NOT use Pressed. It emits
Triggered on the actuation edge then falls to None the next frame while the button is still down, so Enhanced Input fires Completed, the controller sees a phantom release, and Hold transitions can never mature.2. Route presses through the controller
The controller lives on the pawn and must see input before the ASC does, so it can consume a press that drives a transition. The contract is sequential: call Handle Input Action Pressed, and only fall through to the ASC's tag flow when the controller did not consume the press. Mirror the same chain for releases - Hold transitions and tap/hold disambiguation depend on the release arriving.
Add the component: pawn Blueprint -> Add Component -> Crimson Combo Controller.
Then for each combo-capable action: Enhanced Input Action event (Started) -> Handle Input Action Pressed (on the controller, passing the UInputAction) -> read the bOut Handled pin -> Branch. If handled, stop. If not, call Ability Input Tag Pressed on the ASC as usual. Mirror the same chain from the Completed event into Handle Input Action Released.
For dodge, block and other defensive actions, tick b Is Defensive on the same node and always forward to the ASC - see How-To: Tune Combo Input Feel step 4 for why.
Log LogCrimsonAbilitySystem Verbose in the console, then press a combo action in PIE: the log shows [Chord] press <Action> accepted. Press an action that is NOT in the combo IMC (sprint, interact): the log shows press <Action> ignored: not in the combo catalog and the press still reaches the ASC.3. Give it directional intent
Directional handshakes (forward+heavy, back+dodge) need the controller to know where the stick is pointing at press time. It reads that through a provider the pawn installs.
Set Move Input Provider is C++ only - it takes a TFunction, which has no Blueprint representation. A Blueprint-only project can still use combos: leave it unset and author direction-agnostic transitions (Direction = None). For directional handshakes, set the pawn's class to a small C++ base that installs the provider (the C++ tab shows exactly what it does).
Direction and hold the stick that way while pressing. The combo starts; with the stick centered it does not. Magnitudes below the Move Input Dead-Zone setting (default 0.3) read as None.4. Require modifiers on a transition
A transition (or the starting handshake) can require modifiers - Shift+RMB, RT+Y. Select the Transition or Entry node in the graph and find Modifier Alternatives (FCrimsonComboInputBinding::ModifierAlternatives). Each alternative (FCrimsonComboModifierRequirement) is a set of modifiers that must ALL be held at press time; the press counts when any one alternative is satisfied; none means an unmodified input. Each alternative holds gameplay tags (RequiredModifierTags, picked with a tag picker) and/or raw keys (RequiredModifierKeys). Those are the two channels; use either or both.
| Channel | Holds | Use when |
|---|---|---|
Tags (RequiredModifierTags) | Modifier gameplay tags (for example Input.Modifier.Shift). Resolved to held/not-held by FCrimsonAbilitySystemIntegrationHooks::ResolveHeldComboModifierTags or your SetModifierStateProvider. | Your project binds that hook (CrimsonCore binds it to CrimsonInput's modifier registry). Tags survive the modifier itself being rebound. |
Raw Keys (RequiredModifierKeys) | Raw FKeys that must be held (Shift/Ctrl/Alt, pad triggers and shoulders), read from engine key state (axis keys count as held at >= 0.5). | Any Enhanced Input project. Works standalone, but does not follow rebinds. |
Multiple chords exist because keyboard and gamepad modifier layouts are not 1:1. A keyboard can hang everything off Shift, while a pad has to spread the same actions across LT and RT - so one transition may need Shift+RMB on keyboard but LT+FaceButtonLeft on pad, and its sibling Shift+E / RT+FaceButtonLeft. Author one chord per layout - {Shift} and {LeftTrigger} - and no device gate is needed: the chord whose modifier the current device cannot hold simply never matches.
The held modifiers are sampled at press time, like direction, and each chord is matched exactly: the sampled set must equal that chord's required set. Two consequences worth knowing. First, the controller only samples modifiers the active graph names somewhere - a modifier no transition mentions is invisible and can never block anything. Second, within that vocabulary exact-match cuts both ways: while a graph-named modifier is held, a chordless transition on the same action stands down, which is what lets RMB and Shift+RMB live on one button without an ordering race. A modifier that also drives the pressed action's own key (hold Shift to sprint) never counts toward its own press. Asset validation warns when one input repeats a chord, and errors when two transitions on the same step could match the same press - including through different chord lists that share a chord.
SetModifierStateProvider (C++ only, takes a TFunction); it overrides both channels. Left unset, raw keys are read from engine key state and tags go through FCrimsonAbilitySystemIntegrationHooks::ResolveHeldComboModifierTags (unbound = no tag counts as held).Shift+<action> on one transition and plain <action> on another from the same step. With verbose logging on, the chorded press logs [Chord] sample: ... sampled=Input.Modifier.Shift then [Chord] MATCH <Action> (Tap) chord=Input.Modifier.Shift; the plain press logs sampled=none and matches the chordless transition.5. Keep input-level chord triggers off combo actions
Enhanced Input can gate a press before it ever reaches the combo controller. Any UInputTrigger on the IMC mapping - the stock Chorded Action trigger, or a chord trigger a rebinding system injects into mappings for you - runs inside Enhanced Input, before the action fires. If one gates a combo action, the press never arrives while a modifier is held and chorded transitions can never fire.
- If your input setup injects a chord trigger into rebindable mappings, exclude every combo input action from that injection. Combo chords belong to the graph; the input-level gate is for rebindable action-bar style bindings.
- Do the same on the modifier input actions themselves - an injected empty chord would otherwise block a modifier's own action while it is held.
- Author each combo chord in the graph only, never also as a Chorded Action trigger on the same IMC mapping - the input-level trigger would block the press before the controller could match it.
Troubleshooting
Run Log LogCrimsonAbilitySystem Verbose and read the [Chord] lines - every press tells you what happened to it. How-To: Debug Combos (Dev Tools) covers the visual debugger.
| Log line | Meaning |
|---|---|
press <Action> accepted | The press entered the input buffer. |
press <Action> ignored: not in the combo catalog | The action is not mapped in the Combo Input Mapping Context. The suffix (declared modifier) means it is in the Modifier Input Mapping Context - working as intended. |
press <Action> DROPPED: already in CurrentlyHeldActions | No release was seen for the previous press. The usual cause is the Pressed trigger - see step 1's trigger warning. |
sample: ... sampled=<chord> | The modifiers seen at press time. vocabTags/vocabKeys count what the active graph names; heldTags/heldKeys what is held right now. |
REJECT <Action>: pressed=<X> required=<Y> | Exact-match chord mismatch between the press and this transition. |
MATCH <Action> (...) chord=<X> | A transition matched and will fire per its fire mode. |
No [Chord] line at all for a press | The press never reached the controller. Check your routing (step 2), then check step 5 - an input-level chord trigger may be eating it. |
See also
- How-To: Author a Combo - the graph this input drives.
- How-To: Tune Combo Input Feel - fire modes, tap/hold, buffering, defensive input.
- How-To: Debug Combos (Dev Tools) - the visual debugger.
- Concept: Combo System - the model behind it.