Saltmire
Practical Godot 4 tutorials — game feel, saving, and shipping polish fast.

RigidBody2D vs CharacterBody2D for knockback in Godot 4 — which and when

Every combat game needs knockback, and every combat game asks the same question at some point: should the thing that just got hit be a RigidBody2D, or should you fake the shove on the CharacterBody2D you already have? Both work. They fail in different ways, and picking the wrong one costs you a rewrite mid-project.

The CharacterBody2D approach: a velocity you decay

Most character controllers in Godot 4 are already CharacterBody2D, driven by move_and_slide(). Knockback here means adding a temporary velocity on top of the normal movement input and letting it bleed off over a few frames.

extends CharacterBody2D

var knockback := Vector2.ZERO
const KNOCKBACK_FRICTION := 900.0

func apply_knockback(direction: Vector2, force: float) -> void:
    knockback = direction.normalized() * force

func _physics_process(delta: float) -> void:
    var input_velocity := get_input_velocity() # your normal movement code

    knockback = knockback.move_toward(Vector2.ZERO, KNOCKBACK_FRICTION * delta)

    velocity = input_velocity + knockback
    move_and_slide()

This is cheap, deterministic, and stays inside move_and_slide()'s collision rules — the body still slides along walls correctly while it's being pushed. The catch: it is entirely your own simulation. There is no real physics response, so two bodies pushed into each other don't interact, and the "weight" of a hit is whatever curve you hand-tune into KNOCKBACK_FRICTION.

The RigidBody2D approach: apply_impulse and let physics do it

A RigidBody2D gives you a real physics body — mass, impulses, collision response between bodies for free.

extends RigidBody2D

func apply_knockback(direction: Vector2, force: float) -> void:
    apply_impulse(direction.normalized() * force)

That's the whole function. The physics engine handles the deceleration (via linear_damp), the collisions with other rigid bodies, and bodies piling into each other look and feel physical because they are physical. The catch: RigidBody2D doesn't give you move_and_slide()-style character control. If your enemy or player also needs precise, responsive movement input, you're now fighting the physics engine for control of the same body, and that fight usually shows up as jitter against walls or a character that "floats" a frame after landing.

The comparison

CharacterBody2D (faked) RigidBody2D (real)
Setup cost one velocity field + move_toward apply_impulse and done
Feel fully tunable, but hand-rolled physically consistent, less tunable
Body-to-body interaction none — you'd have to script it free, physics engine handles it
Movement input while knocked back trivial to blend fights the physics solver
Determinism yes mostly, minor solver variance
Good for player character, most enemies ragdolls, debris, crowds, destructibles

The hybrid most shipped games actually use

The honest answer is that neither option alone covers a whole game. The pattern that holds up: keep your player and most enemies on CharacterBody2D with faked knockback, because you need tight control over their movement every single frame. Switch to RigidBody2D only for things that are allowed to lose control entirely — a dead enemy's ragdoll, a barrel that goes flying, loose debris in a destructible wall.

A trick that gets you most of the RigidBody feel without giving up control: keep the CharacterBody2D, but scale the knockback force by the target's own "weight" stat and add a short window where player input is ignored so the hit actually reads:

var knockback_lock_timer := 0.0

func apply_knockback(direction: Vector2, force: float, weight: float = 1.0) -> void:
    knockback = direction.normalized() * (force / max(weight, 0.1))
    knockback_lock_timer = 0.12  # player input ignored for ~2 hit-stop frames

func _physics_process(delta: float) -> void:
    knockback_lock_timer = max(knockback_lock_timer - delta, 0.0)
    var input_velocity := Vector2.ZERO if knockback_lock_timer > 0.0 else get_input_velocity()

    knockback = knockback.move_toward(Vector2.ZERO, KNOCKBACK_FRICTION * delta)
    velocity = input_velocity + knockback
    move_and_slide()

That weight parameter is the part people skip and then wonder why every enemy flies the same distance regardless of size.

The number that actually decides it

If the object needs to respond to player input on the same frame it's simulating physics, it's a CharacterBody2D with faked knockback — no exceptions, because fighting the physics solver for input responsiveness is a losing, ongoing cost. If the object never takes player input again after the hit (a corpse, debris, a thrown prop), it's a RigidBody2D and you get the physics for free.

Wiring this up by hand means a direction, a force, a weight, teams so you don't knock back your own allies, and i-frames so one hit doesn't chain into five — all before you've written a single line of actual combat design. Saltmire Hitbox ships that whole layer as a drop-in Hitbox2D / Hurtbox2D pair, knockback included, so the comparison above is a five-minute decision instead of a rewrite.

Built this the long way once too many times. Saltmire Hitbox does it as a drop-in tool: https://saltmire.itch.io/saltmire-hitbox