@ericrisco/rsc 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/manifest.json +241 -2
- package/package.json +1 -1
- package/scripts/lib/domains.js +1 -0
- package/scripts/lib/recommend.js +4 -0
- package/skills/game-design/SKILL.md +170 -0
- package/skills/game-design/evals/README.md +13 -0
- package/skills/game-design/evals/cases.yaml +47 -0
- package/skills/game-design/references/mechanics-and-loops.md +78 -0
- package/skills/game-design/references/playtesting-and-scope.md +58 -0
- package/skills/game-design/references/progression-and-economy.md +70 -0
- package/skills/game-storytelling/SKILL.md +159 -0
- package/skills/game-storytelling/evals/README.md +28 -0
- package/skills/game-storytelling/evals/cases.yaml +51 -0
- package/skills/game-storytelling/references/branching-and-state.md +134 -0
- package/skills/game-storytelling/references/dialogue-tools.md +117 -0
- package/skills/game-storytelling/references/environmental-storytelling.md +71 -0
- package/skills/gamedev-multiplayer/SKILL.md +250 -0
- package/skills/gamedev-multiplayer/evals/README.md +12 -0
- package/skills/gamedev-multiplayer/evals/cases.yaml +44 -0
- package/skills/gamedev-multiplayer/references/godot-high-level-multiplayer.md +133 -0
- package/skills/gamedev-multiplayer/references/prediction-and-latency.md +104 -0
- package/skills/gamedev-multiplayer/references/unity-and-unreal-netcode.md +166 -0
- package/skills/gamedev-pathing/SKILL.md +242 -0
- package/skills/gamedev-pathing/evals/README.md +14 -0
- package/skills/gamedev-pathing/evals/cases.yaml +49 -0
- package/skills/gamedev-pathing/references/navmesh-workflow.md +110 -0
- package/skills/gamedev-pathing/references/search-algorithms.md +135 -0
- package/skills/gamedev-pathing/references/steering-and-avoidance.md +120 -0
- package/skills/gamedev-physics/SKILL.md +225 -0
- package/skills/gamedev-physics/evals/README.md +12 -0
- package/skills/gamedev-physics/evals/cases.yaml +54 -0
- package/skills/gamedev-physics/references/character-controllers.md +120 -0
- package/skills/gamedev-physics/references/determinism-and-queries.md +124 -0
- package/skills/gamedev-physics/references/layers-and-masks.md +115 -0
- package/skills/gamedev-shaders/SKILL.md +252 -0
- package/skills/gamedev-shaders/evals/README.md +14 -0
- package/skills/gamedev-shaders/evals/cases.yaml +45 -0
- package/skills/gamedev-shaders/references/effect-recipes.md +205 -0
- package/skills/gamedev-shaders/references/godot-shading-language.md +133 -0
- package/skills/gamedev-shaders/references/unity-and-unreal-shaders.md +169 -0
- package/skills/gamedev-shipping/SKILL.md +236 -0
- package/skills/gamedev-shipping/evals/README.md +9 -0
- package/skills/gamedev-shipping/evals/cases.yaml +55 -0
- package/skills/gamedev-shipping/references/mobile-and-desktop.md +127 -0
- package/skills/gamedev-shipping/references/stores-and-ci.md +162 -0
- package/skills/gamedev-shipping/references/web-export-headers.md +140 -0
- package/skills/godot/SKILL.md +319 -0
- package/skills/godot/evals/README.md +13 -0
- package/skills/godot/evals/cases.yaml +50 -0
- package/skills/godot/references/export-and-testing.md +102 -0
- package/skills/godot/references/gdscript-style.md +84 -0
- package/skills/godot/references/godot3-to-4-traps.md +89 -0
- package/skills/godot/references/nodes-scenes-resources.md +109 -0
- package/skills/level-design/SKILL.md +175 -0
- package/skills/level-design/evals/README.md +35 -0
- package/skills/level-design/evals/cases.yaml +58 -0
- package/skills/level-design/references/blockout-to-art.md +118 -0
- package/skills/level-design/references/guidance-and-readability.md +152 -0
- package/skills/level-design/references/pacing-and-encounters.md +142 -0
- package/skills/roast-me/tools/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/claude.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/codex.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/gemini.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/unity/SKILL.md +282 -0
- package/skills/unity/evals/README.md +8 -0
- package/skills/unity/evals/cases.yaml +43 -0
- package/skills/unity/references/build-and-platforms.md +65 -0
- package/skills/unity/references/input-system.md +102 -0
- package/skills/unity/references/lifecycle-deep-dive.md +75 -0
- package/skills/unreal/SKILL.md +270 -0
- package/skills/unreal/evals/README.md +12 -0
- package/skills/unreal/evals/cases.yaml +45 -0
- package/skills/unreal/references/blueprint-cpp-parity.md +109 -0
- package/skills/unreal/references/gameplay-framework.md +95 -0
- package/skills/unreal/references/packaging.md +83 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
skill: gamedev-physics
|
|
2
|
+
|
|
3
|
+
should_trigger:
|
|
4
|
+
- prompt: "My player keeps falling through the floor when he moves fast, but it's fine when he walks slowly. Godot 4."
|
|
5
|
+
why: "Classic tunneling symptom (speed-dependent fall-through); routes here for CCD / fixed-tick + shapecast-then-move, not a generic debugger."
|
|
6
|
+
- prompt: "Two crates that should bump into each other just pass straight through. How do I set up the collision layer and mask so they collide?"
|
|
7
|
+
why: "Direct collision layer/mask filtering question — the #1-confusion section this skill owns."
|
|
8
|
+
- prompt: "In Unity should I put my rigidbody movement in Update or FixedUpdate? My box vibrates against the ground."
|
|
9
|
+
why: "Fixed-timestep placement + jitter stability, both core determinism topics; also touches the Unity version contract."
|
|
10
|
+
- prompt: "What's the difference between AddForce with Impulse mode, VelocityChange, and just setting velocity on a rigidbody?"
|
|
11
|
+
why: "Forces vs impulses vs direct velocity is an explicit section with a per-engine table."
|
|
12
|
+
- prompt: "I want a coin pickup that detects when the player walks over it but doesn't physically block them. What component do I use?"
|
|
13
|
+
why: "Trigger/Area overlap-with-no-response — colliders/triggers section; must not be treated as solid collision."
|
|
14
|
+
- prompt: "Building a first-person character controller in Godot 4 — how do I do gravity, jumping, and slopes with CharacterBody3D?"
|
|
15
|
+
why: "Character controller with move_and_slide/velocity and slope/step handling; also exercises the Godot 4 no-args move_and_slide version contract."
|
|
16
|
+
- prompt: "My rigidbody physics 'explodes' whenever a heavy object rests on a light one, or when I move the object by setting its transform each frame."
|
|
17
|
+
why: "Mass-ratio instability + writing transform on a dynamic body — two determinism guardrails this skill owns."
|
|
18
|
+
|
|
19
|
+
should_not_trigger:
|
|
20
|
+
- prompt: "My water surface shader is shimmering and the reflections flicker at grazing angles. Fix the material."
|
|
21
|
+
route_to: "gamedev-shaders"
|
|
22
|
+
why: "Visual/material jitter from a shader, not simulated physics; explicitly delegated in the NOT-when clause."
|
|
23
|
+
- prompt: "My enemies need to find a path around obstacles to the player using a navmesh and avoid each other."
|
|
24
|
+
route_to: "gamedev-pathing"
|
|
25
|
+
why: "Navigation/pathfinding decides where to go; physics only executes the move. Delegated to gamedev-pathing."
|
|
26
|
+
- prompt: "How do I set up the Godot editor, import my project, and configure export templates for Android?"
|
|
27
|
+
route_to: "godot"
|
|
28
|
+
why: "Engine setup/editor/export tooling belongs to the godot engine skill, not the cross-engine physics concept skill."
|
|
29
|
+
- prompt: "Sync player movement across clients with server-authoritative netcode and rollback so it stays consistent."
|
|
30
|
+
route_to: "gamedev-multiplayer"
|
|
31
|
+
why: "Netcode/rollback is multiplayer's domain; this skill only supplies the deterministic fixed-tick foundation."
|
|
32
|
+
- prompt: "Design the combat loop and difficulty curve for my roguelike."
|
|
33
|
+
route_to: "game-design"
|
|
34
|
+
why: "Gameplay/systems design, no physics simulation, colliders, or bodies involved."
|
|
35
|
+
|
|
36
|
+
capability:
|
|
37
|
+
- scenario: "User's fast bullet tunnels through thin walls in Unity 6, and they ask why and how to fix it, including a code snippet."
|
|
38
|
+
must_include:
|
|
39
|
+
- "Explains tunneling: the body moves more than its own thickness in one physics tick so no overlap is tested (discrete collision)"
|
|
40
|
+
- "Recommends enabling Continuous Collision Detection via Rigidbody.collisionDetectionMode = Continuous (or ContinuousDynamic/ContinuousSpeculative) on the fast body"
|
|
41
|
+
- "Offers the raycast/shapecast-then-move alternative for very small, very fast projectiles as more reliable than CCD"
|
|
42
|
+
- "Places physics movement in FixedUpdate, not Update"
|
|
43
|
+
- "Uses linearVelocity (not the deprecated Rigidbody.velocity) if it sets velocity directly, per Unity 6"
|
|
44
|
+
- scenario: "User asks for a Godot 4 CharacterBody3D controller with gravity, jump, and slope handling."
|
|
45
|
+
must_include:
|
|
46
|
+
- "Uses CharacterBody3D with a velocity property and calls move_and_slide() with NO arguments (Godot 4), not the Godot 3 move_and_slide(velocity, ...) signature"
|
|
47
|
+
- "Runs the movement in _physics_process(delta), the fixed-tick callback"
|
|
48
|
+
- "Adds gravity as velocity += gravity * delta but does NOT multiply the whole velocity by delta before move_and_slide()"
|
|
49
|
+
- "Uses is_on_floor() for grounded/jump checks and mentions floor_max_angle and/or floor_snap_length for slopes/steps"
|
|
50
|
+
- scenario: "User cannot get a pickup trigger and a colliding wall filtered correctly and asks about layers/masks."
|
|
51
|
+
must_include:
|
|
52
|
+
- "States layer = what the object is, mask = what it scans for, as independent bit sets"
|
|
53
|
+
- "Gives the correct per-engine mechanism: Godot collision_layer/collision_mask (mask must include the other's layer), Unity Layer Collision Matrix + LayerMask, or Unreal object channels + Block/Overlap/Ignore responses"
|
|
54
|
+
- "Uses a trigger/Area (Godot Area, Unity IsTrigger, Unreal Overlap response) for the detect-but-don't-block pickup, and notes a Unity trigger needs a Rigidbody on one object"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Character controllers — kinematic vs dynamic, per engine
|
|
2
|
+
|
|
3
|
+
A "character controller" is the code that turns intent (move left, jump) into collision-resolved
|
|
4
|
+
motion. Two approaches; pick one and commit.
|
|
5
|
+
|
|
6
|
+
## Kinematic vs dynamic — the trade
|
|
7
|
+
|
|
8
|
+
| | Kinematic character | Dynamic (rigidbody) character |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| Motion | you set velocity, a sweep resolves it | forces/impulses, solver resolves it |
|
|
11
|
+
| Feel | precise, snappy, fully authored | physical, momentum-y, harder to make crisp |
|
|
12
|
+
| Pushed by physics | no (you handle it) | yes, for free |
|
|
13
|
+
| Pushes dynamics | yes | yes |
|
|
14
|
+
| Gravity | you add it manually | automatic |
|
|
15
|
+
| Typical use | platformers, shooters, most players | ragdoll-ish, vehicles, physics puzzles |
|
|
16
|
+
| Pain points | you re-implement gravity/slopes/steps | tipping, sliding, jitter — needs constraints + friction |
|
|
17
|
+
|
|
18
|
+
Default to **kinematic** for a controllable character. Reach for dynamic only when physical
|
|
19
|
+
interaction *is* the gameplay.
|
|
20
|
+
|
|
21
|
+
## Godot 4 — CharacterBody (kinematic)
|
|
22
|
+
|
|
23
|
+
`CharacterBody2D/3D` exposes a `velocity` property and `move_and_slide()`, which sweeps by
|
|
24
|
+
`velocity` (already delta-scaled) and slides along surfaces.
|
|
25
|
+
|
|
26
|
+
```gdscript
|
|
27
|
+
extends CharacterBody3D
|
|
28
|
+
|
|
29
|
+
const SPEED := 5.0
|
|
30
|
+
const JUMP_VELOCITY := 4.5
|
|
31
|
+
|
|
32
|
+
func _physics_process(delta: float) -> void:
|
|
33
|
+
if not is_on_floor():
|
|
34
|
+
velocity += get_gravity() * delta # get_gravity() is 4.3+; else read ProjectSettings
|
|
35
|
+
if Input.is_action_just_pressed("jump") and is_on_floor():
|
|
36
|
+
velocity.y = JUMP_VELOCITY
|
|
37
|
+
var input := Input.get_vector("left", "right", "up", "down")
|
|
38
|
+
var dir := (transform.basis * Vector3(input.x, 0, input.y)).normalized()
|
|
39
|
+
velocity.x = dir.x * SPEED
|
|
40
|
+
velocity.z = dir.z * SPEED
|
|
41
|
+
move_and_slide()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Tuning knobs on the node:
|
|
45
|
+
- `up_direction` — defines "up" for floor/wall/ceiling classification (default `Vector3.UP`).
|
|
46
|
+
- `floor_max_angle` — steepest slope treated as floor (radians; default 45°). Steeper = wall.
|
|
47
|
+
- `floor_snap_length` — pulls the body to the ground while descending ramps/stairs (kills
|
|
48
|
+
launch-off-ramp and airborne-on-stairs). Requires `floor_stop_on_slope` behavior tuning.
|
|
49
|
+
- `is_on_floor()` / `is_on_wall()` / `is_on_ceiling()`, `get_floor_normal()`,
|
|
50
|
+
`get_slide_collision_count()` + `get_slide_collision(i)` to inspect what was hit.
|
|
51
|
+
- `move_and_collide(motion)` — lower-level single sweep returning a `KinematicCollision`; use when
|
|
52
|
+
you want full manual control (custom slide, wall-jump math).
|
|
53
|
+
|
|
54
|
+
For one-off exact motion (dashes, kinematic bodies not needing slide) use `move_and_collide`.
|
|
55
|
+
|
|
56
|
+
## Unity 6 — CharacterController (kinematic) and the dynamic alternative
|
|
57
|
+
|
|
58
|
+
`CharacterController` is a built-in kinematic capsule with sweeping `Move`. It handles steps and
|
|
59
|
+
slopes via `stepOffset` and `slopeLimit`; it does **not** apply gravity — you do.
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
[RequireComponent(typeof(CharacterController))]
|
|
63
|
+
public class Player : MonoBehaviour {
|
|
64
|
+
public float speed = 6f, jumpVelocity = 5f, gravity = -20f;
|
|
65
|
+
CharacterController cc;
|
|
66
|
+
Vector3 vel; // world-space velocity we integrate ourselves
|
|
67
|
+
|
|
68
|
+
void Awake() => cc = GetComponent<CharacterController>();
|
|
69
|
+
|
|
70
|
+
void Update() { // CharacterController is kinematic: Update is fine
|
|
71
|
+
if (cc.isGrounded && vel.y < 0f) vel.y = -2f; // keep grounded
|
|
72
|
+
Vector3 wish = transform.right * Input.GetAxis("Horizontal")
|
|
73
|
+
+ transform.forward * Input.GetAxis("Vertical");
|
|
74
|
+
if (cc.isGrounded && Input.GetButtonDown("Jump")) vel.y = jumpVelocity;
|
|
75
|
+
vel.y += gravity * Time.deltaTime; // integrate gravity
|
|
76
|
+
cc.Move((wish * speed + Vector3.up * vel.y) * Time.deltaTime);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Key fields: `isGrounded`, `slopeLimit` (max walkable slope), `stepOffset` (max step height),
|
|
82
|
+
`skinWidth` (small penetration tolerance — keep ~10% of radius; too small causes jitter),
|
|
83
|
+
`center`/`radius`/`height`. `Move` returns `CollisionFlags`; hook `OnControllerColliderHit` to push
|
|
84
|
+
dynamic rigidbodies.
|
|
85
|
+
|
|
86
|
+
**Dynamic rigidbody character** instead: `Rigidbody` with `freezeRotation = true` (or constrain
|
|
87
|
+
X/Z rotation), a high-friction physics material, movement via `AddForce`/target-velocity in
|
|
88
|
+
`FixedUpdate`, and a grounded raycast. More physical, more tuning.
|
|
89
|
+
|
|
90
|
+
## Unreal (UE5) — Character + CharacterMovementComponent
|
|
91
|
+
|
|
92
|
+
`ACharacter` bundles a capsule + `UCharacterMovementComponent` (CMC), a sophisticated kinematic
|
|
93
|
+
controller. It sweeps, handles Walking / Falling / Swimming / Flying **movement modes**, and applies
|
|
94
|
+
its own gravity scale.
|
|
95
|
+
|
|
96
|
+
- **Input → movement:** `AddMovementInput(WorldDirection, ScaleValue)`; jump via `Jump()` /
|
|
97
|
+
`StopJumping()` (`ACharacter` overrides).
|
|
98
|
+
- **Slopes/steps:** CMC `MaxStepHeight`, `SetWalkableFloorAngle` / `GetWalkableFloorAngle`,
|
|
99
|
+
`MaxWalkSpeed`, `GravityScale`, `JumpZVelocity`, `AirControl`.
|
|
100
|
+
- **Ground state:** `Movement->IsMovingOnGround()`, `IsFalling()`; `CurrentFloor` holds the hit.
|
|
101
|
+
- Moving platforms are handled by CMC's base-relative movement when the character stands on a
|
|
102
|
+
movable primitive set as its movement base.
|
|
103
|
+
|
|
104
|
+
For non-Character pawns you can drive a movable component manually or add a `PawnMovementComponent`.
|
|
105
|
+
|
|
106
|
+
## Ground detection, slopes, steps, moving platforms — the recurring problems
|
|
107
|
+
|
|
108
|
+
- **Ground detection** must be robust: a single downward ray can miss on ledges. Use the built-in
|
|
109
|
+
`is_on_floor()` / `isGrounded` / `IsMovingOnGround()` where possible; if rolling your own, use a
|
|
110
|
+
short **shapecast** (capsule/sphere) slightly wider than the character, not a thin ray.
|
|
111
|
+
- **Slopes:** set the max-walkable-angle knob so steep slopes read as walls; add slide-down on
|
|
112
|
+
too-steep surfaces if desired. Project intended velocity onto the floor plane to avoid bumpy speed
|
|
113
|
+
on ramps.
|
|
114
|
+
- **Steps/stairs:** rely on `stepOffset` / `MaxStepHeight` / `floor_snap_length`. A capsule collider
|
|
115
|
+
climbs small steps far more smoothly than a box.
|
|
116
|
+
- **Moving platforms:** the platform must be a **kinematic mover** that reports motion (Godot
|
|
117
|
+
`AnimatableBody` with `sync_to_physics = true`; Unity kinematic `Rigidbody.MovePosition` in
|
|
118
|
+
`FixedUpdate`; Unreal movable primitive as movement base). A **static** body will not carry a
|
|
119
|
+
character. For a kinematic character on a moving platform, add the platform's per-tick delta to the
|
|
120
|
+
character before moving, or parent to the platform's motion.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Determinism, stability & queries
|
|
2
|
+
|
|
3
|
+
Most "my physics is unstable" reports reduce to running simulation in the wrong loop, scaling by the
|
|
4
|
+
wrong delta, or tunneling. This is the fix list plus the query APIs.
|
|
5
|
+
|
|
6
|
+
## The fixed timestep — where physics must live
|
|
7
|
+
|
|
8
|
+
Engines separate the **render frame** (variable rate, once per drawn image) from the **physics tick**
|
|
9
|
+
(fixed rate, e.g. 60 Hz). The solver assumes a constant `dt`; feeding it a variable frame time makes
|
|
10
|
+
behavior frame-rate-dependent — the same jump reaches different heights at 30 vs 144 fps.
|
|
11
|
+
|
|
12
|
+
| Engine | Fixed-tick callback | Rate setting |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Godot 4 | `_physics_process(delta)` | `Engine.physics_ticks_per_second` / Project Settings → Physics → Physics Ticks per Second (default 60) |
|
|
15
|
+
| Unity 6 | `FixedUpdate()` | `Time.fixedDeltaTime` / Project Settings → Time → Fixed Timestep |
|
|
16
|
+
| Unreal | Chaos **substepping** / Async Physics Tick | Project Settings → Physics → Substepping (`Max Substep Delta Time`, `Max Substeps`) |
|
|
17
|
+
|
|
18
|
+
**Pattern:** poll input in the frame update (`_process` / `Update` / `Tick`), cache intent, then
|
|
19
|
+
*apply* forces/velocity and run physics queries in the fixed tick. Never move a physics body or read
|
|
20
|
+
physics state from the render frame.
|
|
21
|
+
|
|
22
|
+
In Unreal, enabling **Substepping** lets the solver take several fixed sub-steps within a long frame,
|
|
23
|
+
keeping constraints stable at low fps; for hard determinism use the Chaos async physics tick, which
|
|
24
|
+
runs your callback at a fixed rate off the game thread.
|
|
25
|
+
|
|
26
|
+
## Scale by delta — but not twice
|
|
27
|
+
|
|
28
|
+
- Manual position changes, accelerations, and continuous forces you integrate yourself: multiply by
|
|
29
|
+
the tick delta (`delta` / `fixedDeltaTime`).
|
|
30
|
+
- Helpers that already integrate time: do **not** delta-scale their input. Godot `move_and_slide()`
|
|
31
|
+
takes a per-second `velocity` and applies the timestep internally. Unity `CharacterController.Move`
|
|
32
|
+
expects a per-*frame* displacement, so you pass `velocity * Time.deltaTime` (velocity is per
|
|
33
|
+
second) — the classic mistake is forgetting the delta there while double-applying it elsewhere.
|
|
34
|
+
- Gravity done by hand: `velocity += gravity * delta` each tick (gravity is an acceleration).
|
|
35
|
+
|
|
36
|
+
## Interpolation — smooth visuals without touching the sim
|
|
37
|
+
|
|
38
|
+
At frame rates above the tick rate, rendering the raw physics position looks choppy. Turn on
|
|
39
|
+
interpolation to render an in-between pose; it is **purely visual** and never alters the simulation.
|
|
40
|
+
|
|
41
|
+
- Godot 4: physics interpolation (Project Settings → Physics → Common → Physics Interpolation, plus
|
|
42
|
+
per-node where applicable).
|
|
43
|
+
- Unity: `Rigidbody.interpolation = RigidbodyInterpolation.Interpolate` (or `Extrapolate`).
|
|
44
|
+
- Unreal: interpolation handled by the movement/render sync; async physics results are interpolated.
|
|
45
|
+
|
|
46
|
+
## Tunneling & CCD
|
|
47
|
+
|
|
48
|
+
A fast body moving more than its own thickness per tick can pass fully through a thin collider before
|
|
49
|
+
any overlap is tested (discrete collision). Fixes, cheapest first:
|
|
50
|
+
|
|
51
|
+
1. Make the wall/collider thicker than the fastest per-tick travel, or the projectile bigger.
|
|
52
|
+
2. Enable **Continuous Collision Detection** on the *fast* body only (it is expensive):
|
|
53
|
+
- Godot: `continuous_cd` (RigidBody2D: Disabled / Cast Ray / Cast Shape; RigidBody3D: on/off).
|
|
54
|
+
- Unity: `Rigidbody.collisionDetectionMode = Continuous` / `ContinuousDynamic` /
|
|
55
|
+
`ContinuousSpeculative`.
|
|
56
|
+
- Unreal: **Use CCD** on the body instance.
|
|
57
|
+
3. For bullets, prefer a **raycast/shapecast-then-move**: sweep from last position to next, and if it
|
|
58
|
+
hits, resolve at the hit point. This is more reliable than CCD for very small, very fast objects.
|
|
59
|
+
|
|
60
|
+
## Other stability rules
|
|
61
|
+
|
|
62
|
+
- **Mass ratios:** keep interacting/jointed bodies within ~10:1 (100:1 blows up). Very light bodies
|
|
63
|
+
under very heavy ones jitter or get ejected.
|
|
64
|
+
- **Never scale a collider at runtime.** Scaling the transform of a body with a collider (especially
|
|
65
|
+
non-uniform scale) corrupts the shape and broadphase — swap the shape or size the shape resource
|
|
66
|
+
instead.
|
|
67
|
+
- **Never write `transform`/`position`/`rotation` on a dynamic (simulated) body.** It teleports past
|
|
68
|
+
the solver, producing explosions, missed contacts, and ghost collisions. Move dynamics with
|
|
69
|
+
forces/impulses/velocity; move kinematics with the provided API (`MovePosition`,
|
|
70
|
+
`move_and_slide`, `SetWorldLocation` with sweep).
|
|
71
|
+
- **Sleeping:** resting bodies sleep to save CPU and stop micro-jitter. Applying a force or a query
|
|
72
|
+
may need to wake them (`RigidBody.sleeping = false`, `Rigidbody.WakeUp()`); don't fight the sleep
|
|
73
|
+
system with tiny constant forces.
|
|
74
|
+
- **Solver iterations:** if stacks/joints sag or jitter, raise solver iteration counts (Project/
|
|
75
|
+
physics settings) before hacking masses.
|
|
76
|
+
|
|
77
|
+
## Queries — raycast, shapecast, overlap
|
|
78
|
+
|
|
79
|
+
Run queries in the fixed tick so they see the simulated state, and filter by layer/mask/channel.
|
|
80
|
+
|
|
81
|
+
**Godot 4** — via the space state (works in `_physics_process`):
|
|
82
|
+
|
|
83
|
+
```gdscript
|
|
84
|
+
var space := get_world_3d().direct_space_state
|
|
85
|
+
var q := PhysicsRayQueryParameters3D.create(from, to)
|
|
86
|
+
q.collision_mask = 0b0001 # only scan layer 1
|
|
87
|
+
q.exclude = [self] # ignore ourselves
|
|
88
|
+
var hit := space.intersect_ray(q) # {} if nothing; else position, normal, collider, ...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Shape sweep: `PhysicsShapeQueryParameters3D` + `intersect_shape` (overlap) or `cast_motion` (how far
|
|
92
|
+
a shape can move before hitting). Node helpers `RayCast2D/3D` and `ShapeCast2D/3D` update every tick.
|
|
93
|
+
|
|
94
|
+
**Unity 6:**
|
|
95
|
+
|
|
96
|
+
```csharp
|
|
97
|
+
int mask = LayerMask.GetMask("Ground");
|
|
98
|
+
if (Physics.Raycast(origin, dir, out RaycastHit hit, dist, mask)) { /* hit.point, hit.normal */ }
|
|
99
|
+
if (Physics.SphereCast(origin, radius, dir, out RaycastHit sh, dist, mask)) { /* swept */ }
|
|
100
|
+
Collider[] hits = Physics.OverlapSphere(center, radius, mask); // overlap test
|
|
101
|
+
bool blocked = Physics.CheckSphere(center, radius, mask); // cheap boolean
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use `RaycastNonAlloc` / `OverlapSphereNonAlloc` in hot paths to avoid garbage. 2D equivalents live on
|
|
105
|
+
`Physics2D` (`Raycast`, `OverlapCircle`, `CircleCast`).
|
|
106
|
+
|
|
107
|
+
**Unreal (UE5):**
|
|
108
|
+
|
|
109
|
+
```cpp
|
|
110
|
+
FHitResult Hit;
|
|
111
|
+
FCollisionQueryParams Params(NAME_None, /*bTraceComplex=*/false, this);
|
|
112
|
+
bool bHit = GetWorld()->LineTraceSingleByChannel(
|
|
113
|
+
Hit, Start, End, ECC_Visibility, Params); // trace channel
|
|
114
|
+
GetWorld()->SweepSingleByChannel( // shapecast
|
|
115
|
+
Hit, Start, End, FQuat::Identity, ECC_Pawn,
|
|
116
|
+
FCollisionShape::MakeSphere(30.f), Params);
|
|
117
|
+
TArray<FOverlapResult> Overlaps; // overlap test
|
|
118
|
+
GetWorld()->OverlapMultiByChannel(
|
|
119
|
+
Overlaps, Center, FQuat::Identity, ECC_WorldDynamic,
|
|
120
|
+
FCollisionShape::MakeBox(FVector(50)), Params);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `...ByObjectType` variants to query by object channels, `...ByChannel` for trace channels. Set
|
|
124
|
+
`bTraceComplex = true` only when you need per-triangle precision (costly).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Collision layers & masks — the filtering that trips everyone up
|
|
2
|
+
|
|
3
|
+
The single most common physics bug that is not a crash: "these two things should collide and they
|
|
4
|
+
don't," or "this ghost trigger fires on everything." Both are filtering. The concept is universal;
|
|
5
|
+
only the words change.
|
|
6
|
+
|
|
7
|
+
**Layer = what I *am*. Mask = what I *scan for*.** They are independent bit sets. A body being in
|
|
8
|
+
layer 3 says nothing about what it collides with — that is the mask's job.
|
|
9
|
+
|
|
10
|
+
## Godot 4
|
|
11
|
+
|
|
12
|
+
Two properties on every `CollisionObject2D/3D` (StaticBody, RigidBody, CharacterBody, Area):
|
|
13
|
+
|
|
14
|
+
- `collision_layer` — the layer bits this body occupies (what it *is*).
|
|
15
|
+
- `collision_mask` — the layer bits this body scans (what it *looks for*).
|
|
16
|
+
|
|
17
|
+
**The rule:** a contact is registered when **either** body's `collision_mask` intersects the
|
|
18
|
+
other's `collision_layer`. It is an **OR**, which is why detection can be *asymmetric* — body A can
|
|
19
|
+
detect body B even if B does not scan for A. This is deliberate (one-way triggers, sensors) but is
|
|
20
|
+
also the classic footgun. **Keep layer/mask symmetric for solid collisions** unless you truly want
|
|
21
|
+
one-way behavior.
|
|
22
|
+
|
|
23
|
+
For an `Area2D/3D`, the split matters: the Area's **mask** determines which bodies/areas it
|
|
24
|
+
*detects* (fires `body_entered` / `area_entered`); the Area's **layer** determines whether something
|
|
25
|
+
else can detect the Area. A detector-only Area often has a mask set and layer cleared.
|
|
26
|
+
|
|
27
|
+
Name your layers in **Project Settings → Layer Names → 2D/3D Physics** so the inspector shows
|
|
28
|
+
"player / enemy / world / pickup" instead of bare numbers. Set bits from code with the helpers, not
|
|
29
|
+
raw math:
|
|
30
|
+
|
|
31
|
+
```gdscript
|
|
32
|
+
set_collision_layer_value(2, true) # I am on layer 2
|
|
33
|
+
set_collision_mask_value(1, true) # I scan layer 1
|
|
34
|
+
set_collision_mask_value(3, true) # ...and layer 3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Bits are 1-indexed in these helpers; the raw `collision_layer` int is a bitmask (layer 1 = value 1,
|
|
38
|
+
layer 2 = value 2, layer 3 = value 4, ...). `collision_layer = 0b101` means layers 1 and 3.
|
|
39
|
+
|
|
40
|
+
## Unity 6
|
|
41
|
+
|
|
42
|
+
Physics filtering is layer-pair based, not per-object masks:
|
|
43
|
+
|
|
44
|
+
1. Assign each GameObject a **Layer** (Inspector top-right; define names in Tags & Layers). Physics
|
|
45
|
+
layers are the same 32 layers used everywhere.
|
|
46
|
+
2. **Project Settings → Physics → Layer Collision Matrix** (and Physics 2D separately) is a
|
|
47
|
+
checkbox grid: an unticked cell means those two layers **never** collide or trigger. This is the
|
|
48
|
+
global filter.
|
|
49
|
+
3. Toggle a single pair at runtime: `Physics.IgnoreLayerCollision(layerA, layerB, true)` — or
|
|
50
|
+
`Physics.IgnoreCollision(colliderA, colliderB)` for two specific colliders.
|
|
51
|
+
|
|
52
|
+
Queries take a **`LayerMask`** argument, which is a *different* use of the same layers — it says
|
|
53
|
+
which layers the ray/overlap considers:
|
|
54
|
+
|
|
55
|
+
```csharp
|
|
56
|
+
// Build a mask from names, then raycast only against Ground + Enemy.
|
|
57
|
+
int mask = LayerMask.GetMask("Ground", "Enemy");
|
|
58
|
+
if (Physics.Raycast(origin, dir, out RaycastHit hit, 50f, mask)) { /* ... */ }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A `LayerMask` is a bitfield: `1 << layerIndex`. Combine with `|`, exclude with `~`. A common mistake
|
|
62
|
+
is passing a layer *index* where a *mask* is expected — always shift or use `LayerMask.GetMask`.
|
|
63
|
+
|
|
64
|
+
## Unreal (UE5, Chaos)
|
|
65
|
+
|
|
66
|
+
Two axes per primitive component, configured via **Collision Presets** (Details → Collision):
|
|
67
|
+
|
|
68
|
+
- **Object Type** — the single channel this component *is* (WorldStatic, WorldDynamic, Pawn,
|
|
69
|
+
PhysicsBody, or a custom object channel). This is "what I am."
|
|
70
|
+
- **Collision Responses** — for every channel, one of **Block / Overlap / Ignore**. This is "how I
|
|
71
|
+
react to each kind of thing."
|
|
72
|
+
|
|
73
|
+
For two components to physically **block**, *both* must respond Block to the other's object type. For
|
|
74
|
+
an **overlap** event, at least one responds Overlap (and the other not Ignore) and **Generate
|
|
75
|
+
Overlap Events** is enabled on both. Bundle a configuration into a named **Collision Preset**
|
|
76
|
+
(Project Settings → Collision) so a whole class of actors shares it — e.g. a `Trigger` preset that
|
|
77
|
+
overlaps Pawns and ignores everything else.
|
|
78
|
+
|
|
79
|
+
**Trace channels** (Visibility, Camera, or custom) are a *separate* set used only by line/shape
|
|
80
|
+
traces, distinct from object channels used for physical collision. A common error is tracing on an
|
|
81
|
+
object channel or vice versa.
|
|
82
|
+
|
|
83
|
+
```cpp
|
|
84
|
+
// Custom object channel example (defined in DefaultEngine.ini as ECC_GameTraceChannel1).
|
|
85
|
+
Comp->SetCollisionObjectType(ECC_Pawn);
|
|
86
|
+
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
|
87
|
+
Comp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
|
|
88
|
+
Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
|
89
|
+
Comp->SetGenerateOverlapEvents(true);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Worked example — player / enemy / pickup / wall
|
|
93
|
+
|
|
94
|
+
Desired: player and enemies collide with walls and each other; the pickup collides with nothing but
|
|
95
|
+
detects the player touching it.
|
|
96
|
+
|
|
97
|
+
| Thing | Godot layer | Godot mask | Unity layer / matrix | Unreal preset |
|
|
98
|
+
| --- | --- | --- | --- | --- |
|
|
99
|
+
| Wall | World | (none needed) | World; collides with Player, Enemy | BlockAll |
|
|
100
|
+
| Player | Player | World, Enemy, Pickup | Player; collides World, Enemy | Pawn (blocks World/Pawn) |
|
|
101
|
+
| Enemy | Enemy | World, Player, Enemy | Enemy; collides World, Player, Enemy | Pawn |
|
|
102
|
+
| Pickup (Area/Trigger) | Pickup | Player | Pickup; **only** Player pair ticked, `IsTrigger` | Custom `Pickup`: overlap Pawn, ignore all else |
|
|
103
|
+
|
|
104
|
+
The pickup is a **trigger/Area**: it scans only for the Player layer and produces an overlap event,
|
|
105
|
+
never a solid collision.
|
|
106
|
+
|
|
107
|
+
## Debug checklist when a collision "doesn't happen"
|
|
108
|
+
|
|
109
|
+
- Do the layer/mask bits actually overlap? (Godot: is one body's mask covering the other's layer?)
|
|
110
|
+
- Unity: is the pair ticked in the **Layer Collision Matrix**, and did you use 2D matrix for 2D?
|
|
111
|
+
- Unity trigger: is there a **Rigidbody** on at least one of the two objects? Trigger events need one.
|
|
112
|
+
- Unreal: are **both** set to Block (for solid) or is Generate Overlap Events on **both** (for overlap)?
|
|
113
|
+
- Query returns nothing: are you passing a **mask/channel** that includes the target? Passing a raw
|
|
114
|
+
layer index instead of a shifted mask silently matches the wrong layer.
|
|
115
|
+
- One-way collision you did not intend → Godot asymmetric mask; make it symmetric.
|