Designing Combat Systems and Interaction Mechanics
A combat system doesn't break where you design it—it breaks at the junction between animation, hitbox, and damage logic. A typical scenario: a designer places the attack window at Animation Event frame 12, the programmer reads it in OnAttackHit(), and QA finds that the hit passes through the enemy at fps below 30—because Physics.OverlapSphere is called exactly once at the moment of the event, and the enemy collider doesn't overlap with the hitbox between frames.
Hitbox System Architecture
A professional implementation separates hurtbox (area you can hit) and hitbox (area the character attacks with). Both are separate colliders on child objects, managed via Hurtbox : MonoBehaviour and Hitbox : MonoBehaviour components.
The hitbox is activated not via SetActive(), but through toggling isTrigger and layer—this is cheaper performance-wise for frequent on/off. A single attack hitbox must hit each hurtbox only once: this is controlled via HashSet<int> with InstanceID of already-hit targets, which is cleared when the hitbox deactivates.
For melee weapons with fast movements, OverlapSphere per frame isn't enough. More reliable is Physics.CapsuleCast from the weapon position on the previous frame to the current one—this catches targets in the blade's trajectory between frames. previousPosition is stored in LateUpdate() of the previous frame.
Managing Combat States
The combat state machine is not an Animator State Machine. Animator manages the visuals; game logic lives in a separate CombatStateMachine. States: Idle, Attacking, Recovering, Staggered, Blocking, Parrying. Each state is a separate class with Enter(), Update(), Exit().
Attack cancel priority is one of the most complex parts. In fighting games and action-RPGs, the player should be able to cancel part of the attack animation into a dash or the next hit. This is implemented via cancelWindows[]—an array of structures with startFrame, endFrame, allowedCancels. When the normalized Animator time falls within a window, the canCancel flag is raised, and CombatStateMachine accepts new input.
Interaction Systems: Interactive Objects and Dialogs
The interaction component is built on the Interactable / Interactor scheme. IInteractable is an interface with the Interact(GameObject interactor) method. InteractorComponent on the player holds List<IInteractable> within range, updated via OnTriggerEnter/Exit. On button press, closest.Interact(gameObject) is called.
A common mistake is implementing interaction via Raycast in Update() every frame. This is both unnecessary computation and priority problems with multiple objects in the ray. A trigger zone with OverlapSphere checked every 0.1 seconds via InvokeRepeating is cheaper and more reliable.
For dialog interactions, an event queue is important. If the player presses the button again during dialog, the next Interact shouldn't be processed until the current dialog closes. The isInteracting flag in InteractorComponent blocks new interactions—and is cleared via the OnInteractionComplete event.
Balancing Responsiveness and Readability
Feedback on hit is critical for combat feel. A minimal set: hitpause (stop the attacker's animation for 2–4 frames on hit), screen shake via CinemachineImpulse, sound effect with pitch variation. Hitpause is implemented by temporarily setting animator.speed = 0 and not touching Time.timeScale—important if there's UI or other systems.
Damage numbers are a separate conversation. Floating text with TextMeshPro should be instantiated from a pool, not via Instantiate() every hit. In active combat with AoE attacks, without a pool you can easily get 50+ instantiations per second and GC spike.
Timeline Guidelines
| Scale | Components | Timeframe |
|---|---|---|
| Basic | Single attack, hurtbox/hitbox, HP component | 3–6 days |
| Medium | Combo system, block, parry, i-frames | 2–3 weeks |
| Extended | Multiple weapon types, abilities, status effects | 4–6 weeks |
| Full system | Network combat sync, rollback netcode | 2–4 months |
Design Process
We start with a state and transition table in a document—before writing code. Each state, each transition, condition, and priority. This uncovers conflicts at design stage: for example, what happens if the player takes damage while parrying—stagger or not?
Then—a prototype with placeholder animations (even cubes) to verify attack window timing and cancel system. Integration with real animations is the last stage, not the first.





