Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot 2d Physics

  • 317 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-2d-physics for development tasks

About

godot-2d-physics: A skill for development. This provides functionality for development workflows.

  • godot-2d-physics

Godot 2d Physics by the numbers

  • 317 all-time installs (skills.sh)
  • +24 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #1,291 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-2d-physics

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs317
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-2d-physics for development tasks

Files

SKILL.mdMarkdownGitHub ↗

2D Physics

Expert guidance for collision detection, triggers, and raycasting in Godot 2D.

NEVER Do

  • NEVER scale `CollisionShape2D` nodes — Use the shape handles in the editor, NOT the Node2D scale property. Scaling causes unpredictable physics behavior and incorrect collision normals [12].
  • NEVER confuse `collision_layer` with `collision_mask` — Layer = "What AM I?", Mask = "What do I DETECT?". Setting both to the same value is usually wrong [13].
  • NEVER multiply velocity by delta when using `move_and_slide()`move_and_slide() automatically includes timestep. Only multiply gravity/acceleration by delta [14].
  • NEVER forget `force_raycast_update()` for manual mid-frame raycasts — Raycasts update once per physics frame. If you change target_position, you MUST force an update [15].
  • NEVER use `get_overlapping_bodies()` every frame — It is expensive. Cache results with body_entered/body_exited signals instead [16].
  • NEVER modify `RigidBody2D` state directly in `_process` — Use _integrate_forces() for safe, synchronized access to PhysicsDirectBodyState2D [17, 411].
  • NEVER move `PhysicsBody2D` nodes in `_process()` — Use _physics_process(). Moving bodies outside the physics step causes stutter and unreliable collision detection.
  • NEVER use `RigidBody2D` for 1000+ simple entities — Use PhysicsServer2D to bypass node overhead for massive performance gains (Swarms/Bullets) [18, 397].
  • NEVER use `Area2D` for high-frequency blocking (Bullets) — Area signals can be delayed. Use move_and_collide() or ShapeCast2D for frame-perfect results [19].
  • NEVER ignore 'Physics Jitter' on high-refresh monitors — Enable Physics Interpolation to prevent micro-stutter in motion [21, 400].
  • NEVER scale collision shapes directly at runtime — It causes major instability. Resize the shape resource (size/radius) instead.
  • NEVER use `set_deferred` for immediate physics transform logic — It happens at the end of the frame. Use force_raycast_update() or PhysicsServer2D instead.
  • NEVER leave Continuous CD (CCD) enabled for slow objects — It adds significant CPU overhead. Reserve it for high-speed projectiles to prevent tunneling.
  • NEVER use a single collision layer for all tiles/entities — Separate layers (Ground, Walls, Enemies) to allow selective filtering via masks.
  • NEVER forget to free `PhysicsServer2D` RIDs manually — They are not garbage collected and will leak memory permanently.

---

Available Scripts

MANDATORY: Read the script matching your use case before implementation.

collision_setup.gd

Programmatic layer/mask management with named layer constants and debug visualization.

physics_query_cache.gd

Frame-based caching for PhysicsDirectSpaceState2D queries - eliminates redundant expensive queries.

custom_physics.gd

Custom physics integration patterns for CharacterBody2D. Covers non-standard gravity, forces, and manual stepping. Use for non-standard physics behavior.

physics_queries.gd

PhysicsDirectSpaceState2D query patterns for raycasting, point queries, and shape queries. Use for line-of-sight, ground detection, or area scanning.

physics_server_swarm.gd

Low-level PhysicsServer2D usage for thousands of moving objects. Bypasses node overhead for massive performance gains in bullet hells or swarms.

substepping_logic.gd

Manual physics sub-stepping for high-velocity projectiles. Ensures frame-perfect collision for objects moving faster than the physics tick.

safe_rigidbody_state.gd

Thread-safe RigidBody2D modification using _integrate_forces. Ideal for teleporting bodies or applying custom impulses without jitter.

physics_direct_query.gd

Lighweight environment sensing using PhysicsDirectSpaceState2D. Performs ray queries without the overhead of RayCast2D nodes.

collision_bitmask_helper.gd

Clean architectural pattern for managing complex collision layers/masks using bitwise Enums and helpers.

raycast_vision_stack.gd

