Physics tuning
Most "bad physics" is not a bug in the engine — it's a mismatch between the
fixed-timestep simulation and the variable-rate render loop, or untuned
mass/drag/CCD/layer settings. This skill covers the engine-neutral knobs that
make physics stable and responsive; pair it with godot-physics or
unity-physics for the concrete APIs.
When to use
- Use when motion jitters, objects pass through walls (tunneling), stacks
explode, or movement feels floaty/sticky/laggy.
- Use to decide what goes in the fixed (physics) step vs the render frame, and
how to interpolate between them.
- Use to tune gravity, mass, drag, restitution, solver iterations, sleeping, and
collision layers/masks.
When not to use: for an engine's exact physics nodes/components and
collision callbacks, use godot-physics or unity-physics. For movement
decisions (when to jump, AI steering) use input-systems and game-ai. For
platformer jump-feel specifics like coyote time/jump buffering, that's input/
controller territory — see input-systems and the platformer genre.
Core workflow
- Run physics on a fixed timestep. Simulate at a constant rate (e.g. 50–60
Hz). A fixed
dt makes the simulation deterministic-ish and stable; a
variable dt makes integration and collisions inconsistent.
- Put physics work in the physics callback, not the render frame. Apply
forces/velocities and read collisions in the fixed step (
FixedUpdate /
_physics_process), using that step's dt.
- Interpolate rendering between physics ticks. The render frame rate ≠ the
physics rate, so smoothly interpolate transforms toward the latest physics
state, or enable the engine's Rigidbody interpolation, to remove visible
stutter.
- Tune the body, not the scene. Set mass for relative weight, drag for
damping, gravity scale per object, and restitution/friction via materials.
- Stop tunneling with CCD on small/fast bodies; cap maximum velocity.
- Stabilize stacks/joints with more solver iterations, sane mass ratios, and
sleeping for resting bodies.
- Verify by feel and stress test. Play at low and high frame rates; throw
fast objects at thin walls; stack and shove bodies. Report what you observed.
Patterns
1. Fixed timestep for simulation, render interpolation for smoothness
# Physics callback: runs at the FIXED rate. Use its dt for all integration.
func _physics_process(dt): # Unity: void FixedUpdate()
velocity += gravity * dt # integrate with the FIXED dt
move_and_slide() # engine resolves collisions this step
_prev_pos = _curr_pos; _curr_pos = global_position # record for interpolation
# Render frame: runs as fast as the display. Interpolate between physics states.
func _process(_frame_dt): # Unity: void Update()
var alpha = Engine.get_physics_interpolation_fraction() # 0..1 within the tick
visual.global_position = _prev_pos.lerp(_curr_pos, alpha)
# RIGHT: integrate in the fixed step, render via interpolation.
# WRONG: applying forces in _process/Update with frame dt — speed and collisions
# then depend on frame rate and jitter under load.
Most engines offer this for you (Godot physics_interpolation/Rigidbody
interpolate; Unity Rigidbody.interpolation = Interpolate). Prefer the built-in
before hand-rolling.
2. Stop tunneling: CCD + a speed cap
# Fast, small bodies skip past thin colliders between ticks. Two fixes:
body.continuous_cd = true # RigidBody3D bool (RigidBody2D: CCD_MODE_* enum). Unity: rb.collisionDetectionMode = Continuous
# Cap velocity so a single step can't move more than ~one collider thickness.
const MAX_SPEED := 40.0
if velocity.length() > MAX_SPEED:
velocity = velocity.normalized() * MAX_SPEED
# Rule of thumb: max_distance_per_step (= speed / physics_hz) should be < the
# thinnest wall. Raise physics_hz or enable CCD when that fails.
3. Body tuning: mass, drag, gravity scale, material
# Mass is RELATIVE weight in collisions; it does NOT change fall speed (gravity
# accelerates all masses equally). Use drag and gravity_scale to shape feel.
body.mass = 2.0 # heavier pushes lighter in collisions
body.linear_damp = 0.5 # air drag: higher = stops sooner (Unity: drag)
body.gravity_scale = 1.5 # per-object gravity multiplier (snappier fall)
# Bounce/slide come from the physics material, not code:
material.bounce = 0.2 # restitution 0..1 (Unity: bounciness)
material.friction = 0.8 # surface grip
4. Collision layers and masks (who collides with whom)
# A body is ON its layer(s) and SCANS the layers in its mask. Both directions of a
# pair must be configured for them to interact.
player.collision_layer = LAYER_PLAYER
player.collision_mask = LAYER_WORLD | LAYER_ENEMY # player detects world+enemies
pickup.collision_layer = LAYER_PICKUP
pickup.collision_mask = LAYER_PLAYER # pickup only reacts to player
# Unity equivalent: assign GameObject layers and edit the Physics collision matrix
# (or Physics.IgnoreLayerCollision). Keep a named layer constant table, not magic numbers.
Pitfalls
- Applying forces/movement in the render frame (
Update/_process) makes
behavior frame-rate dependent — faster PCs run faster, and collisions get
flaky. Do simulation in the fixed step.
- Visible jitter even with a fixed step usually means no render
interpolation: the physics rate and display rate beat against each other.
Enable interpolation.
- Tunneling through thin walls: discrete collision misses fast movers. Enable
CCD, cap speed, thicken walls, or raise the physics rate.
- Expecting heavier objects to fall faster. Gravity is acceleration; mass
affects collision response, not fall speed. Use
gravity_scale/drag for feel.
- Exploding stacks / jittery joints: mass ratios too extreme, or too few
solver iterations. Keep mass ratios modest and raise iteration counts.
- Bodies that never rest burn CPU and twitch. Enable sleeping and a sensible
sleep threshold for resting objects.
- One-directional layer setup: A's mask includes B but B's mask excludes A.
Detection/collision can need both sides; verify the full matrix.
- Huge
dt spikes (load hitches, breakpoints) blow up integration. Clamp the
max physics step / substep count so a stall doesn't launch everything.
References
references/timestep-and-ccd.md — the fixed-timestep accumulator loop,
interpolation math, substepping, CCD modes, solver/iteration tuning, sleeping,
and a stability checklist.
Related skills
godot-physics, unity-physics — concrete bodies, colliders, and callbacks.
input-systems — responsive controls, jump buffering, coyote time.
game-ai — agent movement that must agree with the physics step.
platformer, fps-shooter — genres whose feel depends on this tuning.
1---2name: physics-tuning3description: Tune game physics for stable, good-feeling motion — fixed vs variable timestep, render interpolation, mass/gravity/drag, continuous collision detection (CCD) to stop tunneling, fixing jitter, and collision layers/masks. Engine-neutral. Use when the user mentions physics feel, jitter, tunneling, fixed timestep, FixedUpdate, CCD, bouncing/unstable physics, or collision layers.4---5
6# Physics tuning
7
8Most "bad physics" is not a bug in the engine — it's a mismatch between the
9**fixed-timestep simulation** and the **variable-rate render loop**, or untuned
10mass/drag/CCD/layer settings. This skill covers the engine-neutral knobs that
11make physics stable and responsive; pair it with `godot-physics` or
12`unity-physics` for the concrete APIs.
13
14## When to use
15
16- Use when motion jitters, objects pass through walls (tunneling), stacks
17 explode, or movement feels floaty/sticky/laggy.
18- Use to decide what goes in the fixed (physics) step vs the render frame, and
19 how to interpolate between them.
20- Use to tune gravity, mass, drag, restitution, solver iterations, sleeping, and
21 collision layers/masks.
22
23**When *not* to use:** for an engine's exact physics nodes/components and
24collision callbacks, use `godot-physics` or `unity-physics`. For *movement
25decisions* (when to jump, AI steering) use `input-systems` and `game-ai`. For
26platformer jump-feel specifics like coyote time/jump buffering, that's input/
27controller territory — see `input-systems` and the `platformer` genre.
28
29## Core workflow
30
311. **Run physics on a fixed timestep.** Simulate at a constant rate (e.g. 50–60
32 Hz). A fixed `dt` makes the simulation deterministic-ish and stable; a
33 variable `dt` makes integration and collisions inconsistent.
342. **Put physics work in the physics callback**, not the render frame. Apply
35 forces/velocities and read collisions in the fixed step (`FixedUpdate` /
36 `_physics_process`), using that step's `dt`.
373. **Interpolate rendering between physics ticks.** The render frame rate ≠ the
38 physics rate, so smoothly interpolate transforms toward the latest physics
39 state, or enable the engine's Rigidbody interpolation, to remove visible
40 stutter.
414. **Tune the body, not the scene.** Set mass for relative weight, drag for
42 damping, gravity scale per object, and restitution/friction via materials.
435. **Stop tunneling with CCD** on small/fast bodies; cap maximum velocity.
446. **Stabilize stacks/joints** with more solver iterations, sane mass ratios, and
45 sleeping for resting bodies.
467. **Verify by feel and stress test.** Play at low and high frame rates; throw
47 fast objects at thin walls; stack and shove bodies. Report what you observed.
48
49## Patterns
50
51### 1. Fixed timestep for simulation, render interpolation for smoothness
52
53```gdscript
54# Physics callback: runs at the FIXED rate. Use its dt for all integration.
55func _physics_process(dt): # Unity: void FixedUpdate()
56 velocity += gravity * dt # integrate with the FIXED dt
57 move_and_slide() # engine resolves collisions this step
58 _prev_pos = _curr_pos; _curr_pos = global_position # record for interpolation
59
60# Render frame: runs as fast as the display. Interpolate between physics states.
61func _process(_frame_dt): # Unity: void Update()
62 var alpha = Engine.get_physics_interpolation_fraction() # 0..1 within the tick
63 visual.global_position = _prev_pos.lerp(_curr_pos, alpha)
64# RIGHT: integrate in the fixed step, render via interpolation.
65# WRONG: applying forces in _process/Update with frame dt — speed and collisions
66# then depend on frame rate and jitter under load.
67```
68
69Most engines offer this for you (Godot `physics_interpolation`/Rigidbody
70interpolate; Unity `Rigidbody.interpolation = Interpolate`). Prefer the built-in
71before hand-rolling.
72
73### 2. Stop tunneling: CCD + a speed cap
74
75```gdscript
76# Fast, small bodies skip past thin colliders between ticks. Two fixes:
77body.continuous_cd = true # RigidBody3D bool (RigidBody2D: CCD_MODE_* enum). Unity: rb.collisionDetectionMode = Continuous
78# Cap velocity so a single step can't move more than ~one collider thickness.
79const MAX_SPEED := 40.0
80if velocity.length() > MAX_SPEED:
81 velocity = velocity.normalized() * MAX_SPEED
82# Rule of thumb: max_distance_per_step (= speed / physics_hz) should be < the
83# thinnest wall. Raise physics_hz or enable CCD when that fails.
84```
85
86### 3. Body tuning: mass, drag, gravity scale, material
87
88```gdscript
89# Mass is RELATIVE weight in collisions; it does NOT change fall speed (gravity
90# accelerates all masses equally). Use drag and gravity_scale to shape feel.
91body.mass = 2.0 # heavier pushes lighter in collisions
92body.linear_damp = 0.5 # air drag: higher = stops sooner (Unity: drag)
93body.gravity_scale = 1.5 # per-object gravity multiplier (snappier fall)
94# Bounce/slide come from the physics material, not code:
95material.bounce = 0.2 # restitution 0..1 (Unity: bounciness)
96material.friction = 0.8 # surface grip
97```
98
99### 4. Collision layers and masks (who collides with whom)
100
101```gdscript
102# A body is ON its layer(s) and SCANS the layers in its mask. Both directions of a
103# pair must be configured for them to interact.
104player.collision_layer = LAYER_PLAYER
105player.collision_mask = LAYER_WORLD | LAYER_ENEMY # player detects world+enemies
106pickup.collision_layer = LAYER_PICKUP
107pickup.collision_mask = LAYER_PLAYER # pickup only reacts to player
108# Unity equivalent: assign GameObject layers and edit the Physics collision matrix
109# (or Physics.IgnoreLayerCollision). Keep a named layer constant table, not magic numbers.
110```
111
112## Pitfalls
113
114- **Applying forces/movement in the render frame** (`Update`/`_process`) makes
115 behavior frame-rate dependent — faster PCs run faster, and collisions get
116 flaky. Do simulation in the fixed step.
117- **Visible jitter** even with a fixed step usually means no render
118 interpolation: the physics rate and display rate beat against each other.
119 Enable interpolation.
120- **Tunneling** through thin walls: discrete collision misses fast movers. Enable
121 CCD, cap speed, thicken walls, or raise the physics rate.
122- **Expecting heavier objects to fall faster.** Gravity is acceleration; mass
123 affects collision response, not fall speed. Use `gravity_scale`/drag for feel.
124- **Exploding stacks / jittery joints**: mass ratios too extreme, or too few
125 solver iterations. Keep mass ratios modest and raise iteration counts.
126- **Bodies that never rest** burn CPU and twitch. Enable sleeping and a sensible
127 sleep threshold for resting objects.
128- **One-directional layer setup**: A's mask includes B but B's mask excludes A.
129 Detection/collision can need both sides; verify the full matrix.
130- **Huge `dt` spikes** (load hitches, breakpoints) blow up integration. Clamp the
131 max physics step / substep count so a stall doesn't launch everything.
132
133## References
134
135- `references/timestep-and-ccd.md` — the fixed-timestep accumulator loop,
136 interpolation math, substepping, CCD modes, solver/iteration tuning, sleeping,
137 and a stability checklist.
138
139## Related skills
140
141- `godot-physics`, `unity-physics` — concrete bodies, colliders, and callbacks.
142- `input-systems` — responsive controls, jump buffering, coyote time.
143- `game-ai` — agent movement that must agree with the physics step.
144- `platformer`, `fps-shooter` — genres whose feel depends on this tuning.