How-To: NPC Combos
Goal: have an AI enemy perform a combo string - start it, chain it step by step, end it, and tell a finisher from a whiff - driven from the NPC's StateTree, with enough variation that it does not read as a machine.
ACrimsonNPCBase, so both plugins must be enabled.ACrimsonNPCBase subclass with a UCrimsonNPCData asset that has an ability set and a StateTree - see Character Pipeline for how NPC ability sets are granted, and Spawning Pooled NPCs for the spawn path. A compiled combo graph asset and the ability that roots it: CrimsonAbilitySystem -> How-To: Author a Combo covers the graph and its transition windows, and CrimsonAbilitySystem -> How-To: Drive Combos from AI covers the authority drive API this page uses.What CrimsonCore adds
The combo state machine itself belongs to CrimsonAbilitySystem. CrimsonCore puts it on every AI character and gives StateTree a task that drives it.
| Piece | What it is |
|---|---|
ComboController | A UCrimsonComboController created by ACrimsonNPCBase as a default subobject. It is created on every NPC, never conditionally - a subobject whose existence depends on a project setting changes the CDO's component list between runs and corrupts NPC Blueprint archetypes when the setting flips. It is fully inert while the combo system is disabled or while nothing is granted. Read it with GetComboController() (Get Combo Controller in Blueprint). |
FCrimsonPerformComboTask | A StateTree task, shown in the node picker as Crimson Perform Combo under Crimson. Starts one combo on enter, drives it while the state is active, and succeeds once the string is over. |
ECrimsonCoreComboDrivePolicy | The four orders the task can try the active step's continuations in: First Available (graph order), Highest Priority First, Random, Explicit Path. |
| Pooling reset | ACrimsonNPCBase::DeactivateForPooling() resets a running combo with reason Manual before the ability system teardown, so a pooled NPC can never come back mid-step with a live phase tag still on its ability system component. |
UCrimsonComboController added by hand in the Components panel, delete it. ACrimsonNPCBase now creates one as a default subobject, and the Blueprint would carry two.1. Turn the combo system on
Open Project Settings -> Crimson -> Crimson Ability System and tick Enable Combo System, then restart the editor. The setting is read at module startup, so the restart is what makes the combo asset type, the graph editor and the Combo category on gameplay abilities exist. With it off, every drive call returns ECrimsonComboDriveResult::ComboSystemDisabled and the controller on your NPC does nothing at all.
UCrimsonGameplayAbility Blueprint shows a Combo category in its Class Defaults, and the Content Browser right-click menu offers a Crimson Combo Graph Asset.2. Grant the combo and its step abilities to the NPC
A driven combo has two hard requirements, and both are checked before anything plays: the graph must be granted and unlocked on this NPC's own ComboController, and every step ability in the graph must be granted on the NPC's ability system component. There is no press and no rooting ability involved - the drive path starts an asset the controller already holds.
The data path, which needs no code at all. Open the ability that roots the combo (any UCrimsonGameplayAbility Blueprint). In Class Defaults, under Combo -> Default Combos, add an entry: set Combo to your combo graph asset and leave Start Unlocked ticked. The controller seeds the grant the moment that ability is granted to the NPC. Then open the UCrimsonAbilitySet listed in your UCrimsonNPCData's Ability Sets and add, under Granted Abilities, both the root ability and every ability used as a Step Ability Class in the graph.
The runtime path, for a combo learned mid-fight: Event -> Has Authority (Branch, True) -> Get Combo Controller -> Grant Dynamic Combo (Root Ability Class, Combo) -> Preload Combo Abilities (Combo). Authored the entry with Start Unlocked off instead? Call Unlock Combo Grant (Root Ability Class, Combo) on the same controller. Both nodes live under Crimson|Combo|Grants and are server-only.
GetGrantedCombosFor(RootAbilityClass) (Get Granted Combos For) on the NPC's combo controller returns your graph asset, and GetUngrantedStepAbilities(Combo) returns an empty array. Call Preload Combo Abilities first and let a frame or two pass for a precise answer there - a step ability class that is not resident yet counts as ungranted.3. Add the task to the NPC's StateTree
The NPC's tree is the asset on UCrimsonNPCData::StateTree; ACrimsonNPCController assigns it and restarts logic on possession. Open that tree, select the state that means "attacking", and add the task Crimson Perform Combo. Its AIController context property binds itself to the schema's AI controller, so the only pin you must fill is Combo. This step is the same asset work in a Blueprint project and a C++ one.
| Property | Type | Default | What it does |
|---|---|---|---|
| AIController (Context) | AAIController | None | Auto-bound to the schema's AI controller. The combo controller is found on that controller's pawn. |
| Combo (Parameter) | UCrimsonComboGraphAsset | None | The graph the NPC performs. Required - the task fails on enter without it. |
| Policy (Parameter) | ECrimsonCoreComboDrivePolicy | Highest Priority First | The order continuations are tried in when the active step has more than one. See step 4. |
| Explicit Path (Parameter) | Array of FGuid | Empty | Explicit Path policy only: the node GUIDs to walk in order after the first step. Ignored by every other policy. |
| Drop Chance (Parameter) | float, clamped 0.0 to 1.0 | 0.0 | Chance per step that the NPC deliberately lets the string drop. Rolled once when a step begins, so it reads as "chance this step is the last one". 0.0 means it never drops on purpose. |
| Min Step Delay Seconds (Parameter) | float, clamped to 0.0 or more | 0.0 | Seconds to wait after a step begins before attempting a continuation. 0.0 means as soon as the transition window allows. |
| Completed Cleanly (Output) | bool | false | Written once, as the task succeeds: true when the combo ended on an End node, false when it dropped, timed out, or was reset. |
The task is polled rather than delegate-bound - StateTree instance data is a USTRUCT and cannot bind dynamic delegates - so every tick it re-reads the controller's own state. It drives the graph and nothing else: targeting, facing and range belong to the states and tasks around it. Crimson Focus Target on the parent state is the usual partner, bound to the Crimson NPC Context evaluator's HostileTarget.
| Phase | Result | When |
|---|---|---|
EnterState | Failed | The AI controller has no pawn, the pawn has no UCrimsonComboController, the pawn is not authoritative, no combo asset is set, or StartComboByAsset refused. Every one of those logs a warning naming the pawn and, for a refusal, the ECrimsonComboDriveResult value. |
EnterState | Running | The combo started. First the task preloads the step ability classes, warns about anything still ungranted, and snapshots the controller's completion counter. |
Tick | Failed | The combo controller went away: the pawn was unpossessed, destroyed, or returned to the pool mid-string. |
Tick | Succeeded | The combo is no longer active. Completed Cleanly is set from whether the completion counter changed, which only an End node does. |
Tick | Running | The string is still going. A refused continuation is never fatal - the window may simply not have opened yet, and the candidate list is rebuilt and retried next tick. |
ExitState | - | Cancels any pending drive latch, then resets the combo - but only on authority, only while a combo is still active, and only when the running combo is still this task's Combo. A string that already ended and was replaced by something else is not this task's to cancel. |
Crimson Perform Combo warning. A warning naming a refusal code tells you exactly which prerequisite is missing - ComboNotGranted for step 2, ComboSystemDisabled for step 1, NotAuthority if something is running the tree off the server.4. Choose a policy
When the active step has more than one outgoing transition, Policy decides the order the task asks for them in. It only picks an order: the combo controller stays the authority on whether a transition may actually fire, and it answers one candidate at a time until one is accepted.
| Policy | Order it asks in | Use it for |
|---|---|---|
| First Available (graph order) | The order the controller reports the active step's transitions in. | A string that should always take the same branch, without depending on priorities. |
| Highest Priority First | Sorted by the edge's Priority, descending - a larger Priority means preferred, matching the compile-time transition sort. This is the default. | Most enemies: author the intended string as the high-priority edges and let the fallbacks cover the rest. |
| Random | The whole candidate list is shuffled, so the second and third picks are unbiased too. It is rebuilt and reshuffled on each tick the task drives. | Variety between repeats of the same fight. |
| Explicit Path | Only the node at the current Explicit Path index, and only while that node really is a transition target of the active step. | Scripted set-pieces, boss phases, and tutorials where the exact string matters. |
Priority among them. The "would fire if pressed now" flag is deliberately not consulted: it answers whether a human press would fire the edge, which folds in the direction, chord and kind gates an authority drive request ignores. The task asks, and lets the refusal codes do the filtering.NodeGUID, and that is what Explicit Path holds - the nodes to walk after the first step. The walk advances only when the step that actually landed is the one the task asked for, so a transition fired by anything else does not desynchronize it. Once the list runs out, or the next GUID is not reachable from the active step, the task stops driving and lets the string end on its own. GetCurrentNodeID() on the combo controller reports the running step's GUID if you would rather read them while playing than copy them from the asset.5. Make it read as a person, not a machine
With both humanizing values left at 0.0 the task chains the instant the transition window allows and never drops a string on purpose. That is technically perfect and immediately readable as a machine. Two values fix it, and neither needs any code.
| Property | Value | What the player sees |
|---|---|---|
| Drop Chance | 0.0 | The NPC always performs the full string, every time. |
| Drop Chance | 0.25 | Roughly one step in four is the last one: the enemy sometimes stops after the first or second hit, which opens a punish window. |
| Drop Chance | 0.6 | Long strings become rare. Useful for a weak enemy that should mostly poke once and reset. |
| Min Step Delay Seconds | 0.0 | The next step fires on the first frame its window is open. |
| Min Step Delay Seconds | 0.15 | A short, consistent hesitation before each chain. Enough to read the first hit before the second arrives. |
| Min Step Delay Seconds | 0.4 | A visibly deliberate fighter. Set it longer than the step's transition window and the chain will not fire at all, so keep it inside the window you authored. |
Drop Chance is rolled once when a step begins, not every tick, which is what makes it read as "chance this step is the last one" rather than a per-frame coin flip. Min Step Delay Seconds measures from the moment the step began, not from the moment its window opened.
0.25 and Min Step Delay Seconds at 0.15, the same enemy produces different string lengths across a dozen engagements, and each chained hit is visibly separated from the one before it.6. Branch on Completed Cleanly
A dropped or timed-out string is a normal outcome, not a task failure, so the task succeeds either way and reports how it ended on Completed Cleanly. Branch the tree on the output, not on Succeeded versus Failed - Failed means the combo never ran at all.
In the StateTree asset, bind the task's Completed Cleanly output to a bool parameter on the state, then add two transitions out of the state that trigger on state completion, each with a bool condition comparing that parameter: true goes to your follow-up or recovery state, false goes back to the approach state. This is the same asset work in both Blueprint and C++ projects.
1.0 temporarily: the task always succeeds with Completed Cleanly false after the first step, and the tree takes the false branch every time. Set it back to 0.0 and the true branch runs once the string reaches its End node.7. Driving a combo without a StateTree (optional)
Nothing about the combo system is StateTree-specific. A Behavior Tree task, a custom controller, or a scripted sequence calls the same authority drive API on the NPC's combo controller. No Behavior Tree nodes ship - the drive API is what you would build one out of.
From any server-side graph on the NPC: Has Authority (Branch, True) -> Get Combo Controller -> Start Combo By Asset (Combo, with the Out Result pin telling you why a refusal happened). To chain, Request Transition To Node (Target Node ID, Timing) with Timing = When Window Opens, which latches the request on the active step and retries it until the window opens. Cancel Pending Drive Request drops the latch, and Get Completion Counter before and after tells a clean finish from a reset - compare for inequality only, the counter wraps.
OwnerIsPlayerControlled, which is the intended answer.Where it runs
| Piece | Runs on | Notes |
|---|---|---|
| The StateTree and the task | Server (authority) only | StateTree AI runs on the server, and the drive API checks HasAuthority() explicitly rather than relying on the Blueprint-only authority gate. The task fails cleanly on enter anywhere else instead of half-running. |
| The grant calls | Server only | GrantDynamicCombo, UnlockComboGrant and LockComboGrant are all authority-only. |
| The combo controller's replicated state | Nowhere useful, on an NPC | Grants and the current node replicate to the owning connection, and an AI pawn has no owning connection. Do not build client logic that reads an NPC's combo state off the controller. |
| What clients actually see | All clients | The step abilities and their montages replicate like any other ability, so every client sees the full string play out on the NPC. |
| The pooling reset | Server | DeactivateForPooling() resets a running combo before the ability system teardown. A node authored to ignore the Manual reset reason survives it, because ResetCombo honors a node's reset-ignore list. |
- Both clients see the NPC play the whole string, step by step, including the chained steps - the montages replicate even though the combo state does not.
- The string length varies between engagements once Drop Chance is above
0.0.- The server log has no
Crimson Perform Combo warning.- Kill the NPC mid-string while it is pooled (
bCanBePooled ticked on its UCrimsonNPCData) and spawn it again: it comes back idle, with no leftover combo phase tag, because DeactivateForPooling() reset it.API reference
| Type / member | Module | Purpose |
|---|---|---|
FCrimsonPerformComboTask | CrimsonCore | StateTree task, shown as Crimson Perform Combo under Crimson. Instance data FCrimsonPerformComboTaskInstanceData carries AIController, Combo, Policy, ExplicitPath, DropChance, MinStepDelaySeconds and the output bCompletedCleanly. Server only. |
ECrimsonCoreComboDrivePolicy | CrimsonCore | FirstAvailable (First Available (graph order)), HighestPriority (Highest Priority First, the default), Random, ExplicitPath. |
ACrimsonNPCBase::GetComboController() | CrimsonCore | Blueprint pure, category Crimson|NPC. Returns the UCrimsonComboController default subobject ComboController that every NPC carries. |
ACrimsonNPCBase::DeactivateForPooling() | CrimsonCore | Resets a running combo with reason Manual before the ability system teardown, then runs the rest of the pooling teardown. |
UCrimsonComboController | CrimsonAbilitySystem | The combo state machine. Its authority drive API - StartComboByAsset, RequestTransitionToNode, CancelPendingDriveRequest, GetCompletionCounter, GetUngrantedStepAbilities, PreloadComboAbilities - is what the task calls, and what a Behavior Tree would call. Documented in CrimsonAbilitySystem -> How-To: Drive Combos from AI. |
See also
- Character Pipeline - how an NPC's ability sets are granted, and why order matters
- Spawning Pooled NPCs - the spawn path and the pooling contract the reset belongs to
- CrimsonAbilitySystem -> How-To: Author a Combo - authoring a combo graph and its transition windows
- CrimsonAbilitySystem -> How-To: Drive Behavior from Step Phases - the step phase tags and the edge tag queries they gate
- CrimsonAbilitySystem -> How-To: Drive Combos from AI - the authority drive API and every refusal code