Optimized multicasting vision system for AI. Reuses a single RayCast2D to check multiple angles in one physics frame.

shapecast_aoe.gd

Robust AOE detection using ShapeCast2D. Provides instant collision information without the signal-lag of Area2D.

custom_gravity_override.gd

Logic for localized gravity zones (Water, Space, Wind) and manual character-weight simulation.

collision_debouncer.gd

Expert pattern for preventing signal spam when multi-shape bodies enter triggers.

jitter_interpolation_fix.gd

Standard configuration and runtime adjustments to ensure smooth character movement on high-refresh-rate monitors.

physics_server_direct_body.gd

Direct PhysicsServer2D RID management for peak performance in massive physics simulations.

move_and_collide_precision.gd

Expert bounce and friction logic implementation for precision-critical movement.

continuous_collision_detection.gd

Advanced CCD management for preventing bullet tunneling at extremely high velocities.

performance_batch_mover.gd

Optimized batch movement for multiple static/animatable bodies using riders-aware logic.

---

Collision Layers & Masks (Bitmask Deep Dive)

The Mental Model

# collision_layer (32 bits): What broadcast channels am I transmitting on?
# collision_mask (32 bits): What broadcast channels am I listening to?

# Example: Player vs Enemy
# Player:
#   layer = 0b0001 (Channel 1: "I am a player")
#   mask  = 0b0110 (Channels 2+3: "I listen for enemies and walls")
# Enemy:
#   layer = 0b0010 (Channel 2: "I am an enemy")
#   mask  = 0b0101 (Channels 1+3: "I listen for players and walls")

Bitmask Helpers

# ✅ GOOD: Use helper functions for clarity
func setup_player_collision() -> void:
    # I am layer 1
    set_collision_layer_value(1, true)
    
    # I detect layers 2 (enemies) and 3 (world)
    set_collision_mask_value(2, true)
    set_collision_mask_value(3, true)

# ✅ GOOD: Bit shift for programmatic layer math
func enable_layers(base_layer: int, count: int) -> void:
    var mask := 0
    for i in range(count):
        mask |= (1 << (base_layer + i - 1))
    collision_mask = mask

# ❌ BAD: Hardcoded bitmasks without documentation
collision_mask = 0b110110  # What does this mean?!

Common Patterns

# Pattern: Projectile that hits enemies but ignores other projectiles
# projectile.gd
extends Area2D

func _ready() -> void:
    set_collision_layer_value(4, true)   # Layer 4: "Projectiles"
    set_collision_mask_value(2, true)    # Mask Layer 2: "Enemies"
    # Result: Projectiles don't collide with each other

# Pattern: One-way platform (player can jump through from below)
# platform.gd
extends StaticBody2D

@export var one_way := true

func _ready() -> void:
    set_collision_layer_value(3, true)   # Layer 3: "World"
    if one_way:
        # Use Area2D + collision exemption instead
        # (Standard one-way platforms use different technique)
        pass

---

Area2D Expert Patterns

Problem: Duplicate Triggers on Multi-CollisionShape

# ❌ BAD: body_entered fires MULTIPLE times if Area2D has multiple shapes
extends Area2D

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

func _on_body_entered(body: Node2D) -> void:
    print("Entered!")  # Fires 3x if Area has 3 CollisionShapes!

# ✅ GOOD: Track unique bodies with Set
extends Area2D

var _active_bodies := {}  # Use dict as Set

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

func _on_body_entered(body: Node2D) -> void:
    if body not in _active_bodies:
        _active_bodies[body] = true
        print("First entrance!")  # Fires once

func _on_body_exited(body: Node2D) -> void:
    _active_bodies.erase(body)

Damage-Over-Time with Immunity Frames

# lava_zone.gd
extends Area2D

@export var damage_per_tick := 5
@export var tick_rate := 0.5  # Damage every 0.5s

var _damage_timers := {}  # body -> time_until_next_tick

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

func _on_body_entered(body: Node2D) -> void:
    if body.has_method("take_damage"):
        _damage_timers[body] = 0.0  # Immediate first tick

func _on_body_exited(body: Node2D) -> void:
    _damage_timers.erase(body)

func _process(delta: float) -> void:
    for body in _damage_timers.keys():
        _damage_timers[body] -= delta
        if _damage_timers[body] <= 0.0:
            body.take_damage(damage_per_tick)
            _damage_timers[body] = tick_rate

