# 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