# 5. I-Frames, Dodge and Guard Part of [[Combat Feel and Hit Feedback]]. Prev: [[4. Hit-Stop Knockback and Feedback]]. Next: [[6. Worked Example Three-Hit Melee]]. Rogers lists **dodging and guarding** next to attack strings in Level 10 ([[Level Up! (Review)]]). Both are readable *defense verbs*. The implementation is a state, a window, and a hurtbox that sometimes is not there. ## 5.1 I-frames are a disabled hurtbox Invulnerability frames are not a `bool` the overlap query forgets to read (you will forget). Call `Hurtbox.SetInvulnerable(true)`, which disables the collider. Queries and triggers both miss. Visual feedback (flicker, a trail, a material pulse) is mandatory or the player cannot tell they were safe. Drive the window from **animation events** (`Anim_IFramesOn` / `Anim_IFramesOff`) once the dodge clip exists. A timed fallback is fine for placeholders: ```csharp using UnityEngine; using UnityEngine.InputSystem; /// <summary> /// Short dodge with a movement burst and an i-frame window. /// NOT compiled in Unity. /// </summary> public sealed class DodgeController : MonoBehaviour { [SerializeField] InputActionReference dodgeAction; [SerializeField] Animator animator; [SerializeField] Hurtbox hurtbox; [SerializeField] MeleeComboController combo; [SerializeField] CharacterController character; [SerializeField] float burstSpeed = 12f; [SerializeField] float burstDuration = 0.18f; [SerializeField] float iFrameDuration = 0.22f; [SerializeField] float cooldown = 0.45f; [SerializeField] bool cancelAttackOnDodge = true; static readonly int DodgeHash = Animator.StringToHash("Dodge"); Vector3 _burst; float _burstUntil; float _iFrameUntil; float _readyAt; bool _eventDrivenIFrames; public bool IsDodging { get; private set; } void OnEnable() { if (dodgeAction == null || dodgeAction.action == null) return; dodgeAction.action.performed += OnDodge; dodgeAction.action.Enable(); } void OnDisable() { if (dodgeAction == null || dodgeAction.action == null) return; dodgeAction.action.performed -= OnDodge; EndDodge(); } void OnDodge(InputAction.CallbackContext _) { if (Time.unscaledTime < _readyAt) return; if (IsDodging) return; if (cancelAttackOnDodge) combo?.ForceEnd(); Vector3 dir = character != null ? character.velocity : transform.forward; dir.y = 0f; if (dir.sqrMagnitude < 0.05f) dir = transform.forward; dir.Normalize(); IsDodging = true; _burst = dir * burstSpeed; _burstUntil = Time.unscaledTime + burstDuration; _readyAt = Time.unscaledTime + cooldown; if (!_eventDrivenIFrames) { hurtbox?.SetInvulnerable(true); _iFrameUntil = Time.unscaledTime + iFrameDuration; } if (animator != null) animator.SetTrigger(DodgeHash); } void Update() { if (!IsDodging) return; if (character != null && Time.unscaledTime < _burstUntil) { Vector3 delta = _burst * Time.deltaTime; character.Move(delta); } if (!_eventDrivenIFrames && Time.unscaledTime >= _iFrameUntil) hurtbox?.SetInvulnerable(false); if (Time.unscaledTime >= _burstUntil && Time.unscaledTime >= _iFrameUntil) IsDodging = false; } // Animation events - once planted, set _eventDrivenIFrames = true in the Inspector // or call EnableEventDrivenIFrames from a setup script. public void EnableEventDrivenIFrames() => _eventDrivenIFrames = true; public void Anim_IFramesOn() { _eventDrivenIFrames = true; hurtbox?.SetInvulnerable(true); } public void Anim_IFramesOff() { hurtbox?.SetInvulnerable(false); IsDodging = false; } void EndDodge() { IsDodging = false; hurtbox?.SetInvulnerable(false); } } ``` Notes: - Cooldown and i-frame *timers* use unscaled time so a hit-stop does not extend your invulnerability by accident. The *movement burst* uses `Time.deltaTime` so it freezes with the world - you should not slide during someone else's hit-stop. - `CharacterController.velocity` is the velocity from the last `Move`. If your motor wraps `Move`, expose a `PlanarVelocity` from that motor instead of reading the controller. - Add a `Dodge` trigger and a Dodge state that can be entered from Locomotion and from Combat recovery (and, if the design allows, from windup). Do not enter Dodge from Active unless you are building a cancel-out-of-swing game and have authored it. - **Leave i-frames off before the burst starts.** A dodge that is invulnerable on frame 0 of the button press, before the body has moved, feels like a panic invuln, not a skill. ## 5.2 A simple guard A guard is a state with a front arc. Hits from behind still land. Hits from the front are reduced or blocked, and you can still be thrown by an "unblockable" flag later. ```csharp using UnityEngine; using UnityEngine.InputSystem; public sealed class GuardController : MonoBehaviour { [SerializeField] InputActionReference guardAction; [SerializeField] Animator animator; [SerializeField] float frontDot = 0.35f; // roughly 70 degrees either side [SerializeField] float blockedDamageScale = 0.2f; [SerializeField] float blockedKnockbackScale = 0.25f; static readonly int GuardingHash = Animator.StringToHash("Guarding"); public bool IsGuarding { get; private set; } void OnEnable() { if (guardAction == null || guardAction.action == null) return; guardAction.action.started += _ => SetGuarding(true); guardAction.action.canceled += _ => SetGuarding(false); guardAction.action.Enable(); } void OnDisable() { if (guardAction == null || guardAction.action == null) return; SetGuarding(false); } void SetGuarding(bool value) { IsGuarding = value; if (animator != null) animator.SetBool(GuardingHash, value); } public bool TryBlock(ref DamageInfo info) { if (!IsGuarding) return false; Vector3 inbound = -info.contactNormal; inbound.y = 0f; if (inbound.sqrMagnitude < 0.0001f) inbound = info.source != null ? (transform.position - info.source.transform.position) : -transform.forward; inbound.y = 0f; inbound.Normalize(); if (Vector3.Dot(transform.forward, inbound) > -frontDot) return false; // came from behind / side info.amount = Mathf.RoundToInt(info.amount * blockedDamageScale); info.knockback *= blockedKnockbackScale; info.hitStopDuration *= 0.5f; return true; } } ``` Call `TryBlock` at the top of `CombatVictim.ApplyDamage` before subtracting health. A blocked hit should still play *some* feedback (a different clip, a lighter Impulse) so the player knows the timing worked. "Always strive to enable the player, even when you're disabling them" ([[2.Modifiers and Timed Buffs]], Level 10 status-effect heading in the review) applies here too: a guard that eats the input and shows nothing feels like lag. ## 5.3 Exercise Tune i-frames until dodging *through* a single enemy swing is possible and dodging *in place* against a two-hit string is not. If both are possible, the window is too long. If neither is, the window is too short or the hurtbox is still enabled. Record a Play Mode capture and count frames; do not tune by vibe alone. ## Sources - [[Input System]] - performed / started / canceled on actions - [[5.Sub-State Machines Combat Locomotion]] - [[Level Up! (Review)]] Level 10