---

RayCast2D Advanced Usage

Dynamic Raycast Rotation

# enemy_vision.gd - Enemy looks toward player
extends CharacterBody2D

@onready var vision_ray: RayCast2D = $VisionRay

func can_see_target(target: Node2D) -> bool:
    var direction := global_position.direction_to(target.global_position)
    vision_ray.target_position = direction * 300  # 300px range
    vision_ray.force_raycast_update()  # CRITICAL: Update mid-frame
    
    if vision_ray.is_colliding():
        return vision_ray.get_collider() == target
    return false

Multipa Raycasts for Ledge Detection

# platformer_controller.gd
extends CharacterBody2D

@onready var floor_front: RayCast2D = $FloorCheckFront
@onready var floor_back: RayCast2D = $FloorCheckBack

func at_ledge() -> bool:
    return floor_front.is_colliding() and not floor_back.is_colliding()

func _physics_process(delta: float) -> void:
    if at_ledge() and is_on_floor():
        # Enemy AI: Turn around at ledges
        velocity.x *= -1

Raycast Exclusions

# Ignore specific bodies (e.g., self)
func _ready() -> void:
    $RayCast2D.add_exception(self)
    $RayCast2D.add_exception($Weapon)  # Ignore attached weapon collider

# Reset exclusions
$RayCast2D.clear_exceptions()

---

PhysicsDirectSpaceState2D (Manual Queries)

Point Query: Click Detection

# Check if mouse click hits any physics body
func get_body_at_mouse() -> Node2D:
    var mouse_pos := get_global_mouse_position()
    var space := get_world_2d().direct_space_state
    
    var query := PhysicsPointQueryParameters2D.new()
    query.position = mouse_pos
    query.collide_with_areas = false
    query.collision_mask = 0b11111111  # All layers
    
    var results := space.intersect_point(query, 1)  # Max 1 result
    if results.is_empty():
        return null
    return results[0].collider

Shape Cast: AOE Attack

# AOE damage in circle around player
func damage_nearby_enemies(center: Vector2, radius: float, damage: int) -> void:
    var space := get_world_2d().direct_space_state
    var query := PhysicsShapeQueryParameters2D.new()
    
    var circle := CircleShape2D.new()
    circle.radius = radius
    query.shape = circle
    query.transform = Transform2D(0.0, center)
    query.collision_mask = 0b0010  # Layer 2: Enemies
    
    var hits := space.intersect_shape(query)
    for hit in hits:
        var enemy: Node2D = hit.collider
        if enemy.has_method("take_damage"):
            enemy.take_damage(damage)

Ray Cast: Instant Hit Weapon

# Hitscan weapon (no projectile)
func fire_hitscan_weapon(from: Vector2, direction: Vector2, max_range: float) -> void:
    var space := get_world_2d().direct_space_state
    var query := PhysicsRayQueryParameters2D.create(from, from + direction * max_range)
    query.exclude = [self]
    query.collision_mask = 0b0010  # Enemies
    
    var result := space.intersect_ray(query)
    if result:
        var hit_enemy: Node2D = result.collider
        var hit_point: Vector2 = result.position
        
        spawn_hit_effect(hit_point)
        if hit_enemy.has_method("take_damage"):
            hit_enemy.take_damage(25)

---

Decision Tree: Collision Detection Methods

Use CaseMethodWhy
Continuous trigger zoneArea2D + signalsMemory of what's inside, signals are efficient
One-time pickup (coin)Area2D + queue_free() on enterSimple, automatic cleanup
Line-of-sight checkRayCast2DEfficient, built-in
Click-to-select unitsPhysicsPointQueryParameters2DSingle query, no permanent node
AOE spellPhysicsShapeQueryParameters2DOne-shot query, flexible shape
Instant-hit weaponPhysicsRayQueryParameters2DHitscan, no projectile physics
Platformer ground checkRayCast2D or raycast downPrecise ledge detection

---

Edge Cases

Collision During _ready()

# ❌ BAD: Raycasts don't work in _ready() (physics not initialized)
func _ready() -> void:
    if $RayCast2D.is_colliding():  # Always false!
        print("Hit something")

