Skip to content

Commit 71c5a6b

Browse files
authored
feat: sdk collision layer for main player + corrections (#8865)
1 parent e313025 commit 71c5a6b

19 files changed

Lines changed: 867 additions & 44 deletions

File tree

.claude/agents/dcl-sdk-feature-implementation/dcl-sdk-specialist.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ Create a test file at a location matching the system's purpose:
203203

204204
Look at `test/ecs/events/videoEventsSystem.spec.ts` for a reference test structure.
205205

206+
## ColliderLayer mask semantics
207+
208+
When generating helpers or tests for components with a `collision_mask` / `collisionMask` field of type `ColliderLayer`, keep these rules in mind (full table + examples in `unity-explorer/docs/how-to-implement-new-sdk-components.md` under "ColliderLayer mask semantics"):
209+
210+
- **Additive avatar semantics:** main player is tagged with both `CL_PLAYER` and `CL_MAIN_PLAYER`; remote avatars only with `CL_PLAYER`.
211+
- **Unified main-player qualification** (Raycast + TriggerArea): the main player qualifies only when the mask contains `CL_PLAYER` or `CL_MAIN_PLAYER` (`PLAYER_QUALIFYING_BITS`). `CL_PHYSICS`, `CL_POINTER`, `CL_CUSTOM*`, and `CL_NONE` do NOT qualify the main player.
212+
- **`CL_PHYSICS` targets scene-mesh walls / floors**, not the character. Scenes that need to detect the player must opt in via `CL_PLAYER` or `CL_MAIN_PLAYER`.
213+
- **Remote avatars qualify only on `CL_PLAYER`.**
214+
- **`CL_MAIN_PLAYER`-only TriggerArea** is fast-pathed via `targetOnlyMainPlayer` / `TargetTransform` early-out. Any other mask must NOT enable this.
215+
- **Scene-mesh routing:** for `MeshCollider` / `GltfContainer`, avatar-only masks (`CL_PLAYER` / `CL_MAIN_PLAYER`, no other bits) route to the `SDKAvatarHit` Unity layer — pass-through for the player capsule, raycast- and trigger-detectable via the matrix. Mixed masks containing `CL_PHYSICS` route to `CharacterOnly` and remain solid.
216+
206217
## GROWN_ONLY_COMPONENTS (GOVS)
207218

208219
For grow-only result components (not LWW), add the component name to:

.claude/agents/dcl-sdk-feature-implementation/dcl-test-scene-specialist.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,41 @@ sdk7-test-scenes/
3232

3333
Each scene folder follows the naming convention: `{x},{y}-{scene-name}`
3434

35+
## MANDATORY: Folder Naming Convention
36+
37+
**Every new scene folder MUST be named `{x},{y}-{scene-name}` where `{x},{y}` matches the parcel coordinates declared in `scene.json` (`scene.parcels[0]` / the base parcel).**
38+
39+
Examples:
40+
-`scenes/5,5-collider-layer-main-player` (parcel `5,5`)
41+
-`scenes/0,7-particle-system` (parcel `0,7`)
42+
-`scenes/100,100-mannakia-test-scene` (parcel `100,100`)
43+
-`scenes/collider-layer-main-player` — missing coordinate prefix
44+
-`scenes/my-feature-test` — missing coordinate prefix
45+
-`scenes/3,3-feature` while `scene.json` says base `5,5` — folder/parcel mismatch
46+
47+
Tooling and humans alike rely on this prefix:
48+
- `dcl-workspace.json` lists scenes alphabetically by folder name — coordinate-prefixed entries sort coherently.
49+
- `npm run check-parcels` validates parcel collisions; a missing prefix slips past spatial review.
50+
- Operators locate scenes by parcel when bug-bashing; an unprefixed folder is invisible to that workflow.
51+
52+
If you ever find yourself about to create a folder without `{x},{y}-` at the start, stop and pick the parcel first.
53+
3554
## Scene Creation Workflow
3655

3756
### Step 1: Duplicate an existing scene
3857

39-
Choose a scene close to your use case and copy it:
58+
Choose a scene close to your use case and copy it. **The destination folder name MUST be `{x},{y}-{scene-name}`** — pick the parcel before creating the folder:
59+
4060
```bash
4161
cd ../sdk7-test-scenes
4262
cp -r scenes/0,0-cube-spawner scenes/<x>,<y>-<new-scene-name>
4363
```
4464

65+
Example for a new scene at parcel `5,5`:
66+
```bash
67+
cp -r scenes/0,0-cube-spawner scenes/5,5-my-feature-test
68+
```
69+
4570
### Step 2: Update scene metadata
4671

4772
**`package.json`** — Update the `name` field:
@@ -170,14 +195,23 @@ npm run check-parcels # Validates all scene parcels, update
170195

171196
## Completion Gate
172197

173-
**Do not report success until `npm run build` passes with zero errors.** Always run before finishing:
174-
175-
```bash
176-
cd ../sdk7-test-scenes/scenes/<x>,<y>-<scene-name>
177-
npm run build # TypeScript compilation must succeed with no errors
178-
```
179-
180-
If the build fails, diagnose and fix before reporting done. Do not hand off a scene that does not compile.
198+
Before reporting success, ALL of the following must hold:
199+
200+
1. **Folder name matches `{x},{y}-{scene-name}`** and `{x},{y}` equals the parcel coordinates declared in `scene.json`. Verify with:
201+
```bash
202+
cd ../sdk7-test-scenes
203+
ls -d scenes/<x>,<y>-<scene-name> # folder exists with coordinate prefix
204+
grep -E '"base"|"parcels"' scenes/<x>,<y>-<scene-name>/scene.json # matches folder prefix
205+
```
206+
If the folder is missing the coordinate prefix (or the prefix does not match the parcel in `scene.json`), rename it now via `mv` and update `dcl-workspace.json` accordingly — do NOT defer this to the user.
207+
2. `npm run check-parcels` (from repo root) reports `✅ No collisions found`.
208+
3. `npm run build` (from inside the scene folder) passes with zero TypeScript errors:
209+
```bash
210+
cd ../sdk7-test-scenes/scenes/<x>,<y>-<scene-name>
211+
npm run build
212+
```
213+
214+
If any gate fails, diagnose and fix before reporting done. Do not hand off a scene that does not compile, lacks the coordinate prefix, or collides with another parcel.
181215

182216
## Git Rules
183217

Explorer/Assets/DCL/Character/CharacterCamera/Character Camera.prefab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2105,7 +2105,7 @@ Camera:
21052105
m_Depth: -1
21062106
m_CullingMask:
21072107
serializedVersion: 2
2108-
m_Bits: 1677918839
2108+
m_Bits: 1678967415
21092109
m_RenderingPath: -1
21102110
m_TargetTexture: {fileID: 0}
21112111
m_TargetDisplay: 0

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/Physics/PhysicsLayers.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ public static class PhysicsLayers
88
private const ColliderLayer LAYER_PHYSICS = ColliderLayer.ClPhysics;
99
private const ColliderLayer LAYER_POINTER = ColliderLayer.ClPointer;
1010
private const ColliderLayer LAYER_PHYSICS_POINTER = LAYER_PHYSICS | LAYER_POINTER;
11+
private const ColliderLayer LAYER_PLAYER = ColliderLayer.ClPlayer;
12+
private const ColliderLayer LAYER_MAIN_PLAYER = ColliderLayer.ClMainPlayer;
13+
14+
/// <summary>
15+
/// Bits that qualify the main player on Raycast and TriggerArea queries.
16+
/// Remote avatars only match on <see cref="ColliderLayer.ClPlayer"/>.
17+
/// </summary>
18+
public const ColliderLayer PLAYER_QUALIFYING_BITS = ColliderLayer.ClPlayer | ColliderLayer.ClMainPlayer;
1119

1220
private const ColliderLayer NON_CUSTOM_LAYERS = ColliderLayer.ClPhysics
1321
| ColliderLayer.ClPointer
@@ -31,7 +39,8 @@ public static class PhysicsLayers
3139
public static readonly int SDK_CUSTOM_LAYER = LayerMask.NameToLayer("SDKCustomLayer");
3240
public static readonly int OTHER_AVATARS_LAYER = LayerMask.NameToLayer("OtherAvatars");
3341
public static readonly int SDK_ENTITY_TRIGGER_AREA = LayerMask.NameToLayer("SDKEntityTriggerArea");
34-
public static readonly int ALL_AVATARS = LayerMask.NameToLayer("AllAvatars");
42+
public static readonly int SDK_AVATAR_TRIGGER_AREA = LayerMask.NameToLayer("SDKAvatarTriggerArea");
43+
public static readonly int SDK_AVATAR_HIT_LAYER = LayerMask.NameToLayer("SDKAvatarHit");
3544

3645
public static readonly LayerMask PLAYER_ORIGIN_RAYCAST_MASK = (1 << ON_POINTER_EVENT_LAYER) | (1 << DEFAULT_LAYER) | (1 << OTHER_AVATARS_LAYER);
3746
public static readonly LayerMask CHARACTER_ONLY_MASK = (1 << DEFAULT_LAYER) | (1 << FLOOR_LAYER) | (1 << CHARACTER_ONLY_LAYER);
@@ -40,6 +49,15 @@ public static class PhysicsLayers
4049
public static bool LayerMaskHasAnySDKCustomLayer(ColliderLayer layerMask) =>
4150
(layerMask & ~NON_CUSTOM_LAYERS) != 0;
4251

52+
/// <summary>
53+
/// True when the mask is exclusively avatar bits (CL_PLAYER / CL_MAIN_PLAYER, no other bits).
54+
/// Used to route SDK colliders to the SDKAvatarHit Unity layer.
55+
/// </summary>
56+
public static bool IsAvatarOnlyMask(ColliderLayer sdkMask) =>
57+
sdkMask != ColliderLayer.ClNone
58+
&& (sdkMask & PLAYER_QUALIFYING_BITS) != 0
59+
&& (sdkMask & ~PLAYER_QUALIFYING_BITS) == 0;
60+
4361
public static bool LayerMaskContainsTargetLayer(uint layerMask, uint targetLayer)
4462
=> (layerMask & targetLayer) != 0;
4563

@@ -51,7 +69,16 @@ public static bool LayerMaskContainsTargetLayer(ColliderLayer layerMask, Collide
5169

5270
public static int CreateUnityLayerMaskFromSDKMask(ColliderLayer sdkMask)
5371
{
54-
int unityLayerMask = (1 << CHARACTER_LAYER) | (1 << DEFAULT_LAYER);
72+
// Default keeps catching SDK meshes on the Default layer regardless of which SDK bits are set.
73+
int unityLayerMask = 1 << DEFAULT_LAYER;
74+
75+
// Player-qualifying bits include the main player capsule (CHARACTER_LAYER) and the SDK avatar-hit
76+
// layer (SDKAvatarHit) where SDK MeshCollider/GltfContainer colliders tagged with avatar bits live.
77+
if ((sdkMask & PLAYER_QUALIFYING_BITS) != 0)
78+
{
79+
unityLayerMask |= 1 << CHARACTER_LAYER;
80+
unityLayerMask |= 1 << SDK_AVATAR_HIT_LAYER;
81+
}
5582

5683
unityLayerMask |= sdkMask switch
5784
{
@@ -60,6 +87,10 @@ public static int CreateUnityLayerMaskFromSDKMask(ColliderLayer sdkMask)
6087
_ => (1 << CHARACTER_ONLY_LAYER) | (1 << ON_POINTER_EVENT_LAYER),
6188
};
6289

90+
// CL_PLAYER targets any avatar: include OTHER_AVATARS_LAYER. CL_MAIN_PLAYER alone is local-only.
91+
if ((sdkMask & LAYER_PLAYER) == LAYER_PLAYER)
92+
unityLayerMask |= 1 << OTHER_AVATARS_LAYER;
93+
6394
// 8 Custom SDK Layers are projected onto a single Unity layer
6495
if (LayerMaskHasAnySDKCustomLayer(sdkMask))
6596
unityLayerMask |= 1 << SDK_CUSTOM_LAYER;
@@ -87,6 +118,14 @@ public static bool TryGetUnityLayerFromSDKLayer(ColliderLayer sdkMask, out int u
87118
return true;
88119
}
89120

121+
// Avatar-only masks route to SDKAvatarHit. Player capsule passes through (matrix-disabled);
122+
// trigger areas and raycasts targeting avatar bits still detect them.
123+
if (IsAvatarOnlyMask(sdkMask))
124+
{
125+
unityLayer = SDK_AVATAR_HIT_LAYER;
126+
return true;
127+
}
128+
90129
if (LayerMaskHasAnySDKCustomLayer(sdkMask))
91130
{
92131
unityLayer = SDK_CUSTOM_LAYER;

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/Physics/Tests.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"reference": "GUID:da80994a355e49d5b84f91c0a84a721f"
3+
}

Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/Physics/Tests/PhysicsLayers.Tests.asmref.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)