# 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