Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. GDScript
  3. GDScript Collisions Practice
GDScript7 exercises

GDScript Collisions Practice

Fix body_entered not firing, understand monitoring vs monitorable, configure collision layers/masks, and debug with Visible Collision Shapes. RigidBody2D contact_monitor explained.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Declare a custom signal called player_hit that takes an amount parameter.

On this page
  1. 1Layers vs Masks (The Rule That Actually Helps You Debug)
  2. "Why Does My Bullet Go Through the Enemy?"
  3. Recommended Layer Setup
  4. Setting Layers/Masks in Code
  5. Bitmask Shorthand (Readable Version)
  6. 2Monitoring vs Monitorable (Area2D)
  7. 3body_entered Not Firing? Debug Checklist
  8. 4Fast Bullets Going Through Targets?
  9. 5Area2D vs PhysicsBody2D Decision Tree
  10. 6Safe Collision Callbacks
Layers vs Masks (The Rule That Actually Helps You Debug)Monitoring vs Monitorable (Area2D)body_entered Not Firing? Debug ChecklistFast Bullets Going Through Targets?Area2D vs PhysicsBody2D Decision TreeSafe Collision Callbacks

Collision handling in Godot usually comes down to two "shapes":

  1. Physics bodies (like CharacterBody2D) that move and slide.
  2. Areas (Area2D) that detect overlaps and fire signals (hurtboxes, pickups, trigger zones, projectiles).

This page focuses on the patterns you'll actually type: connecting signals, filtering collisions, and keeping collision logic tidy.

Related GDScript Topics
GDScript MovementGDScript Timers & SignalsGDScript Projectiles

Layers = what you ARE, Masks = what you SCAN FOR. Set masks on the detecting object (player, bullets, sensors). Static walls just need a layer.

Layers = what you ARE. "I appear on layer 2 (Player)." Masks = what you SCAN FOR. "I detect things on layers 1 and 3."

The key insight: An object only detects/collides with things on layers included in its own mask. You usually set masks on the thing doing the detecting—the player, bullets, raycasts, and Area2D sensors. Static walls typically just sit on a layer and don't need masks.

Wall: Layer 1, Mask (none)  → Exists but doesn't detect anything
Player: Layer 2, Mask 1,3  → Detects walls and enemies
Enemy: Layer 3, Mask 1,2   → Detects walls and player

"Why Does My Bullet Go Through the Enemy?"

This is the #2 most common beginner question (after movement). The answer is almost always layers/masks:

Problem: Bullet and Enemy both on Layer 1, both scan Mask 1
Result: They detect each other... but so does everything else

Fix: Use dedicated layers

Recommended Layer Setup

Layer 1: World (walls, floors, obstacles)
Layer 2: Player
Layer 3: Enemies
Layer 4: Player Projectiles
Layer 5: Enemy Projectiles
Layer 6: Pickups

