quot;{name}: Hurtbox needs an IDamageable on damageableBehaviour.", this); } public void SetInvulnerable(bool invulnerable) { // Disabling the collider is the most reliable i-frame: queries and triggers both miss. _collider.enabled = !invulnerable; } public bool TryRegisterHit(int attackInstanceId) { if (_recentSet.Contains(attackInstanceId)) return false; _recent.Enqueue(attackInstanceId); _recentSet.Add(attackInstanceId); while (_recent.Count > rememberedHits) _recentSet.Remove(_recent.Dequeue()); return true; } } ``` `TryRegisterHit` is how one swing hits a target once even if the overlap lasts twelve frames. The id comes from the hitbox, which mints a new one each time it turns on. ## 3.3 Hitbox: prefer a query while active `OnTriggerEnter` misses the case where the blade is already overlapping the hurtbox on the frame it enables (no "enter"). A FixedUpdate **overlap query** while the hitbox is active does not have that hole. This is the same NonAlloc pattern as [[3. Layers Matrix and Queries]]. ```csharp using UnityEngine; /// <summary> /// Weapon volume enabled only during the active window. /// NOT compiled in Unity. /// </summary> public sealed class Hitbox : MonoBehaviour { [SerializeField] TeamId team = TeamId.Player; [SerializeField] Transform attackerRoot; [SerializeField] LayerMask hurtboxMask; [SerializeField] int bufferSize = 16; Collider _collider; readonly Collider[] _buffer = new Collider[16]; AttackDefinition _attack; int _instanceId; bool _active; int _nextInstance = 1; public bool IsActive => _active; void Awake() { _collider = GetComponent<Collider>(); _collider.isTrigger = true; _collider.enabled = false; if (_buffer.Length != bufferSize) Debug.LogWarning("Resize _buffer to match bufferSize, or allocate in Awake.", this); } public void Activate(AttackDefinition attack) { _attack = attack; _instanceId = _nextInstance++; _active = true; _collider.enabled = true; } public void Deactivate() { _active = false; _collider.enabled = false; _attack = null; } void FixedUpdate() { if (!_active || _attack == null) return; var box = _collider as BoxCollider; int count; if (box != null) { Vector3 worldCenter = box.transform.TransformPoint(box.center); Vector3 halfExtents = Vector3.Scale(box.size * 0.5f, box.transform.lossyScale); count = Physics.OverlapBoxNonAlloc( worldCenter, halfExtents, _buffer, box.transform.rotation, hurtboxMask, QueryTriggerInteraction.Collide); } else { // Fallback: use the collider bounds as an AABB (less precise for rotated capsules). Bounds b = _collider.bounds; count = Physics.OverlapBoxNonAlloc( b.center, b.extents, _buffer, Quaternion.identity, hurtboxMask, QueryTriggerInteraction.Collide); } for (int i = 0; i < count; i++) { var hurt = _buffer[i].GetComponent<Hurtbox>() ?? _buffer[i].GetComponentInParent<Hurtbox>(); if (hurt == null || hurt.Damageable == null) continue; if (!hurt.Damageable.IsAlive) continue; if (hurt.Damageable.Team == team) continue; if (hurt.Damageable.IsInvulnerable) continue; if (!hurt.TryRegisterHit(_instanceId)) continue; Vector3 point = _buffer[i].ClosestPoint(transform.position); Vector3 from = attackerRoot != null ? attackerRoot.position : transform.position; Vector3 planar = point - from; planar.y = 0f; if (planar.sqrMagnitude < 0.0001f) planar = attackerRoot != null ? attackerRoot.forward : transform.forward; planar.Normalize(); Vector3 knock = (planar + Vector3.up * _attack.knockbackUp).normalized * _attack.knockbackSpeed; var info = new DamageInfo { amount = _attack.damage, knockback = knock, contactPoint = point, contactNormal = -planar, hitStopScale = _attack.hitStopScale, hitStopDuration = _attack.hitStopDuration, attackInstanceId = _instanceId, sourceTeam = team, source = attackerRoot != null ? attackerRoot.gameObject : gameObject, attack = _attack }; hurt.Damageable.ApplyDamage(info); } } } ``` Pass `QueryTriggerInteraction.Collide` or you will miss trigger hurtboxes when the project-wide default is "ignore triggers." Mint `_instanceId` in `Activate`, not in `Awake`. A sword that stays enabled and only changes id once will never restrike on swing 2. ## 3.4 Animation events turn the box on and off Plant `Anim_HitboxOn` at the first active frame and `Anim_HitboxOff` at the last. Callbacks may take `AnimationEvent` or, in Unity 6, `AnimationEventInfo` (a ref struct that avoids allocating the `AnimationEvent` object). Either works; prefer `AnimationEventInfo` if your editor plants events the new way. ```csharp using UnityEngine; public sealed class AttackHitboxEvents : MonoBehaviour { [SerializeField] Hitbox hitbox; [SerializeField] MeleeComboController combo; public void Anim_HitboxOn() { var attack = combo != null ? combo.CurrentAttack : null; if (attack != null) hitbox.Activate(attack); } public void Anim_HitboxOff() => hitbox.Deactivate(); // Allocation-free path (6000.5 AnimationEventInfo). Same method name as above // cannot overload by AnimationEventInfo vs void - pick one signature per event. public void Anim_HitboxOn_Info(AnimationEventInfo info) { Anim_HitboxOn(); // info.animatorStateInfo.normalizedTime is available if you need to log it. } } ``` Event function names must match exactly, and the component must live on the same GameObject as the Animator (or a child that the event is configured to target - confirm in your clip's event inspector). See [[6.Animation Events Reliable Patterns]] for the "event fired after a CrossFade skipped the time" pitfall: if you blend in *past* the on-event, the box never enables. Either keep combat blends short, or also activate from a StateMachineBehaviour on Enter / a normalized-time fallback. ## 3.5 Exercise Draw the Layer Collision Matrix on paper before you click it. Then in Play Mode, turn on Gizmos (or a tiny `OnDrawGizmos` that draws the box when `IsActive`) and confirm the blade volume exists for a handful of frames only. Stand inside an enemy hurtbox, start a swing, and confirm you still get exactly one hit - that is the overlap-on-enable case `OnTriggerEnter` would miss. ## Sources - Layer-based collision detection (6000.5): https://docs.unity3d.com/6000.5/Documentation/Manual/LayerBasedCollision.html - `Physics.OverlapBoxNonAlloc` (6000.5): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Physics.OverlapBoxNonAlloc.html - `AnimationEventInfo` (6000.5): https://docs.unity.com/en-us/engine/6000.5/script-reference/unityengine/animationeventinfo - [[3. Layers Matrix and Queries]]