Skip to content

Weapon Knockback

Yogesh Sevda edited this page Oct 22, 2026 · 2 revisions

When the player hits an enemy with a weapon, the enemy should get pushed back so the player doesn't take damage while attacking. This page explains the fix for #302, merged in PR #410. (It was first merged as #375, reverted in #406 because it went in without review, then re-applied in #410.)

Test plan: Weapon Knockback Test Plan

What was wrong

Hitting an enemy didn't stop it from walking into you. The weird part is that knockback was already being applied, it just got undone straight away:

  1. The weapon hitbox touches the enemy and TouchAttackComponent.attack() gives the enemy's body an impulse (the knockback).
  2. Next frame, the enemy's PhysicsMovementComponent.update() steers it back towards the player. It does this with an impulse of (desiredVelocity - currentVelocity) * mass, which basically resets the body to its chase speed.
  3. So the knockback lasts one frame and the enemy just keeps coming.

There was also a smaller bug with the sword. The push direction was worked out from the centre of the hitbox instead of from the player. If an enemy was standing between you and the middle of the sword swing, it got pushed towards you.

What I changed

File Change
PhysicsMovementComponent Added stagger(float duration) and isStaggered(). While an enemy is staggered, update() doesn't steer it or hold it still, so the push can actually move it. The stagger is checked before the immobilise (freeze) check, see Medusa below. If it gets staggered again with a shorter time, it keeps the longer one. A negative duration throws IllegalArgumentException.
TouchAttackComponent After the knockback impulse, it staggers the target (if it has a PhysicsMovementComponent) for KNOCKBACK_STAGGER. Also added setKnockbackSource(Entity) so the push can come from a different entity than the hitbox. The knockback code is now in its own method, applyKnockback(Entity), which also keeps Sonar happy about method complexity.
HitboxFactory Sets the hitbox's owner (the player) as the knockback source.

Known limitations

  • Enemies that set their velocity directly in an AI task (like dash or stampede attacks) still get pushed, but they aren't staggered, so the task can override the push.
  • Bosses/minibosses with their own movement components don't get the stagger.
  • A frozen enemy that gets hit with knockback slides for the stagger time before the freeze holds it again. This is on purpose, so hits on frozen enemies still feel like hits.
  • Hits with knockback 0 (like the knife slashes) don't stagger anything. That's on purpose.

AI Declaration

Claude (Anthropic, Claude Code) was used to help find the cause of the bug and draft the code, tests and this page. I reviewed and tested everything before publishing.

When the player hits an enemy with a weapon, the enemy should get pushed back so the player doesn't take damage while attacking. This page explains the fix for [[#302](https://github.com/UQcsse3200/2026-studio-4/issues/302)](https://github.com/UQcsse3200/2026-studio-4/issues/302), merged in [[PR #410](https://github.com/UQcsse3200/2026-studio-4/pull/410)](https://github.com/UQcsse3200/2026-studio-4/pull/410). (It was first merged as [[#375](https://github.com/UQcsse3200/2026-studio-4/pull/375)](https://github.com/UQcsse3200/2026-studio-4/pull/375), reverted in [[#406](https://github.com/UQcsse3200/2026-studio-4/pull/406)](https://github.com/UQcsse3200/2026-studio-4/pull/406) because it went in without review, then re-applied in #410.)

Test plan: [Weapon Knockback Test Plan](Weapon-Knockback-Test-Plan)

What was wrong

Hitting an enemy didn't stop it from walking into you. The weird part is that knockback was already being applied, it just got undone straight away:

  1. The weapon hitbox touches the enemy and TouchAttackComponent.attack() gives the enemy's body an impulse (the knockback).
  2. Next frame, the enemy's PhysicsMovementComponent.update() steers it back towards the player. It does this with an impulse of (desiredVelocity - currentVelocity) * mass, which basically resets the body to its chase speed.
  3. So the knockback lasts one frame and the enemy just keeps coming.

There was also a smaller bug with the sword. The push direction was worked out from the centre of the hitbox instead of from the player. If an enemy was standing between you and the middle of the sword swing, it got pushed towards you.

What I changed