Then configure masks:

  • Player: Mask 1 (world), 3 (enemies), 5 (enemy bullets), 6 (pickups)
  • Enemy: Mask 1 (world), 2 (player), 4 (player bullets)
  • Player Bullet: Mask 1 (world), 3 (enemies) — NOT 2 (won't hit player)
  • Pickup: Mask 2 (player only)

Name your layers in Project Settings → General → Layer Names → 2D Physics to keep track.

Setting Layers/Masks in Code

# Set layer (what you are)
set_collision_layer_value(1, true)   # On layer 1
set_collision_layer_value(2, false)  # Not on layer 2

# Set mask (what you detect)
set_collision_mask_value(2, true)    # Detect layer 2

Bitmask Shorthand (Readable Version)

Raw numbers like collision_mask = 10 become cryptic when you change layers. Use bit operations for intent that reads clearly:

# Define your layers as constants
const L_WORLD := 1
const L_PLAYER := 2
const L_ENEMY := 3
const L_PLAYER_BULLET := 4

# Set layer (what you are)
collision_layer = 1 << (L_PLAYER - 1)  # Layer 2

# Set mask (what you detect) - combine with |
collision_mask = (1 << (L_WORLD - 1)) | (1 << (L_ENEMY - 1))  # Layers 1 and 3

This reads like intent ("I detect world and enemies") rather than numerology.

Ready to practice?

Start practicing GDScript Collisions with spaced repetition

monitoring = "my scanner is ON" (I detect overlaps). monitorable = "I show up on other scanners." Hitboxes monitor; hurtboxes are monitorable.

These two properties confuse everyone. Here's the mental model:

  • monitoring = "I am actively detecting overlaps" (scanner is ON)
  • monitorable = "I can be detected by other Areas" (I show up on their radar)
Hitbox: monitoring = true, monitorable = false  → Detects hurtboxes, invisible to them
Hurtbox: monitoring = false, monitorable = true → Detected by hitboxes, doesn't detect

Common gotcha: If your Area2D's body_entered isn't firing, check that monitoring = true (it's the default, but can get toggled). For area_entered, the other Area2D also needs monitorable = true.

Enable Debug → Visible Collision Shapes first. The top causes are: monitoring disabled, missing CollisionShape2D, wrong layers/masks, and RigidBody2D without contact_monitor = true.

Do these two things FIRST—they show you what's actually happening:

  1. Debug → Visible Collision Shapes — Shows collision shapes and raycasts while running
  2. Scene dock → Remote — Inspect live node collision layers/masks during gameplay

Then check these common causes:

  1. Shape exists and enabled? — CollisionShape2D/CollisionPolygon2D present, not disabled
  2. Monitoring enabled? — Area2D.monitoring = true (default, but can get toggled)
  3. Layers/masks match? — The detecting object's mask must include the other's layer
  4. Right signal? — body_entered for PhysicsBody2D, area_entered for Area2D
  5. RigidBody2D? — Collision signals require contact_monitor = true AND max_contacts_reported > 0 (often forgotten!)
  6. Monitorable? — For area_entered, the other Area2D needs monitorable = true

If your layers/masks are correct but fast projectiles still miss, the problem is tunneling—the bullet moves so far per frame that it skips over thin targets.

Solutions:

  • RayCast2D/ShapeCast2D — Sweep the bullet's path each frame (see projectiles). Use collision normals from vector math for bounce and deflection responses.
  • Longer collision shape — Capsule in the direction of travel
  • RigidBody2D with CCD — Set continuous_cd mode (slower but misses fewer collisions)

This is a physics frame-rate issue, not a layer/mask issue.

Need physical response (push, slide, bounce)?
├─ YES → CharacterBody2D / RigidBody2D
└─ NO → Just need to detect overlap?
    ├─ YES → Area2D with body_entered/area_entered
    └─ NO → Probably don't need collision at all

func _on_body_entered(body: Node) -> void:
    # Check group FIRST
    if not body.is_in_group(&"player"):
        return

    # Store what you need BEFORE freeing
    var damage := attack_damage
    var pos := global_position

    # Now safe to free
    queue_free()

    # Apply effect (body still valid this frame)
    body.take_damage(damage)

When to Use GDScript Collisions

  • Pickups and trigger zones (Area2D overlaps)
  • Damage/hit detection (hurtbox + hitbox Areas)
  • Projectile impacts and one-shot triggers

Check Your Understanding: GDScript Collisions

Prompt

Check Your Understanding: When would you use Area2D vs CharacterBody2D for collision logic?

What a strong answer looks like

Use Area2D when you primarily need overlap detection and events (pickups, triggers, hitboxes) and want signals like body_entered. Use CharacterBody2D when you need kinematic movement with sliding and floor/wall detection (move_and_slide, is_on_floor). Many games use both: bodies for movement, areas for gameplay interactions.

What You'll Practice: GDScript Collisions

Connect and handle Area2D body_entered/area_entered signalsConfigure collision layers/masks with readable bit operationsUse monitoring vs monitorable correctly for hitbox/hurtbox patternsDebug collisions with Visible Collision Shapes and Remote scene inspectionAvoid double-hits (cooldowns, disabling monitoring, one-shot triggers)Read collision info (collider, normals) from CharacterBody2D slide collisions

Common GDScript Collisions Pitfalls

  • Forgetting collision layers/masks—everything looks "right" but objects never detect each other. Enable Debug → Visible Collision Shapes to verify.
  • RigidBody2D collision signals require both contact_monitor = true AND max_contacts_reported > 0—both are off by default
  • Queue-freeing a node and then using it later in the same callback—store what you need first, or use call_deferred()
  • Fast projectiles tunneling through targets—not a layer/mask issue, use raycasts or CCD

GDScript Collisions FAQ

Why isn't my body_entered signal firing?

First, enable Debug → Visible Collision Shapes to see what's happening. Common causes: (1) Area2D.monitoring is false, (2) CollisionShape2D missing/disabled, (3) collision layers/masks don't overlap (your mask must include their layer), (4) using body_entered for an Area2D (use area_entered instead), (5) for RigidBody2D: contact_monitor = true AND max_contacts_reported > 0 both required.

What's the difference between monitoring and monitorable?

monitoring = "I actively detect overlaps" (scanner ON). monitorable = "I can be detected by other Areas". For body_entered, only monitoring matters. For area_entered, the other Area also needs monitorable = true. Hitboxes typically have monitoring ON, hurtboxes have monitorable ON.

Why does my bullet go through enemies?

Two possibilities: (1) Layers/masks wrong—bullet's mask must include enemy's layer. (2) Tunneling—fast bullet skips over thin target between frames. For tunneling, use RayCast2D to sweep the path or enable continuous_cd on RigidBody2D.

What's the difference between body_entered and area_entered?

body_entered triggers when a PhysicsBody2D (CharacterBody2D, RigidBody2D, StaticBody2D) enters. area_entered triggers when another Area2D enters. Pick based on what you expect to overlap.

How do collision layers and masks work?

Layers = what you appear on. Masks = what you scan for. An object detects things on layers included in its mask. Set masks on the detecting object (player, bullets, sensors). Static walls just need a layer.

How do I see collision shapes while the game runs?

Debug menu → Visible Collision Shapes. Also use Scene dock → Remote tab to inspect live node properties including collision_layer and collision_mask values.

Why does RigidBody2D not fire collision signals?

RigidBody2D requires contact_monitor = true AND max_contacts_reported set high enough (at least 1). Both are false/0 by default. This is the #1 forgotten setting.

How do I detect hits but not physically collide?

Use Area2D for detection. It fires signals but causes no physics response. Put hitbox/hurtbox Areas on layers that physics bodies don't collide with.

How do I do i-frames / invulnerability?

After taking damage: (1) set a flag and check it in callbacks, (2) temporarily disable collision layer/mask, or (3) set monitoring = false briefly. Use a Timer to restore after i-frame duration.

What happens if I change layers/masks at runtime?

Changes take effect immediately, but signals won't fire for objects already overlapping. Call get_overlapping_bodies() / get_overlapping_areas() to detect existing overlaps after a change.

GDScript Collisions Syntax Quick Reference

Hitbox/hurtbox setup
# Hitbox (on attacker) - detects hurtboxes
extends Area2D
func _ready() -> void:
	monitoring = true   # I detect
	monitorable = false # I'm invisible to other Areas
	area_entered.connect(_on_hit)

func _on_hit(area: Area2D) -> void:
	if area.is_in_group(&"hurtbox"):
		area.get_parent().take_damage(10)
Layer/mask with bit ops
const L_WORLD := 1
const L_PLAYER := 2
const L_ENEMY := 3

func _ready() -> void:
	# I am on player layer
	collision_layer = 1 << (L_PLAYER - 1)
	# I detect world and enemies
	collision_mask = (1 << (L_WORLD - 1)) | (1 << (L_ENEMY - 1))
Pickup overlap
extends Area2D

func _ready() -> void:
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
	if body.is_in_group(&"player"):
		body.call(&"add_gold", 1)
		queue_free()
Read slide collisions
func _physics_process(_delta: float) -> void:
	move_and_slide()
	for i in range(get_slide_collision_count()):
		var col := get_slide_collision(i)
		var collider := col.get_collider()
		if collider.is_in_group(&"enemy"):
			take_damage(1)

GDScript Collisions Sample Exercises

Example 1Difficulty: 1/5

Fill in the blank to emit the damage signal.

emit
Example 2Difficulty: 2/5

Connect the `died` signal to `_on_enemy_died` using the flag that allows connecting the same signal to the same callable multiple times.

died.connect(_on_enemy_died, CONNECT_REFERENCE_COUNTED)

+ 5 more exercises

Practice in Build a Game

Arena Survivor: Part 3: Survive the SwarmBuild a GameRoguelike: Part 2: Every Turn Has TeethBuild a Game

Further Reading

  • GDScript Dictionary map() and map_in_place12 min read
  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Start practicing GDScript Collisions

Free daily exercises with spaced repetition. No credit card required.

← Back to GDScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.