
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-physicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 317 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-2d-physics for development tasks
Files
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_exitedsignals instead [16]. - NEVER modify `RigidBody2D` state directly in `_process` — Use
_integrate_forces()for safe, synchronized access toPhysicsDirectBodyState2D[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
PhysicsServer2Dto 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()orShapeCast2Dfor 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()orPhysicsServer2Dinstead. - 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 falseMultipa 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 *= -1Raycast 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].colliderShape 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 Case | Method | Why |
|---|---|---|
| Continuous trigger zone | Area2D + signals | Memory of what's inside, signals are efficient |
| One-time pickup (coin) | Area2D + queue_free() on enter | Simple, automatic cleanup |
| Line-of-sight check | RayCast2D | Efficient, built-in |
| Click-to-select units | PhysicsPointQueryParameters2D | Single query, no permanent node |
| AOE spell | PhysicsShapeQueryParameters2D | One-shot query, flexible shape |
| Instant-hit weapon | PhysicsRayQueryParameters2D | Hitscan, no projectile physics |
| Platformer ground check | RayCast2D or raycast down | Precise 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: PlayerRaycast 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
# Collision Bitmask Helper
extends Node
## Managing bitmasks logically instead of hardcoding integers.
## Use constants and shift operators to keep complex faction systems readable.
enum Faction { NONE = 0, PLAYER = 1, ENEMY = 2, PROJECTILE = 3, WORLD = 4 }
func setup_actor_collision(node: CollisionObject2D, faction: Faction) -> void:
# Clear current layers and masks
node.collision_layer = 0
node.collision_mask = 0
# Set layer (What am I?)
node.set_collision_layer_value(faction, true)
# Set mask (What do I hit?)
match faction:
Faction.PLAYER:
node.set_collision_mask_value(Faction.ENEMY, true)
node.set_collision_mask_value(Faction.WORLD, true)
Faction.PROJECTILE:
node.set_collision_mask_value(Faction.ENEMY, true)
node.set_collision_mask_value(Faction.PLAYER, true)
# Collision Signal Debouncer
extends Node
## Expert Pattern: Prevents "signal spam" when multiple collision shapes
## on one body enter an Area2D simultaneously.
var _active_bodies: Dictionary = {}
func handle_body_entered(body: Node2D) -> void:
if _active_bodies.has(body):
return
_active_bodies[body] = true
print("Physics Body genuinely entered: ", body.name)
func handle_body_exited(body: Node2D) -> void:
# Small delay or frame-check to ensure it's not a momentary exit/re-entry
_active_bodies.erase(body)
# collision_layer_matrix_manager.gd
# Advanced collision layer/mask management logic
extends Node
# EXPERT NOTE: Use Bit-shifting or Enums for collision layers
# rather than magic numbers to prevent 'Collision Matrix Hell'.
enum Layer {
WORLD = 1,
PLAYER = 2,
ENEMY = 4,
PROJECTILE = 8,
HAZARD = 16
}
func setup_projectile(node: CollisionObject2D):
# Projectiles should be on PROJECTILE layer (8)
# And mask for WORLD (1) and ENEMY (4)
node.collision_layer = Layer.PROJECTILE
node.collision_mask = Layer.WORLD | Layer.ENEMY
func set_ignore_player(node: CollisionObject2D, ignore: bool):
if ignore:
node.collision_mask &= ~Layer.PLAYER # Bitwise NOT and AND to remove
else:
node.collision_mask |= Layer.PLAYER # Bitwise OR to add
# skills/2d-physics/code/collision_setup.gd
extends Node
## Collision Matrix & CCD Professional Standard
# --- 1. Recommended Layer Map ---
# Configure these in Project Settings -> Layer Names -> 2D Physics
enum CollisionLayer {
WORLD = 1, # Static environment
PLAYER = 2, # Player Character
ENEMIES = 4, # Enemy Characters
PROJECTILES = 8,# High-speed bullets (Use CCD)
HURTBOXES = 16, # Area2Ds for damage detection
INTERACTABLES = 32
}
# --- 2. Continuous Collision Detection (CCD) ---
func setup_bullet(bullet: RigidBody2D) -> void:
# REQUIRED for small, fast objects to prevent 'tunneling'
bullet.continuous_cd = RigidBody2D.CCD_MODE_CAST_RAY
# Also set collision mask to only hit what it needs to
bullet.collision_mask = CollisionLayer.WORLD | CollisionLayer.ENEMIES
# --- 3. Optimization Tip ---
# Disable 'Contact Monitor' unless you actually need the 'body_entered' signal.
# RigidBody2D 'move_and_collide' is more performant than signal-based detection
# for hundreds of active projectiles.
# continuous_collision_detection.gd
# Managing CCD for high-speed projectiles to prevent tunneling
extends RigidBody2D
# PROBLEM: High-speed bullets can skip through thin walls between frames.
# SOLUTION: Enable Continuous Collision Detection (CCD).
func _ready() -> void:
# CCD_MODE_CAST_RAY is usually enough for most projectiles
# CCD_MODE_CAST_SHAPE is the most accurate but expensive
continuous_cd = RigidBody2D.CCD_MODE_CAST_RAY
# Optimization: Only use CCD for high-speed phases
contact_monitor = true
max_contacts_reported = 1
# custom_gravity_area.gd
# Implementing custom gravity wells and directional zones
extends Area2D
# Expert: Using Area2D 'Priority' and 'Gravity' overrides.
func _ready() -> void:
# High priority ensures this gravity overrides the global world gravity
gravity_space_override = Area2D.SPACE_OVERRIDE_REPLACE
gravity_point = true
gravity_point_unit_distance = 100.0
gravity = 980.0 # Attraction force
# For directional gravity (e.g. anti-gravity lift)
# gravity_space_override = Area2D.SPACE_OVERRIDE_REPLACE
# gravity_direction = Vector2.UP
# Custom Physics Body Gravity Override
extends CharacterBody2D
## Pattern for localized gravity overrides (Space, Water, Wind).
## Bypasses global physics settings for specialized character movement.
@export var local_gravity := Vector2(0, 400)
@export var drag_factor := 0.95
func _physics_process(delta: float) -> void:
# Ignore default project settings gravity
velocity += local_gravity * delta
# Apply fluid drag/friction
velocity *= pow(drag_factor, delta * 60.0)
move_and_slide()
# skills/2d-physics/scripts/custom_physics_2d.gd
extends CharacterBody2D
## Custom Physics 2D (Expert Pattern)
## Template for platformers requiring custom gravity/dashing/states without FSM overhead.
## Demonstrates use of `move_and_slide` with external forces.
class_name CustomPhysics2D
@export var gravity: float = 980.0
@export var jump_force: float = -400.0
@export var speed: float = 300.0
var external_force: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
# 1. Apply Gravity
if not is_on_floor():
velocity.y += gravity * delta
else:
velocity.y = 0.0 # Reset accum
# 2. Input
var dir = Input.get_axis("ui_left", "ui_right")
if dir:
velocity.x = dir * speed
else:
velocity.x = move_toward(velocity.x, 0, speed * delta * 5.0) # Friction
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = jump_force
# 3. Apply External Forces (Knockback)
velocity += external_force * delta
external_force = external_force.move_toward(Vector2.ZERO, 1000.0 * delta)
# 4. Move
move_and_slide()
func apply_knockback(force: Vector2) -> void:
# Set immediate velocity? Or Add to external force?
# Instant velocity change is usually snappier for hits
velocity += force
## EXPERT USAGE:
## Extend for PlayerController.
# skills/2d-physics/code/custom_physics.gd
extends RigidBody2D
## Deterministic Solver & Custom Gravity Pattern
## Demonstrates _integrate_forces for absolute control over physics state.
@export var custom_gravity_scale := 1.0
@export var local_gravity_center: Node2D
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
# --- 1. Custom Movement Math ---
# Override velocity directly while remaining in the physics solver
# This prevents 'jitter' compared to manual position changes
var current_vel := state.linear_velocity
# Apply custom dampening or acceleration
# current_vel.x = move_toward(current_vel.x, target_speed, accel * state.step)
# --- 2. Localized Gravity Fields ---
if local_gravity_center:
var dir := global_position.direction_to(local_gravity_center.global_position)
var force := dir * 9.8 * custom_gravity_scale
state.apply_central_force(force)
# --- 3. Deterministic Stop ---
if should_stop_instantly():
state.linear_velocity = Vector2.ZERO
state.angular_velocity = 0
func should_stop_instantly() -> bool:
return false
## EXPERT NOTE:
## Always use 'state.step' for delta-consistency in _integrate_forces.
## This function is called during the physics synchronization phase,
## making it the correct place for custom physics engines or character controllers
## that require RigidBody interactions without the 'floatiness' of default physics.
# Physics Interpolation and Jitter Controller
extends Node
## Godot 4 standard physics run at 60Hz, while monitors run at 144Hz+.
## This script ensures smooth visuals by enabling interpolation and
## managing physics/render process synchronization.
func _ready() -> void:
# Architecture Tip: Enable 'Physics Interpolation' in Project Settings.
# This script ensures that crucial camera and player nodes are optimized
# for that interpolation.
if Engine.is_editor_hint(): return
# Lock FPS to refresh rate to prevent visual 'beat' patterns against physics
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
# Low latency input mode for competitive physics feel
Input.set_use_accumulated_input(false)
# move_and_collide_precision.gd
# Using move_and_collide for custom bounce and friction logic
extends CharacterBody2D
# EXPERT NOTE: move_and_slide is for PLATFORMERS.
# Use move_and_collide for BILLIARDS, PINBALL, or high-speed projectiles
# where you need the exact KinematicCollision2D object.
@export var bounce_factor: float = 0.8
func _physics_process(delta: float) -> void:
var collision = move_and_collide(velocity * delta)
if collision:
# Bounce velocity off the normal
velocity = velocity.bounce(collision.get_normal()) * bounce_factor
# Slide slightly along the surface for 'greasy' feel
velocity += collision.get_remainder().slide(collision.get_normal())
# performance_batch_mover.gd
# Moving 100+ StaticBody2Ds without performance tanking
extends Node2D
# PROBLEM: Moving many StaticBody2Ds per frame is expensive.
# SOLUTION: Disable collision while moving or use AnimatableBody2D
# which is optimized for code-driven movement.
@onready var platforms: Array = get_children().filter(func(c): return c is AnimatableBody2D)
func _physics_process(delta: float) -> void:
for p in platforms:
# AnimatableBody2D correctly calculates velocity for riders
p.position.x += 100 * delta * sin(Time.get_ticks_msec() / 1000.0)
# Character Rotation via DirectSpaceState2D
extends Node2D
## Using an Area2D for simple ground detection? Overkill.
## Use a PhysicsDirectSpaceState2D query for a more lightweight, code-driven approach.
func is_grounded() -> bool:
var space_state = get_world_2d().direct_space_state
# Cast a small segment below the player
var query = PhysicsRayQueryParameters2D.create(global_position, global_position + Vector2.DOWN * 5.0)
query.exclude = [get_parent().get_rid()] # Exclude self
var result = space_state.intersect_ray(query)
return !result.is_empty()
# physics_direct_space_query.gd
# Low-level space state queries for complex AI vision [Raycasting]
extends Node2D
# EXPERT NOTE: Querying the space state directly is the fastest way
# to perform raycasts in bulk without creating nodes.
func check_line_of_sight(from: Vector2, to: Vector2) -> bool:
var space_state = get_world_2d().direct_space_state
# Configure the query
var query = PhysicsRayQueryParameters2D.create(from, to)
query.collision_mask = 1 # World layer
query.exclude = [get_parent().get_rid()] # Exclude self
var result = space_state.intersect_ray(query)
# result is empty if nothing was hit
return result.is_empty()
# physics_interpolation_smoothing.gd
# Manual jitter reduction for PhysicsBody2D [12, 13]
extends CharacterBody2D
# PROBLEM: Physics runs at 60Hz, Monitors run at 144Hz+. This causes
# 'micro-stutter' as the rendering frame is between physics frames.
# SOLUTION: In Godot 4.3+, use 'Physics Interpolation'. For older versions,
# or custom needs, use this 'smoothing node' pattern.
@onready var visual_node: Node2D = $Sprite2D
func _process(_delta: float) -> void:
# Calculate how much time has passed since the last physics frame
var weight = Engine.get_physics_interpolation_fraction()
# Interpolate the sprite's position between previous and current physics state
# visual_node.global_position = _prev_pos.lerp(global_position, weight)
pass # Logic is cleaner if using built-in Godot 4 interpolation settings
# skills/2d-physics/code/physics_queries.gd
extends Node2D
## High-Performance Physics Queries Pattern
## Demonstrates using PhysicsDirectSpaceState2D for queries outside the main tree.
func perform_expert_raycast(from: Vector2, to: Vector2, exclude: Array[RID] = []) -> Dictionary:
# 1. Access the direct space state (Thread-safe only in _physics_process)
var space_state := get_world_2d().direct_space_state
# 2. Configure the query
var query := PhysicsRayQueryParameters2D.create(from, to)
query.exclude = exclude
query.collision_mask = 0b0001 # Layer 1 (World)
# Optional: Enable hit_from_inside if needed
# query.hit_from_inside = true
# 3. Execute
var result := space_state.intersect_ray(query)
if result:
# print("Hit: ", result.collider)
return result
return {}
func perform_shapecast(origin: Vector2, shape: Shape2D, motion: Vector2) -> Array[Dictionary]:
# ShapeCast2D is great as a node, but for manual queries:
var space_state := get_world_2d().direct_space_state
var query := PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = Transform2D(0, origin)
query.motion = motion
# intersect_shape is for overlap detection
# cast_motion is for finding how far a shape can move before hitting something
var results := space_state.intersect_shape(query)
return results
## WHY USE THIS?
## Bypassing the Node2D tree for physics queries is significantly faster
## when performing hundreds of checks (e.g., AI vision, projectile prediction).
# skills/2d-physics/scripts/physics_query_cache.gd
extends Node2D
## Physics Query Cache (Expert Pattern)
## Caches expensive Area/Shape queries to avoid running them every frame.
## Useful for "Radar" scans or dense AI environments.
class_name PhysicsQueryCache
@export var scan_radius: float = 300.0
@export var scan_interval: float = 0.2
@export_flags_2d_physics var collision_mask: int = 1
var _cached_results: Array[Dictionary] = []
var _timer: float = 0.0
signal results_updated(results: Array[Dictionary])
func _physics_process(delta: float) -> void:
_timer -= delta
if _timer <= 0.0:
_timer = scan_interval
_perform_scan()
func _perform_scan() -> void:
var space = get_world_2d().direct_space_state
var query = PhysicsShapeQueryParameters2D.new()
var shape = CircleShape2D.new()
shape.radius = scan_radius
query.shape = shape
query.transform = global_transform
query.collision_mask = collision_mask
_cached_results = space.intersect_shape(query)
results_updated.emit(_cached_results)
func get_results() -> Array[Dictionary]:
return _cached_results
func get_closest() -> Node2D:
var closest: Node2D = null
var min_dist = INF
for res in _cached_results:
var collider = res.collider as Node2D
if collider and collider != self:
var d = global_position.distance_squared_to(collider.global_position)
if d < min_dist:
min_dist = d
closest = collider
return closest
## EXPERT USAGE:
## Attach to player/AI. Connect to 'results_updated' or poll 'get_results()'.
## Vastly cheaper than calling intersect_shape every frame.
# physics_server_direct_body.gd
# High-performance physics using PhysicsServer2D directly [17]
extends Node2D
# EXPERT NOTE: For 1000s of objects (bullets, debris), PhysicsServer2D
# is significantly faster than using RigidBody2D nodes as it
# bypasses the scene tree overhead.
var bodies: Array[RID] = []
var shape: RID
func _ready() -> void:
shape = PhysicsServer2D.circle_shape_create()
PhysicsServer2D.shape_set_data(shape, 10.0) # Radius 10
for i in range(100):
var body = PhysicsServer2D.body_create()
PhysicsServer2D.body_set_mode(body, PhysicsServer2D.BODY_MODE_RIGID)
PhysicsServer2D.body_add_shape(body, shape)
PhysicsServer2D.body_set_space(body, get_world_2d().space)
PhysicsServer2D.body_set_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM, Transform2D(0, Vector2(randf()*500, randf()*500)))
bodies.append(body)
func _exit_tree() -> void:
# ALWAYS clean up RIDs manually to prevent memory leaks
for body in bodies:
PhysicsServer2D.free_rid(body)
PhysicsServer2D.free_rid(shape)
# PhysicsServer2D High-Performance Swarm
extends Node2D
## For thousands of objects, bypassing the SceneTree is mandatory.
## This script manages raw physics bodies via the PhysicsServer2D.
var bodies = []
var shape
func _ready() -> void:
shape = PhysicsServer2D.circle_shape_create()
PhysicsServer2D.shape_set_data(shape, 10.0) # Radius 10
for i in range(1000):
var body = PhysicsServer2D.body_create()
PhysicsServer2D.body_set_space(body, get_world_2d().space)
PhysicsServer2D.body_add_shape(body, shape)
# Set initial position
var transform = Transform2D(0, Vector2(randf() * 1000, randf() * 1000))
PhysicsServer2D.body_set_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM, transform)
bodies.append(body)
func _exit_tree() -> void:
# Manual memory management is required for Server objects
for body in bodies:
PhysicsServer2D.free_rid(body)
PhysicsServer2D.free_rid(shape)
# raycast_hit_prediction.gd
# Using RayCast2D for hitscan weapons and prediction logic
extends RayCast2D
# EXPERT NOTE: Don't rely solely on '_physics_process' update.
# Force updates for immediate logic resolution (like rapid fire).
func fire_shot() -> void:
# Move ray to weapon muzzle
force_raycast_update()
if is_colliding():
var target = get_collider()
var point = get_collision_point()
var normal = get_collision_normal()
_apply_impact_vfx(point, normal)
if target.has_method("take_damage"):
target.take_damage(25)
func _apply_impact_vfx(_point: Vector2, _normal: Vector2):
pass
# Optimized RayCast2D AI Detection Stack
extends Node2D
## Expert pattern: Using a shared RayCast2D to perform multiple
## directional checks in one frame to optimize NPC vision swarms.
@onready var vision_ray: RayCast2D = $VisionRay
func scan_for_target(target: Node2D, angles: Array[float], max_range: float) -> bool:
vision_ray.enabled = true
vision_ray.target_position = Vector2.RIGHT * max_range
for angle in angles:
vision_ray.rotation = angle
# MANDATORY: force_raycast_update() must be called after rotation
# if checking multiple directions in the same physics frame.
vision_ray.force_raycast_update()
if vision_ray.is_colliding() and vision_ray.get_collider() == target:
vision_ray.enabled = false
return true
vision_ray.enabled = false
return false
# Safe RigidBody2D State Modification
extends RigidBody2D
## NEVER modify position or linear_velocity in _process or _physics_process.
## Use _integrate_forces for thread-safe access to the PhysicsDirectBodyState.
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
# Example: Teleporting safely mid-collision
if Input.is_action_just_pressed("teleport"):
var new_transform = Transform2D(state.transform.get_rotation(), Vector2.ZERO)
state.transform = new_transform
# Example: Applying a custom impulse that overrides existing velocity
if Input.is_action_just_pressed("jump"):
state.linear_velocity.y = -500
# shapecast_aoe_detection.gd
# Using ShapeCast2D for robust area-of-effect detection [Ray vs Shape]
extends ShapeCast2D
# EXPERT NOTE: RayCasts are pins. ShapeCasts are volumes.
# Use ShapeCast2D for ground detection or melee swings to
# prevent "skinny" collisions from missing targets.
func check_grounded() -> bool:
# A CircleShape2D cast downwards is more stable for slopes
# than a single RayCast2D.
force_shapecast_update()
return is_colliding()
func get_all_targets() -> Array:
var hits = []
for i in range(get_collision_count()):
hits.append(get_collider(i))
return hits
# ShapeCast2D AOE Detection Patterns
extends ShapeCast2D
## Using ShapeCast2D for robust AOE detection.
## Unlike Area2D, ShapeCast2D detects collisions at the EXACT moment of query.
func get_impact_entities(faction_mask: int) -> Array[Node2D]:
collision_mask = faction_mask
force_shapecast_update()
var entities: Array[Node2D] = []
for i in range(get_collision_count()):
var col = get_collider(i)
if col is Node2D:
entities.append(col)
return entities
# Fixed Timestep Sub-stepping Logic
extends CharacterBody2D
## For extremely fast-moving objects (bullets), standard collision fails.
## Sub-stepping manually breaks the frame into smaller physics steps.
@export var velocity_per_second: Vector2 = Vector2(5000, 0)
@export var sub_steps: int = 4
func _physics_process(delta: float) -> void:
var step_delta = delta / sub_steps
for i in range(sub_steps):
var collision = move_and_collide(velocity_per_second * step_delta)
if collision:
_handle_collision(collision)
break
func _handle_collision(collision: KinematicCollision2D) -> void:
print("Impact at: ", collision.get_position())
queue_free()