Quick answer: Enable Contact Monitor on the RigidBody2D and set Max Contacts Reported to at least 1 (8+ for characters). Both are off/zero by default, which is the single most common reason body_entered never fires.

You set up a RigidBody2D for a bouncing enemy, connect the body_entered signal to a callback, and nothing ever fires. You print debug messages, add rocks, double-check the script is attached, and the signal remains silent. The bug is not in your code — it is in two inspector fields that are easy to miss.

The Symptom

A RigidBody2D with a properly-connected signal never reports contacts:

The Two Missing Flags

RigidBody2D has a Contact Monitor section that is disabled by default for performance reasons. Godot’s physics engine does not track contact information unless you explicitly ask it to.

Two fields must be set:

If either is missing, the body does not emit body_entered or body_exited signals and does not populate the contact list. Enabling Contact Monitor without setting Max Contacts Reported is the most common mistake — the field defaults to 0, which means “track no contacts.”

The Fix

Step 1: Enable both in the inspector.

Select the RigidBody2D in the scene tree. In the Inspector, scroll to the Contact Monitor section. Check Enabled. Set Max Contacts Reported to 8 for a character or at least 1 for a simple trigger body.

Or in code during _ready:

extends RigidBody2D

func _ready():
    contact_monitor = true
    max_contacts_reported = 8
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)

func _on_body_entered(body: Node) -> void:
    print("Touched: ", body.name)

func _on_body_exited(body: Node) -> void:
    print("Left: ", body.name)

Step 2: Confirm you are connecting on the right node.

The body_entered signal exists on RigidBody2D, Area2D, and a few other physics nodes. It does not exist on StaticBody2D or CharacterBody2D. If you are trying to detect collisions on a character, either use a child Area2D as a hitbox or use the move_and_slide return value to walk through the collision list after each tick.

Step 3: Size Max Contacts Reported correctly.

Any contacts beyond the reported limit are dropped silently. A pile of rocks on top of a character with max_contacts_reported = 1 will fire body_entered once (for one rock) and miss the rest. For reliable detection, size the limit to the worst-case number of simultaneous contacts you expect. 8 is a good default for characters; 1–2 works for projectiles.

Should You Use Area2D Instead?

If you only need to know when something enters or exits a region, Area2D is cheaper and simpler. It has the same body_entered signal, no Contact Monitor flag to forget, and no physical response. Use RigidBody2D with Contact Monitor when you need both the bounce and the notification — for example, a billiard ball that needs to play a sound on impact while still following physics.

Verifying the Fix

Add a print statement to the signal callback. Run the scene. Walk the body into a StaticBody2D. The print should fire. If it does not, the Contact Monitor or Max Contacts value is still wrong — inspect the node at runtime with print(contact_monitor, max_contacts_reported) and confirm the values persisted.

For more granular debugging, log get_contact_count() in _physics_process. It should return 1 or more while the body is touching something, and 0 when it is not. If it stays at 0, the monitor is still off.

“Godot’s Contact Monitor defaults to off for good reasons — tracking contacts is not free. Turn it on explicitly when you need it and remember that Max Contacts Reported = 0 is the same as having Contact Monitor disabled.”

Related Issues

For Area2D-specific collision issues, see Godot Area2D body_entered signal not firing. For Area3D, see Godot Area3D overlap detection not working. For collision layers that prevent detection entirely, see Godot collision layer mask not working.

Contact Monitor + Max Contacts Reported is one of those two-step settings where missing either one breaks everything silently. Make it a checklist item.