fix: glider response to external forces#9317
Conversation
- ApplyGliding: clamp only fall speed (GravityVelocity.y) instead of the whole vector magnitude - ApplyExternalForce: multiply external force acceleration by GlideWindResponse while gliding - add GlideWindResponse setting (default 1.5) to character controller settings - add ApplyGlidingShould and ApplyExternalForceShould tests
This comment has been minimized.
This comment has been minimized.
|
🔍 Claude reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging. |
|
Warnings count reduced: 26870 => 26830 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ines" This reverts commit 65ecc61.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This comment has been minimized.
This comment has been minimized.
…ableCharacterControllerSettings.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Vitaly Popuzin <35366872+popuz@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
|
PR reviewed and approved by QA on Mac following the PR test instructions. ✅ Build: Test results:
Unrelated errors noted (do not affect verdict):
Note: Player.log does not capture character-motion/physics behavior directly in this alpha build (relevant log categories are suppressed), so log absence of errors is supporting evidence only — the in-game smoke test is the primary confirmation of the fix. Verdict: PENDING ⏳ (Mac PASS ✅ — Windows coverage pending) 08.07.2026_10.03.33_REC.physics.mp4 |
DafGreco
left a comment
There was a problem hiding this comment.
✔️ PR reviewed and approved by QA on windows following instructions playing both happy and un-happy path
Regressions for this ticket had been performed in order to verify that the normal flow is working as expected:
- [✔️ ] Backpack and wearables in world
- [ ✔️] Emotes in world and in backpack
- [ ✔️] Teleport with map/coordinates/Jump In
- [ ✔️] Chat and multiplayer
- [ ✔️] Profile card
- [ ✔️] Skybox
Evidence:
20260708-0811-27.2542213.mp4
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Claude finished @popuz's task in 3m 14s —— View job Todo List
Step 2 — Root-cause checkPASS. Step 3 — Design & integrationPASS. Both The latest commit ( Step 4 — Member auditPASS. Step 5 — Line-level reviewOne blocking issue remains.
No other blocking issues found. Step 6 — ComplexityCOMPLEX. Modifies the character locomotion physics pipeline — Step 7 — QA assessmentQA_REQUIRED: YES. ✅ Already completed — Mac ( Step 8 — Non-blocking warningsNone. REVIEW_RESULT: FAIL ❌ 💰 Review cost: 💰 Review cost: $0.67 · |
# Pull Request Description ## What does this PR change? Fixes SDK **continuous external forces** (`PBPhysicsCombinedForce` — wind tunnels, wind zones) dying the moment any **Portable Experience** is loaded (a global PX or a worn smart wearable), plus a second bug of the same class: unloading any PX/scene killed an active force owned by another scene. **Root cause.** `SDKExternalPhysicsSystems` runs in every scene world. Each instance whose scene is "current" zeroed the shared global `CharacterRigidTransform.ExternalForce` every frame and re-asserted it only from its own world's components. A Portable Experience world is *permanently* current (`PortableExperienceSceneFacade` sets `IsCurrent = true` forever), so the PX instance erased the parcel scene's continuous force every frame and never re-asserted anything (a PX world has no force components of its own). Impulses (jump pads) survived because they accumulate via `+=`. **Fix.** The shared-field write is replaced with **per-scene-world contribution slots** — `CharacterRigidTransform.ExternalForceContributions` (`Dictionary<World, Vector3>`): - each scene world writes/removes **only its own slot**; no world can erase another world's force anymore; - the effective `ExternalForce` is derived every tick in `ApplyExternalForce.Execute` as the **vector sum** of all slots — forces from several current worlds stack (scene wind + PX jetpack is intended physics); - a world unloading (`FinalizeComponents`) or becoming non-current removes only its own contribution; - `/reload` clears the whole map; - the **impulse pipeline is untouched**. Covered by new red-first EditMode regression tests (`SDKExternalPhysicsSystemsShould`, 7 tests — the main repro test was verified failing on the old code) + adapted `ApplyExternalForceShould`. ## Test Instructions ###⚠️ Read first: reproduce the bug BEFORE validating the fix The bug only manifests while a Portable Experience world is running — with no PX loaded every case below trivially "passes" and validates nothing. So the QA pass has two phases: 1. **Phase 1 — repro on a build WITHOUT this fix** (any other recent PR / dev build): confirm a PX is actually running, then confirm the force dies. 2. **Phase 2 — the same steps on THIS PR build**: confirm the force keeps working. **How PX loading works in builds:** The only lever is the environment: the PX list comes from the `alfa-global-portable-experiences` feature flag of the current environment, so launch with `--dclenv org` (production) to get the global PX list (feature flags are environment-specific). ### Prerequisites - [ ] A Portable Experience running: the startup-loaded global PX (launch with `--dclenv org`), a **worn smart wearable** (ideal — a PX world runs on you the whole session), or `/loadpx <name>` / `/killpx <name>` chat commands. - [ ] Test scene with a **continuous external force** (wind tunnel) and a **jump pad** (impulse): launch with `--realm flutterecho.dcl.eth`, or `/goto flutterecho.dcl.eth` from chat. **Steps (standard run)** — verified launch line: ```bash metaforge explorer run 9343 # or launch the downloaded build directly: Decentraland.exe --realm flutterecho.dcl.eth --dclenv org ``` - `--realm flutterecho.dcl.eth` — loads the test world directly (no `/goto` needed) - `--dclenv org` — production environment, whose `alfa-global-portable-experiences` flag carries the global PX list ### Test Steps **Case 0 — repro on an OLD build (phase 1, mandatory):** 1. On any build without this fix (launched with the same args), enter the wind tunnel — the character is pushed. 2. Load any PX (`/loadpx <name>`) or equip a smart wearable. 3. Confirm the push **dies** the moment the PX starts. This is the bug. Now switch to this PR build for all cases below. **Case 1 — continuous force survives a loaded PX (the main fix):** 1. Enter the wind tunnel, confirm the push works. 2. While inside, load a PX (`/loadpx`) — and separately, arrive already wearing a smart wearable. 3. Expected: the push keeps working, no interruption or stutter in either variant. **Case 2 — unloading a PX must not kill a foreign force:** 1. Stand in the wind tunnel with a PX loaded (force working). 2. `/killpx <name>` (or unequip the smart wearable) while standing in the airflow. 3. Expected: the force continues uninterrupted. **Case 3 — jump pads (impulses) unchanged:** 1. Use a jump pad with no PX loaded, then again with a PX loaded and while wearing a smart wearable. 2. Expected: behavior identical to production — same launch strength, no double or missing impulse. **Case 4 — forces from several worlds stack:** 1. Stand in the wind tunnel and activate a PX that applies its own force (e.g. a jetpack smart wearable), if one is available. 2. Expected: the forces combine (vector sum), one does not replace the other; killing the PX removes only its own share, the tunnel keeps pushing. **Case 5 — leaving the scene stops its force:** 1. With a PX still loaded, walk out of the force-emitting parcel to a neighboring parcel. 2. Expected: the scene's push stops on leaving; re-entering restores it. **Case 6 — `/reload`:** 1. Stand in the wind tunnel (force active, PX loaded). 2. Type `/reload`. 3. Expected: the force stops instantly with the reload; after the scene reloads, re-entering the tunnel applies the force again. ### Additional Testing Notes - Run the cases with the PX present in **both** forms: a global PX loaded via `/loadpx` and a **worn smart wearable** — the bug is the same but the world lifecycle differs. - Regression watch: glider response to wind (#9317) must behave as on dev — open the glider inside the tunnel airflow. - When reproducing on the old build the force dies the *same frame* the PX world starts — it is very visible, no need to wait.

Pull Request Description
What does this PR change?
When the player is pushed by scene physics (e.g. a wind turbine using
avatarPhysicsSettings/applyForce) while gliding, the push was very subtle and its direction was distorted compared to being pushed without the glider — while one would expect an open glider to be more responsive to wind, not less.Cause:
ApplyGlidingclamped the magnitude of the wholeGravityVelocityvector toGlideMaxGravity(1 m/s) every physics tick. The clamp predates SDK external forces (#7044), which route the vertical part of a force intoGravityVelocityvia effective gravity — so while gliding the clamp silently erased all wind lift. The horizontal part of the force goes throughExternalVelocityand passed untouched, which is why the push direction bent towards the horizontal.Changes:
ApplyGliding: clamp only the fall speed (GravityVelocity.y = Max(y, -GlideMaxGravity)) instead of the whole vector magnitude — upward velocity from external forces and jump momentum is preserved.ApplyExternalForce: while gliding, the acceleration computed fromExternalForceis multiplied by the newGlideWindResponsesetting (default 1.5, tunable inCharacterControllerSettings) — an open glider catches the airflow with a larger effective area. One-shot impulses (applyImpulse) are intentionally unaffected.ApplyGlidingShould(fall speed clamped, upward velocity preserved),ApplyExternalForceShould(multiplier applied only while gliding).Test Instructions
Steps (standard run):
Expected result: in a scene with a continuous-force zone (wind turbine / fan pushing at an angle), flying into it while gliding pushes the player stronger than without the glider (1.5× by default) and along the force direction, with a clear vertical lift instead of a mostly horizontal drift.
Test Steps
applyImpulse) behave exactly as before, with and without the glider.Quality Checklist
Code Review Reference
Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.
🤖 Generated with Claude Code