# ✅ GOOD: Wait for physics frame
func _ready() -> void:
    await get_tree().physics_frame
    if $RayCast2D.is_colliding():
        print("Hit something")

Area2D Not Detecting CharacterBody2D

# Problem: CharacterBody2D has collision_layer = 0 by default
# Solution: Explicitly set layer

# character.gd
func _ready() -> void:
    collision_layer = 0b0001  # Layer 1: Player

Raycast Hitting Backfaces

# Raycasts hit both front and back of collision shapes
# To raycast one-way (front only), use Area2D monitoring

---

Performance

# ✅ GOOD: Disable raycasts when not needed
func _ready() -> void:
    $OptionalRaycast.enabled = false

func check_vision() -> void:
    $OptionalRaycast.enabled = true
    $OptionalRaycast.force_raycast_update()
    var sees_player := $OptionalRaycast.is_colliding()
    $OptionalRaycast.enabled = false
    return sees_player

# ❌ BAD: Always-on raycasts for rarely-used checks
# Leave RayCast2D.enabled = true for vision checks once per second

---

Expert Techniques & Optimizations

1. Physics-Server-Batching (Low-Level Swarms)

For massive simulations (e.g., thousands of projectiles), avoid the overhead of the SceneTree by using PhysicsServer2D directly. This allows you to batch movement and collision updates in a single loop, significantly reducing CPU usage by bypassing node-based lifecycle overhead.

class_name PhysicsBatchManager extends Node
## Manages thousands of physics bodies directly via PhysicsServer2D.

var _bodies: Array[RID] = []

func create_bullet_swarm(count: int) -> void:
    for i in range(count):
        var body := PhysicsServer2D.body_create()
        PhysicsServer2D.body_set_mode(body, PhysicsServer2D.BODY_MODE_KINEMATIC)
        PhysicsServer2D.body_set_space(body, get_world_2d().space)
        _bodies.append(body)

func _physics_process(_delta: float) -> void:
    # Batch update all body transforms.
    for body in _bodies:
        var current_transform := PhysicsServer2D.body_get_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM)
        var next_transform := current_transform.translated(Vector2.RIGHT * 5.0)
        PhysicsServer2D.body_set_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM, next_transform)

2. Multi-Shape-Sync (Compound RID Bodies)

A single physics body can consist of multiple shapes (e.g., a shield and a character). To sync these shapes dynamically without creating multiple nodes, use PhysicsServer2D.body_add_shape(). This is ideal for characters with dynamic equipment or vehicles with complex, non-uniform collision volumes.

class_name CompoundBodySync extends Node2D
## Synchronizes multiple shapes within a single low-level physics body.

var _body: RID
var _shapes: Array[RID] = []

func _ready() -> void:
    _body = PhysicsServer2D.body_create()
    
    # Add multiple collision shapes to the same body RID.
    var circle := PhysicsServer2D.circle_shape_create()
    PhysicsServer2D.shape_set_data(circle, 20.0)
    PhysicsServer2D.body_add_shape(_body, circle, Transform2D.IDENTITY)
    _shapes.append(circle)
    
    var box := PhysicsServer2D.rectangle_shape_create()
    PhysicsServer2D.shape_set_data(box, Vector2(10, 50))
    PhysicsServer2D.body_add_shape(_body, box, Transform2D.IDENTITY.translated(Vector2(30, 0)))
    _shapes.append(box)

3. Collision-Visual-Debugger (Runtime Gizmos)

Professional debugging requires real-time visualization of collision data that isn't visible via standard debug options. Use CanvasItem._draw() to render contact points and normals extracted from KinematicCollision2D or the physics space state.

class_name CollisionVisualDebugger extends Node2D
## Renders collision normals and hit points for real-time physics debugging.

var _last_collision: KinematicCollision2D

func update_debug_info(collision: KinematicCollision2D) -> void:
    _last_collision = collision
    queue_redraw()

func _draw() -> void:
    if not _last_collision: return
    
    var hit_pos := to_local(_last_collision.get_position())
    var normal := _last_collision.get_normal()
    
    # Draw hit point and normal vector.
    draw_circle(hit_pos, 5.0, Color.RED)
    draw_line(hit_pos, hit_pos + normal * 30.0, Color.GREEN, 2.0)

Reference

Related

  • Master Skill: godot-master

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.