Skip to content
Back to skills

Physics Patterns

ASecurity

Game physics code patterns: collision layer configuration, a full CharacterBody3D controller, RigidBody3D interaction, raycasting, Area3D triggers, and a hinge door joint. Use when writing movement, collision, or physics interaction code.

  • 65 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 1, 2026
developmentgonode

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add HermeticOrmus/claude-code-game-development --skill physics-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Physics Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Physics Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hermeticormus-physics-patterns/badge)](https://www.skillsdirectory.com/skills/hermeticormus-physics-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: physics-patterns
description: "Game physics code patterns: collision layer configuration, a full CharacterBody3D controller, RigidBody3D interaction, raycasting, Area3D triggers, and a hinge door joint. Use when writing movement, collision, or physics interaction code."
---

# Physics Patterns

## Collision Layer Configuration

```gdscript
# Define collision layers as constants - never magic numbers
class_name PhysicsLayers
# Layer values: collision_layer and collision_mask use bitmasks
const WORLD: int        = 1 << 0  # Layer 1: Static geometry (terrain, walls)
const PLAYER: int       = 1 << 1  # Layer 2: Player character
const ENEMY: int        = 1 << 2  # Layer 3: Enemy characters
const PROJECTILE: int   = 1 << 3  # Layer 4: Bullets, arrows, spells
const PICKUP: int       = 1 << 4  # Layer 5: Collectible items
const TRIGGER: int      = 1 << 5  # Layer 6: Area3D triggers (checkpoints)
const VEHICLE: int      = 1 << 6  # Layer 7: Driveable objects

# Layer assignment table:
# Body Type          | collision_layer    | collision_mask
# Player             | PLAYER             | WORLD | ENEMY | PICKUP | TRIGGER
# Enemy              | ENEMY              | WORLD | PLAYER | PROJECTILE
# Projectile         | PROJECTILE         | WORLD | ENEMY (not PLAYER if friendly fire off)
# Pickup (Area3D)    | PICKUP             | PLAYER (only player can collect)
# Terrain (Static)   | WORLD              | 0 (statics don't need to detect anything)

# Usage:
# player_body.collision_layer = PhysicsLayers.PLAYER
# player_body.collision_mask = PhysicsLayers.WORLD | PhysicsLayers.ENEMY | PhysicsLayers.PICKUP
```

## CharacterBody3D Controller (Full Featured)

```gdscript
class_name PlatformerCharacter extends CharacterBody3D
const SPEED: float = 6.0
const JUMP_VELOCITY: float = 6.0
const GRAVITY: float = 20.0
const FALL_GRAVITY_MULTIPLIER: float = 2.0  # Fast fall on descent
const MAX_FALL_SPEED: float = -30.0

@export var floor_snap_length: float = 0.3  # Prevents bouncing on slopes

func _ready() -> void:
    # Floor snap prevents character bouncing when descending slopes
    motion_mode = CharacterBody3D.MOTION_MODE_GROUNDED
    floor_snap_length = 0.3
    floor_stop_on_slope = true
    floor_max_angle = deg_to_rad(46)  # Max walkable slope

func _physics_process(delta: float) -> void:
    _apply_gravity(delta)
    _handle_movement()
    move_and_slide()
    # move_and_slide() updates velocity for you after collisions

func _apply_gravity(delta: float) -> void:
    if is_on_floor():
        # Keep small downward velocity for slope snapping
        velocity.y = -0.1
    else:
        var grav := GRAVITY * delta
        if velocity.y < 0:
            grav *= FALL_GRAVITY_MULTIPLIER  # Faster fall = better game feel
        velocity.y = maxf(velocity.y - grav, MAX_FALL_SPEED)

func _handle_movement() -> void:
    var input := Input.get_axis(&"move_left", &"move_right")
    velocity.x = input * SPEED

func jump() -> void:
    if is_on_floor():
        velocity.y = JUMP_VELOCITY
```

## RigidBody3D Physics Interaction

```gdscript
# Physics-based destructible prop
class_name DestructibleProp extends RigidBody3D
@export var health: float = 30.0
@export var break_force_threshold: float = 10.0  # Newtons to break
@export var debris_scene: PackedScene
@export var break_sound: AudioStream

signal destroyed(position: Vector3)

func _ready() -> void:
    contact_monitor = true
    max_contacts_reported = 4

func _on_body_shape_entered(body_rid: RID, body: Node, body_shape_index: int, local_shape_index: int) -> void:
    var contact_velocity := linear_velocity.length()
    var impact_force := contact_velocity * mass
    if impact_force > break_force_threshold:
        _break(global_position)

func take_damage(amount: float) -> void:
    health -= amount
    if health <= 0.0:
        _break(global_position)

func _break(position: Vector3) -> void:
    destroyed.emit(position)
    if debris_scene:
        for i in 3:
            var debris := debris_scene.instantiate() as RigidBody3D
            get_parent().add_child(debris)
            debris.global_position = position + Vector3(randf_range(-0.3, 0.3), 0.2, randf_range(-0.3, 0.3))
            debris.apply_central_impulse(Vector3(randf_range(-2, 2), randf_range(1, 3), randf_range(-2, 2)))
    queue_free()
```

## Raycasting Patterns

```gdscript
# Multiple raycast patterns for different use cases
class_name PhysicsQueries extends Node

# Single raycast - weapon hit detection
func raycast_hit(from: Vector3, to: Vector3, mask: int = 0b0111) -> Dictionary:
    var space := get_world_3d().direct_space_state
    var params := PhysicsRayQueryParameters3D.create(from, to, mask)
    params.hit_back_faces = false
    return space.intersect_ray(params)

# Sphere cast - area of effect check
func sphere_overlap(center: Vector3, radius: float, mask: int) -> Array[Dictionary]:
    var space := get_world_3d().direct_space_state
    var shape := SphereShape3D.new()
    shape.radius = radius
    var params := PhysicsShapeQueryParameters3D.new()
    params.shape = shape
    params.transform = Transform3D(Basis(), center)
    params.collision_mask = mask
    return space.intersect_shape(params)

# Ground check raycast
func is_grounded(body_position: Vector3, check_distance: float = 0.1) -> bool:
    var result := raycast_hit(
        body_position + Vector3.UP * 0.05,
        body_position + Vector3.DOWN * check_distance,
        PhysicsLayers.WORLD
    )
    return not result.is_empty()

# Line of sight check
func has_line_of_sight(from: Vector3, to: Vector3) -> bool:
    var result := raycast_hit(from, to, PhysicsLayers.WORLD)
    return result.is_empty()  # No hit = clear line of sight
```

## Area3D Trigger Setup

```gdscript
# Flexible trigger volume for game events
class_name TriggerVolume extends Area3D
signal player_entered
signal player_exited

@export var trigger_once: bool = false
var _triggered: bool = false

func _ready() -> void:
    collision_layer = PhysicsLayers.TRIGGER
    collision_mask = PhysicsLayers.PLAYER
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)

func _on_body_entered(body: Node3D) -> void:
    if _triggered and trigger_once:
        return
    if body.is_in_group(&"player"):
        _triggered = true
        player_entered.emit()
        if trigger_once:
            set_deferred(&"monitoring", false)

func _on_body_exited(body: Node3D) -> void:
    if body.is_in_group(&"player"):
        player_exited.emit()
```

## Physics Joint (Hinge Door)

```gdscript
# Hinged door using HingeJoint3D
class_name HingeDoor extends Node3D
@onready var door_body: RigidBody3D = $DoorBody
@onready var hinge: HingeJoint3D = $HingeJoint3D

@export var max_angle_degrees: float = 90.0
@export var damping: float = 0.5

func _ready() -> void:
    # HingeJoint3D connects door_body to a StaticBody3D frame
    hinge.set_flag(HingeJoint3D.FLAG_USE_LIMIT, true)
    hinge.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, deg_to_rad(-max_angle_degrees))
    hinge.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, deg_to_rad(max_angle_degrees))
    hinge.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, 0.0)
    hinge.set_param(HingeJoint3D.PARAM_MOTOR_MAX_IMPULSE, damping)

func open(direction: float = 1.0) -> void:
    door_body.apply_torque(door_body.global_transform.basis.y * direction * 100.0)
```

## Anti-Patterns

- **Mesh collider on moving objects**: Trimesh (ConcavePolygonShape3D) is expensive and doesn't work correctly on moving bodies. Use compound of primitives for characters and props.
- **RigidBody3D for player character**: Physics engine fights player input; momentum makes control feel wrong. Use CharacterBody3D.
- **Raycast every frame for ground check**: Expensive. Use `is_on_floor()` on CharacterBody3D; it's provided for free by `move_and_slide()`.
- **Area3D monitoring with collision_mask = 0**: Area3D with mask 0 monitors nothing. Set mask to the layers you want to detect.
- **High `max_contacts_reported` on all RigidBodies**: Contact generation is expensive. Set `contact_monitor = false` on bodies that don't need contact events.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…