gamedev-skills/awesome-gamedev-agent-skillsチェック済み
SKILL DETAIL
physics-tuning
gamedev-skills/awesome-gamedev-agent-skills/physics-tuning
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.
インストール · 311出典を見る
Installation
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill physics-tuning
スキルファイル
SKILL.md
最終同期 · 2026/10/22
references/timestep-and-ccd.md›
# Fixed timestep, interpolation, CCD, and stability
The root cause of most physics problems is the relationship between a **fixed**
simulation step and a **variable** render frame. Get the loop right and most
"jitter" and "frame-rate dependence" complaints disappear.
## Why a fixed timestep
Numerical integration and collision resolution are sensitive to step size. With a
variable `dt`, the same scene behaves differently at 30 vs 144 FPS — springs gain
energy, fast objects tunnel more, stacks settle differently. A **constant** `dt`
makes the simulation consistent and far more reproducible.
## The accumulator loop (engine-neutral)
Engines implement this for you; understanding it explains the settings.
```python
PHYSICS_HZ = 60
FIXED_DT = 1.0 / PHYSICS_HZ
accumulator = 0.0
MAX_STEPS = 5 # cap to avoid the "spiral of death" after a hitch
def frame(render_dt):
global accumulator
accumulator += min(render_dt, FIXED_DT * MAX_STEPS) # clamp giant dt spikes
steps = 0
while accumulator >= FIXED_DT and steps < MAX_STEPS:
store_previous_state() # for interpolation
physics_step(FIXED_DT) # ALL simulation uses the constant dt
accumulator -= FIXED_DT
steps += 1
alpha = accumulator / FIXED_DT # leftover fraction toward the next tick
render(interpolate(prev_state, curr_state, alpha))
```
- Running 0, 1, or several physics steps per frame is normal and correct.
- Clamping `render_dt` (and `MAX_STEPS`) prevents a slow frame from requesting
dozens of steps, which would make the game slower still — the "spiral of death".
## Render interpolation
The render frame almost never lands exactly on a physics tick, so drawing the raw
physics transform stutters. Interpolate the *visual* between the previous and
current physics states by `alpha`:
```
visual_pos = lerp(prev_pos, curr_pos, alpha)
visual_rot = slerp(prev_rot, curr_rot, alpha) # rotations: spherical lerp
```
This adds up to one physics step of latency but removes stutter. Engines expose
it directly: Unity `Rigidbody.interpolation = Interpolate` (or `Extrapolate`),
Godot physics interpolation / `get_physics_interpolation_fraction()`. Prefer the
built-in; hand-roll only for objects the engine doesn't cover.
## Substepping
For very fast bodies or stiff joints, sub-divide each physics step into N
substeps (smaller `dt`) for accuracy, at CPU cost. Many engines expose a max
substep / solver substep count. Raise it when fast or stiff systems misbehave;
lower it to save CPU.
## Continuous collision detection (CCD)
Discrete collision tests positions once per step; a body moving more than its own
thickness per step can pass through a thin collider entirely (tunneling). CCD
sweeps the body along its motion and catches the first contact.
- Enable CCD only on bodies that need it (small + fast: bullets, balls); it costs
more than discrete checks.
- Modes vary: sweep against static geometry only, or against dynamic bodies too
(more expensive). Use the cheapest mode that fixes your case.
- Complementary fixes: cap maximum velocity so
`speed / PHYSICS_HZ < thinnest_collider`; thicken thin walls; raise PHYSICS_HZ.
## Solver iterations and stability
The constraint solver runs a fixed number of iterations per step; more iterations
= stiffer, more stable stacks and joints, at CPU cost.
- **Exploding/penetrating stacks**: raise solver (position/velocity) iterations.
- **Extreme mass ratios** (a feather resting under a boulder) are inherently
unstable — keep ratios within ~1:10 where you can.
- **Jittery resting contacts**: enable a small contact offset / allowed
penetration, and let bodies sleep.
## Sleeping
Bodies at rest should **sleep** (stop being simulated) until disturbed. This
saves CPU and stops micro-jitter on resting objects. Tune the linear/angular
sleep thresholds and time-to-sleep; ensure gameplay-critical bodies wake on the
right events (collisions, force application).
## Stability checklist
- Physics in the fixed step; rendering interpolated.
- `render_dt` and step count clamped against hitches.
- CCD on fast/small bodies; max velocity capped.
- Mass ratios modest; gravity_scale/drag tuned for feel, not mass.
- Solver iterations high enough for your stacks/joints.
- Sleeping enabled with sane thresholds.
- Collision layer/mask matrix verified in both directions.
- Tested at low and high frame rates and under CPU load.
SKILL.md›
---
name: physics-tuning
description: >
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.
---
# 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
1. **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.
2. **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`.
3. **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.
4. **Tune the body, not the scene.** Set mass for relative weight, drag for
damping, gravity scale per object, and restitution/friction via materials.
5. **Stop tunneling with CCD** on small/fast bodies; cap maximum velocity.
6. **Stabilize stacks/joints** with more solver iterations, sane mass ratios, and
sleeping for resting bodies.
7. **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
```gdscript
# 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
```gdscript
# 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
```gdscript
# 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)
```gdscript
# 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.