# 2. Animator Attack Strings
Part of [[Combat Feel and Hit Feedback]]. Prev: [[1. Combat Feel and the Hit Pipeline]]. Next: [[3. Hitboxes and Hurtboxes]].
The Animator graph from [[5.Sub-State Machines Combat Locomotion]] is the presentation. The **combat brain** decides intent: "the player asked for an attack; we are in a cancel window; the next clip is hit 2." Keep that decision in C#. The Animator gets a small parameter surface and, when you must force a transition, a `CrossFadeInFixedTime` in seconds.
## 2.1 Graph shape for a 3-hit string
Inside the Combat Sub-State Machine:
```
Entry
-> Attack1 (windup, active, recovery are *in the clip*, not three states, unless you need them)
-> Attack2 (from Attack1 when Attack trigger fires AND CanCancel)
-> Attack3 (from Attack2 the same way)
-> Exit to Locomotion (from any recovery when Attack is not buffered, or from Anim_CombatExit)
```
You *can* split each swing into Windup / Active / Recovery states. That is clearer when those phases are different clips or when you need precise state tags. For a student 3-hit string, **one clip per swing plus animation events** is less graph and enough control.
Parameters (same budget as the Animation Advanced facade, plus a buffer in code, not in the graph):
| Param | Type | Owner |
|---|---|---|
| `Attack` | trigger | Combat brain. Consumed by a transition; reset if you cancel the request ([[Animation parameters](https://docs.unity.com/en-us/engine/6000.5/manual/animation-section/animation-mecanim/animation-animator-controller/animation-state-machines/animation-parameters)]) |
| `AttackId` | int | Which clip / branch (0, 1, 2) |
| `InCombat` | bool | Gates entry from Locomotion |
| `CanCancel` | bool | Set by events, or from normalized time as a fallback |
| `Hit` / `Die` | trigger | Hit-react and death, higher priority |
Transitions:
1. Locomotion -> Combat when `Attack` is set (and usually `IsGrounded`).
2. Attack1 -> Attack2 when `Attack` is set and `CanCancel` is true. Same for 2 -> 3.
3. Any Attack -> Locomotion when `InCombat` becomes false (event at end of recovery) or when a `Die`/`Hit` path takes over.
4. Author **Interruption Source** so Hit can interrupt a swing, but Attack2 cannot interrupt Attack1's active frames ([[2.Sync Layers Write Defaults and Interruption]]).
Do not spray Any-State -> Attack1 transitions. That is how strings double-fire.
## 2.2 Input and a combo buffer
An attack press during active frames should not be ignored and should not start a new swing. It should sit in a **short buffer**. When the cancel window opens, the brain consumes the buffer and requests the next `AttackId`.
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// Buffers an Attack press and advances a 3-hit string when CanCancel is open.
/// NOT compiled in Unity.
/// </summary>
public sealed class MeleeComboController : MonoBehaviour
{
[SerializeField] Animator animator;
[SerializeField] InputActionReference attackAction;
[SerializeField] AttackDefinition[] stringAttacks; // index = AttackId
[SerializeField] float bufferSeconds = 0.2f;
static readonly int AttackHash = Animator.StringToHash("Attack");
static readonly int AttackIdHash = Animator.StringToHash("AttackId");
static readonly int InCombatHash = Animator.StringToHash("InCombat");
static readonly int CanCancelHash = Animator.StringToHash("CanCancel");
int _index = -1;
float _bufferUntil;
bool _inString;
public AttackDefinition CurrentAttack =>
_index >= 0 && _index < stringAttacks.Length ? stringAttacks[_index] : null;
public int AttackIndex => _index;
void OnEnable()
{
if (attackAction == null || attackAction.action == null) return;
attackAction.action.performed += OnAttack;
attackAction.action.Enable();
}
void OnDisable()
{
if (attackAction == null || attackAction.action == null) return;
attackAction.action.performed -= OnAttack;
}
void OnAttack(InputAction.CallbackContext _)
{
_bufferUntil = Time.unscaledTime + bufferSeconds;
TryConsumeBuffer();
}
void Update()
{
if (_inString && animator.GetBool(CanCancelHash))
TryConsumeBuffer();
}
void TryConsumeBuffer()
{
if (Time.unscaledTime > _bufferUntil) return;
if (!_inString)
{
BeginString(0);
return;
}
if (!animator.GetBool(CanCancelHash)) return;
if (_index + 1 >= stringAttacks.Length) return;
BeginString(_index + 1);
}
void BeginString(int index)
{
_index = index;
_inString = true;
_bufferUntil = 0f;
animator.SetInteger(AttackIdHash, stringAttacks[index].attackId);
animator.SetBool(InCombatHash, true);
animator.SetBool(CanCancelHash, false);
animator.ResetTrigger(AttackHash);
animator.SetTrigger(AttackHash);
}
// Animation event: end of the last recovery, or early exit.
public void Anim_CombatExit()
{
_inString = false;
_index = -1;
animator.SetBool(InCombatHash, false);
animator.SetBool(CanCancelHash, false);
animator.ResetTrigger(AttackHash);
}
public void ForceEnd()
{
Anim_CombatExit();
}
}
```
Why `Time.unscaledTime` for the buffer? If a hit-stop drops `Time.timeScale` (part 4), a scaled buffer would stall and the player would think the press was eaten. The swing *animation* should freeze with the world; the input buffer should not.
## 2.3 Triggers versus CrossFadeInFixedTime
`SetTrigger("Attack")` is the right default: the transition you authored (duration, interruption, conditions) still runs. Use `Animator.CrossFadeInFixedTime` when the brain must *force* a state in **seconds**, bypassing a condition you cannot express in the graph - for example, a scripted stun that should blend 0.05s into HitReact regardless of `CanCancel`.
```csharp
// Seconds, not normalized time. State name can be "Combat.Attack2" if you need the path.
animator.CrossFadeInFixedTime("HitReact", 0.05f, 0, 0f);
```
`CrossFade` (no `InFixedTime`) uses *normalized* times, so a 2-second clip and a 0.3-second clip with a "0.1" blend are very different wall-clock blends. Combat cancels want seconds. Confirm the state exists with `HasState` if you generate names in data.
If a trigger is still set when you enter hit-react or death, **reset it**. Leftover triggers are the usual cause of a swing that starts after a stun. `ResetTrigger` is the API; call it on `ForceEnd`, `RequestDeath`, and `RequestHit`.
## 2.4 Fallback cancel window from normalized time
Until events are planted on the clip, you can open `CanCancel` from `AnimatorStateInfo.normalizedTime`. Looping states keep counting past 1, so use the fractional part if a clip can loop (attack clips should not).
```csharp
void LateUpdate()
{
if (!_inString || CurrentAttack == null) return;
var info = animator.GetCurrentAnimatorStateInfo(0);
if (animator.IsInTransition(0)) return;
float t = info.normalizedTime;
if (info.loop) t %= 1f;
bool inWindow = t >= CurrentAttack.cancelNormalizedStart
&& t <= CurrentAttack.cancelNormalizedEnd;
animator.SetBool(CanCancelHash, inWindow);
}
```
Prefer events once the clip is final: a normalized-time window is a function of clip length, and designers will tweak the clip without noticing the window moved. See [[6.Animation Events Reliable Patterns]].
## 2.5 Exercise
Build Attack1 / Attack2 / Attack3 with cube-slash placeholder clips (or reused locomotion poses). Prove the buffer: mash the button during Attack1's windup and confirm Attack2 starts only after the cancel event. Then try a press *after* recovery with no buffer time left - you should start a *new* string at Attack1, not skip to Attack3.
## Sources
- Animation parameters (6000.5): https://docs.unity.com/en-us/engine/6000.5/manual/animation-section/animation-mecanim/animation-animator-controller/animation-state-machines/animation-parameters
- `Animator` (6000.5): https://docs.unity.com/en-us/engine/6000.5/script-reference/unityengine/animator
- `AnimatorStateInfo` (6000.5): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AnimatorStateInfo.html