File Change
PhysicsMovementComponent Added stagger(float duration) and isStaggered(). While an enemy is staggered, update() doesn't steer it or hold it still, so the push can actually move it. The stagger is checked before the immobilise (freeze) check, see [Medusa](#medusa) below. If it gets staggered again with a shorter time, it keeps the longer one. A negative duration throws IllegalArgumentException.
TouchAttackComponent After the knockback impulse, it staggers the target (if it has a PhysicsMovementComponent) for KNOCKBACK_STAGGER. Also added setKnockbackSource(Entity) so the push can come from a different entity than the hitbox. The knockback code is now in its own method, applyKnockback(Entity), which also keeps Sonar happy about method complexity.
HitboxFactory Sets the hitbox's owner (the player) as the knockback source.

What happens on a hit now

hitbox touches enemy
  -> TouchAttackComponent.attack(target)
  -> targetStats.hit(...)               // damage, same as before
  -> applyKnockback(target)
       -> direction = enemy centre - source centre   (source = player if set)
       -> applyLinearImpulse(direction * knockback)
       -> movement.stagger(0.25s)
  -> enemy doesn't steer for 0.25s, so it slides back
  -> then it starts chasing again

Which way each weapon pushes

Weapon Pushes away from
Knife The player. Only the last stab of the upgraded flurry knocks back (1.5x), the slashes don't (see Knife).
Sword The player, no matter where the enemy is on the arc.
Bow The arrow, since arrows don't have an owner. In practice that's roughly the direction the arrow was flying.

If the enemy and the source are in exactly the same spot, it uses the hitbox centre instead so the direction is never zero.

Medusa

Medusa has a hit-recovery effect (MedusaHitRecoveryComponent) that freezes her for 0.35s whenever the player hits her. The freeze works through the immobilise check in PhysicsMovementComponent.update(), which sets the body's velocity to zero every frame.

In the first version of this fix, the immobilise check ran before the stagger check, so the freeze zeroed Medusa's velocity on the frame after the knockback and she didn't move back at all. Mitchell caught this in review on #410. The stagger check now runs first:

  1. Hit: Medusa takes damage, gets the 0.35s freeze, and is pushed back.
  2. 0 to 0.25s: she's staggered, so she slides back.
  3. 0.25 to 0.35s: the stagger is over and the freeze holds her still.
  4. After 0.35s: she chases again.

Freeze bombs, freeze traps and petrification still hold enemies completely still when nothing has just knocked them back.

Why it's done this way

  • I didn't want to change the movement maths itself, because every NPC made through NPCFactory.createBaseNPC() uses PhysicsMovementComponent. A short stagger only changes things right after a hit and leaves normal movement alone.
  • The stagger check sits in PhysicsMovementComponent.update() because every steered enemy goes through there anyway, so the AI tasks don't each need to know about knockback. Freeze/immobilise effects are already handled in the same spot, so it fits.
  • Pushing away from the player is what you'd expect when you hit something. Using the hitbox centre only happened to work for the knife.
  • The player doesn't use PhysicsMovementComponent, so enemies knocking the player around works exactly like before.

Tuning

Value Where What it does
Knockback 2f PlayerFactory, new WeaponStatsComponent(0.5f, 1f, 2f) How hard every weapon pushes.
KNOCKBACK_STAGGER = 0.25f TouchAttackComponent How long (seconds) an enemy stops chasing after being knocked back.

Known limitations

  • Enemies that set their velocity directly in an AI task (like dash or stampede attacks) still get pushed, but they aren't staggered, so the task can override the push.
  • Bosses/minibosses with their own movement components don't get the stagger.
  • A frozen enemy that gets hit with knockback slides for the stagger time before the freeze holds it again. This is on purpose, so hits on frozen enemies still feel like hits.
  • Hits with knockback 0 (like the knife slashes) don't stagger anything. That's on purpose.

AI Declaration

Claude (Anthropic, Claude Code) was used to help find the cause of the bug and draft the code, tests and this page. I reviewed and tested everything before publishing.

Table of Contents

Home

Getting Started

Game Engine

Entities and Components
Input Handling

Features

Components
Achievements
Player
Items and Inventory
Enemies
Bosses
Interactable NPCs
Cutscenes and Dialogue
Training Grounds
Music
Weapons
Abilities & Spells
Rooms and Dungeons
Game Screens and Areas
UI

Test Plans

Team 1
Team 2

Troubleshooting

Clone this wiki locally