Body step 2.3: view rides the body, mantling and climbing work
- view follows the body (deadzone, smoothed, hard 8 cm limit); no drift over six held poses; hanging keeps the view on the character's head - hold-back measured at the hips, capped leg push while held, stiffer balance - sticky legs tuck with free-space check (fixes the mantle shove loop) - strength asymmetric by direction; structural guide strength push-only - limb/torso friction set before AddChild (Box3D reads it only at creation) - new phases: pose hold, hang, human-motion mantle, hand-over-hand wall climb Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6006c5de98
commit
b5f598aae9
6 files changed
+327
-79
No files matched your search
@@ -312,6 +312,11 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
|
|||||||
Rules: low-pass every feed-forward; muscles stay local to the TARGET pose (never the physical parent — rotation
|
Rules: low-pass every feed-forward; muscles stay local to the TARGET pose (never the physical parent — rotation
|
||||||
feedback); no free-path view chasing while grounded; bodies whose shapes can all be disabled need explicit
|
feedback); no free-path view chasing while grounded; bodies whose shapes can all be disabled need explicit
|
||||||
inertia; Box3D shape queries return a null `collider` — use `collider_id`.
|
inertia; Box3D shape queries return a null `collider` — use `collider_id`.
|
||||||
|
**Step 2.3:** the VIEW RIDES THE BODY (deadzone + smoothed follow + hard 8 cm `MaxViewLead`; never detach the view
|
||||||
|
to avoid drift — make the body able to match its pose instead); hold-back measured at the hips; sticky legs tuck
|
||||||
|
with a free-space check; strength asymmetric by direction (pull-down/flexion strong, raise weak; structural guide
|
||||||
|
strength push-only); **Box3D wrapper reads physics-material friction only at shape creation — set
|
||||||
|
`PhysicsMaterialOverride` before AddChild.**
|
||||||
Owner scope: thumbstick locomotion only, no jump, avatar scaling and NPCs later.
|
Owner scope: thumbstick locomotion only, no jump, avatar scaling and NPCs later.
|
||||||
10. **Body physics Stage 2 — PHYSICAL ARMS, implemented 2026-10-02, awaiting owner playtest.**
|
10. **Body physics Stage 2 — PHYSICAL ARMS, implemented 2026-10-02, awaiting owner playtest.**
|
||||||
`PhysicsArms.cs`: per side an upper-arm + forearm RigidBody3D (2.0 / 1.2 kg, layer 5) that
|
`PhysicsArms.cs`: per side an upper-arm + forearm RigidBody3D (2.0 / 1.2 kg, layer 5) that
|
||||||
|
|||||||
+2
-1
@@ -106,7 +106,8 @@ TERMINOLOGY (owner-corrected — use precisely):
|
|||||||
- **RagdollDriver.cs (step 2.2)** — `PoseFilter` (timestamps each new tracking sample, extrapolates between them in
|
- **RagdollDriver.cs (step 2.2)** — `PoseFilter` (timestamps each new tracking sample, extrapolates between them in
|
||||||
play-space-local space; 90 Hz tracking under 144 Hz physics otherwise feeds jerks into every feed-forward) and
|
play-space-local space; 90 Hz tracking under 144 Hz physics otherwise feeds jerks into every feed-forward) and
|
||||||
`RagdollDriver` (filter → OriginCorrection → True Body → build → Step), used by BOTH PlayerRig and RagdollSpike.
|
`RagdollDriver` (filter → OriginCorrection → True Body → build → Step), used by BOTH PlayerRig and RagdollSpike.
|
||||||
The 2.1 instabilities, their root causes and fixes: `research/ragdoll-player-step2.md` "Step 2.2".
|
The 2.1 instabilities, their root causes and fixes: `research/ragdoll-player-step2.md` "Step 2.2"; view
|
||||||
|
alignment / mantle / climbing fixes: "Step 2.3".
|
||||||
- **Playground.cs** — the body sandbox around the table (east climbing/mantling, west physics toys, south
|
- **Playground.cs** — the body sandbox around the table (east climbing/mantling, west physics toys, south
|
||||||
crawl/duck/door); dynamic pieces are "prop"s reset by the menu's Reset props. `tools/playground_shots.tscn` renders
|
crawl/duck/door); dynamic pieces are "prop"s reset by the menu's Reset props. `tools/playground_shots.tscn` renders
|
||||||
overview PNGs (`godot-mono --path . tools/playground_shots.tscn -- <dir>`).
|
overview PNGs (`godot-mono --path . tools/playground_shots.tscn -- <dir>`).
|
||||||
|
|||||||
@@ -131,3 +131,27 @@ RagdollSpike passed all of it, because its input was IDEAL: smooth poses every 1
|
|||||||
|
|
||||||
New spike phases: hold a crouch (eyes 1.0 m) and a deep crouch (0.7 m) without drift (< 5 cm) or spin (< 5°),
|
New spike phases: hold a crouch (eyes 1.0 m) and a deep crouch (0.7 m) without drift (< 5 cm) or spin (< 5°),
|
||||||
eye on camera (< 5 cm). 29 phases total, all on real input.
|
eye on camera (< 5 cm). 29 phases total, all on real input.
|
||||||
|
|
||||||
|
## Step 2.3 — view alignment, mantling, climbing (2026-10-02)
|
||||||
|
|
||||||
|
Owner, after 2.2:
|
||||||
|
- "my head leaving the character's head … losing the alignment when climbing" (screenshots: the camera behind and
|
||||||
|
below the character's own head while on the monkey bars).
|
||||||
|
- Mantling: "legs trying to get off the ground and instantly come back".
|
||||||
|
- Climbing near impossible.
|
||||||
|
- Grabbing world objects and ducking now worked.
|
||||||
|
|
||||||
|
| Change | Why |
|
||||||
|
|---|---|
|
||||||
|
| **The view rides the body.** Deadzone 6 cm (2 cm climbing), smoothed follow τ 0.12 s (0.03 s climbing), and a hard limit `MaxViewLead` 8 cm. Standing, height belongs to the ground follow; airborne or climbing, the follow is 3D. The blocked eye→camera sweep stays | 2.2's "never follow while standing" fixed the drift loop by letting the view leave the body. The drift actually needed a body that can match its pose; with crouch error ~5 mm, a follow with a deadzone doesn't drift (new phase: six held poses, 0.000 m drift, eye within 9 mm) |
|
||||||
|
| **Hold-back measured at the HIPS**, plus capped leg push (`BlockedPush` 150 N) while held, and balance 800 N·m | Measured at the head it was fooled: the body tipped over the knee-high box, so the head wasn't behind the camera. Full leg force bulldozed the body over the box or steep slope |
|
||||||
|
| **Sticky legs tuck.** It tucks while the hands brace and lift (or hold static), and untucks only after 0.3 s unneeded AND a free-space check | The flip-flop re-enabled the collider inside the ledge face every few ticks, shoving the body off: the reported loop |
|
||||||
|
| **Strength by direction (Nimso "asymmetric")**: shoulder raise 35 N·m vs pull-down 100 N·m; elbow extension 45 vs flexion 70; caps chosen per tick from the commanded direction. Structural hand-guide strength is PUSH-only (bone in compression) | Pull-ups and climbing are muscle work. 12 kg at arm's length sags 49 cm (close: 1.4 cm). Pull-up: two hands 0.74 m, one hand 0.16 m. Mantle: two ✓, one ✗. Wall climb hand over hand: 0.98 m |
|
||||||
|
| **Limb friction 0.25 / torso 0.3, set BEFORE AddChild** | Box3D wrapper quirk: friction is read only when shapes are created. A later `PhysicsMaterialOverride` silently does nothing, so 2.1's torso friction never applied. Hands brushing a wall were pinned by friction and their own arm's pressure |
|
||||||
|
| New phases: held poses without drift, hanging keeps the view on the body, the human-motion mantle (crouch, hands on the edge, stand and press), hand-over-hand wall climb. The pull-up grabs when the hands actually reach the bar | Tests now act like a person, on 90 Hz input with jitter |
|
||||||
|
|
||||||
|
Rules added:
|
||||||
|
- The view must stay on the body; prevent drift by making the body able to match its pose, not by detaching the view.
|
||||||
|
- Measure "blocked" where the body is blocked.
|
||||||
|
- Hysteresis plus a free-space check before re-enabling any collider.
|
||||||
|
- In the Box3D wrapper, physics materials must be set before AddChild.
|
||||||
+107
-52
@@ -26,8 +26,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// Muscles: PD gain (1/s), max commanded spin (rad/s), torque cap (N·m).
|
// Muscles: PD gain (1/s), max commanded spin (rad/s), torque cap (N·m).
|
||||||
public const float SpineKp = 15f, SpineTorque = 400f;
|
public const float SpineKp = 15f, SpineTorque = 400f;
|
||||||
public const float NeckKp = 20f, NeckTorque = 60f;
|
public const float NeckKp = 20f, NeckTorque = 60f;
|
||||||
public const float ShoulderKp = 20f, ShoulderTorque = 45f; // ~human shoulder flexion strength
|
// Asymmetric by direction (Nimso; human strength tables): pulling the arm DOWN (lats/back —
|
||||||
public const float ElbowKp = 20f, ElbowTorque = 45f;
|
// pull-ups, hanging, climbing) is roughly twice as strong as raising it (front delts — a weight
|
||||||
|
// held out front), and elbow flexion (biceps) beats extension.
|
||||||
|
public const float ShoulderKp = 20f, ShoulderTorque = 35f, ShoulderPullTorque = 100f;
|
||||||
|
public const float ElbowKp = 20f, ElbowTorque = 45f, ElbowFlexTorque = 70f;
|
||||||
public const float MuscleMaxSpin = 15f;
|
public const float MuscleMaxSpin = 15f;
|
||||||
// Feed-forwards are derivatives of tracking: low-pass them or tracking jitter (±1–2 mm per
|
// Feed-forwards are derivatives of tracking: low-pass them or tracking jitter (±1–2 mm per
|
||||||
// sample) becomes hundreds of newtons of buzz (RagdollSpike REAL input mode).
|
// sample) becomes hundreds of newtons of buzz (RagdollSpike REAL input mode).
|
||||||
@@ -55,25 +58,31 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public const float MaxStepHeight = 0.35f;
|
public const float MaxStepHeight = 0.35f;
|
||||||
public const float MaxSlopeDegrees = 35f; // steeper ground gives no support (Nimso: "slopes beyond an angle can't be run up") // higher ground under the hips isn't stood on unless the body got up there
|
public const float MaxSlopeDegrees = 35f; // steeper ground gives no support (Nimso: "slopes beyond an angle can't be run up") // higher ground under the hips isn't stood on unless the body got up there
|
||||||
public const float BracedAirControl = 150f; // N of horizontal hip carry while airborne but braced on the hands (mantling)
|
public const float BracedAirControl = 150f; // N of horizontal hip carry while airborne but braced on the hands (mantling)
|
||||||
public const float CrawlCarry = 300f; // N: the torso lies on the floor in a crawl; knees/hands would push (not simulated)
|
public const float CrawlCarry = 300f;
|
||||||
|
public const float BlockedPush = 150f; // N horizontal leg push while the hips are held back // N: the torso lies on the floor in a crawl; knees/hands would push (not simulated)
|
||||||
public const float TorsoFriction = 0.3f; // clothing on floor: lets a crawl slide
|
public const float TorsoFriction = 0.3f; // clothing on floor: lets a crawl slide
|
||||||
|
public const float LimbFriction = 0.25f;
|
||||||
// Lower-body collider: a box from the hips down to MaxStepHeight above the feet, so knee-high
|
// Lower-body collider: a box from the hips down to MaxStepHeight above the feet, so knee-high
|
||||||
// obstacles block walking (no walking over tables) while lower steps pass under it. It slides
|
// obstacles block walking (no walking over tables) while lower steps pass under it. It slides
|
||||||
// up into the torso when crouching and switches off when crawling (Nimso: legs don't snag).
|
// up into the torso when crouching and switches off when crawling (Nimso: legs don't snag).
|
||||||
public const float LegsMass = 6f;
|
public const float LegsMass = 6f;
|
||||||
public const float LegsKp = 30f, LegsForce = 3000f, LegsTorque = 400f;
|
public const float LegsKp = 30f, LegsForce = 3000f, LegsTorque = 400f;
|
||||||
public const float LegsOffAbove = 0.25f; // collider disabled once slid this far above the hips
|
public const float LegsOffAbove = 0.25f; // collider disabled once slid this far above the hips
|
||||||
public const float MantleLift = 0.08f; // braced + hips this far above the support goal = being lifted (legs tuck)
|
public const float MantleLift = 0.03f; // braced + hips this far above the support goal = being lifted (legs tuck)
|
||||||
|
public const float UntuckDelay = 0.3f; // s the tuck must be unneeded before the collider returns
|
||||||
// Balance: an implicit torque-capped motor from a world anchor to the pelvis (explicit torques
|
// Balance: an implicit torque-capped motor from a world anchor to the pelvis (explicit torques
|
||||||
// let the arms' reaction torques set the whole trunk swinging).
|
// let the arms' reaction torques set the whole trunk swinging).
|
||||||
public const float BalanceKp = 10f;
|
public const float BalanceKp = 10f;
|
||||||
public const float BalanceTorque = 400f;
|
public const float BalanceTorque = 800f;
|
||||||
public const float AirBalanceShare = 0.3f;
|
public const float AirBalanceShare = 0.3f;
|
||||||
// Play-space reconciliation.
|
// Play-space reconciliation.
|
||||||
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
|
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
|
||||||
public const float FreeSlack = 0.2f; // camera leading the body's eye by more than this = body held back: stick stops pushing that way
|
public const float FreeSlack = 0.12f; // camera leading the body's eye by more than this = body held back: stick stops pushing that way
|
||||||
public const float AirSlack = 0.05f; // climbing/hanging (braced): the view rides the body tightly
|
public const float ViewDeadzone = 0.06f; // m the head may lead the body before the view is eased back onto it
|
||||||
public const float FallSlack = 0.25f; // free airborne: brief ground loss (sliding off a steep slope) mustn't sink the view
|
public const float ViewFollowTau = 0.12f; // s
|
||||||
|
public const float ClimbViewDeadzone = 0.02f; // hanging/mantling: the view rides the body tightly
|
||||||
|
public const float ClimbViewFollowTau = 0.03f;
|
||||||
|
public const float MaxViewLead = 0.08f;
|
||||||
public const float GroundFollowRate = 8f;
|
public const float GroundFollowRate = 8f;
|
||||||
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
|
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
|
||||||
// Geometry.
|
// Geometry.
|
||||||
@@ -123,6 +132,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public float PrevFlexTarget = -1f;
|
public float PrevFlexTarget = -1f;
|
||||||
public float FlexFf;
|
public float FlexFf;
|
||||||
public float GuideCap = HandGuideForce;
|
public float GuideCap = HandGuideForce;
|
||||||
|
public float ShoulderCap = ShoulderTorque, ElbowCap = ElbowTorque;
|
||||||
public PhysicsBody3D Held;
|
public PhysicsBody3D Held;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +143,8 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
public bool Grounded { get; private set; }
|
public bool Grounded { get; private set; }
|
||||||
public float GroundY => _footY;
|
public float GroundY => _footY;
|
||||||
public bool Braced { get; private set; } // a hand touches or holds static world
|
public bool Braced { get; private set; } // a hand touches or holds static world
|
||||||
|
public bool LegsSolid => _legsSolid;
|
||||||
|
public bool Lifted => _lifted;
|
||||||
// Horizontal direction in which the body is held back from the camera (zero if not): the stick
|
// Horizontal direction in which the body is held back from the camera (zero if not): the stick
|
||||||
// must not keep moving the play space that way.
|
// must not keep moving the play space that way.
|
||||||
public Vector3 HeldBack { get; private set; }
|
public Vector3 HeldBack { get; private set; }
|
||||||
@@ -154,6 +166,15 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
|
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
|
||||||
private float _footY;
|
private float _footY;
|
||||||
private float _supportGoalY;
|
private float _supportGoalY;
|
||||||
|
private float _tuckFree, _dt;
|
||||||
|
private PhysicsShapeQueryParameters3D _legsQuery;
|
||||||
|
|
||||||
|
private bool LegsSpaceFree(Vector3 center, Basis basis)
|
||||||
|
{
|
||||||
|
_legsQuery ??= new PhysicsShapeQueryParameters3D { Shape = Legs.GetChild<CollisionShape3D>(0).Shape, CollisionMask = WorldMask };
|
||||||
|
_legsQuery.Transform = new Transform3D(basis, center);
|
||||||
|
return GetWorld3D().DirectSpaceState.IntersectShape(_legsQuery, 1).Count == 0;
|
||||||
|
}
|
||||||
private Vector3 _goalVelocity;
|
private Vector3 _goalVelocity;
|
||||||
|
|
||||||
private static float LowPass(float dt, float tau) => 1f - Mathf.Exp(-dt / tau);
|
private static float LowPass(float dt, float tau) => 1f - Mathf.Exp(-dt / tau);
|
||||||
@@ -206,7 +227,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
|
|
||||||
Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f);
|
Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f);
|
||||||
Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants,
|
Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants,
|
||||||
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal));
|
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal), friction: TorsoFriction);
|
||||||
float standingHip = pose.PelvisTarget.Y - (pose.Eye.Y - p.EyeHeight);
|
float standingHip = pose.PelvisTarget.Y - (pose.Eye.Y - p.EyeHeight);
|
||||||
_legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight);
|
_legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight);
|
||||||
Legs = MakeBody("Legs", new BoxShape3D { Size = new Vector3(0.28f * s, _legsHeight, 0.2f * s) }, LegsMass, BodyLayer, WorldMask, pants,
|
Legs = MakeBody("Legs", new BoxShape3D { Size = new Vector3(0.28f * s, _legsHeight, 0.2f * s) }, LegsMass, BodyLayer, WorldMask, pants,
|
||||||
@@ -217,13 +238,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Legs.Inertia = new Vector3(_legsHeight * _legsHeight + 0.04f, 0.28f * 0.28f + 0.04f, _legsHeight * _legsHeight + 0.08f) * (LegsMass / 12f);
|
Legs.Inertia = new Vector3(_legsHeight * _legsHeight + 0.04f, 0.28f * 0.28f + 0.04f, _legsHeight * _legsHeight + 0.08f) * (LegsMass / 12f);
|
||||||
_footY = pose.Eye.Y - p.EyeHeight;
|
_footY = pose.Eye.Y - p.EyeHeight;
|
||||||
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
|
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
|
||||||
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal));
|
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal), friction: TorsoFriction);
|
||||||
Head = MakeBody("Head", new BoxShape3D { Size = HeadSize * s }, 0.081f * m, BodyLayer, WorldMask, skin,
|
Head = MakeBody("Head", new BoxShape3D { Size = HeadSize * s }, 0.081f * m, BodyLayer, WorldMask, skin,
|
||||||
c * new Transform3D(Basis.Identity, neckPivot + headCenterFromPivot));
|
c * new Transform3D(Basis.Identity, neckPivot + headCenterFromPivot), friction: TorsoFriction);
|
||||||
// The neck: a box hanging under the head, same body.
|
// The neck: a box hanging under the head, same body.
|
||||||
Vector3 neckCenter = (neckBase + neckPivot) * 0.5f - (neckPivot + headCenterFromPivot);
|
Vector3 neckCenter = (neckBase + neckPivot) * 0.5f - (neckPivot + headCenterFromPivot);
|
||||||
foreach (RigidBody3D torso in new[] { Pelvis, Chest, Head, Legs })
|
|
||||||
torso.PhysicsMaterialOverride = new PhysicsMaterial { Friction = TorsoFriction };
|
|
||||||
Head.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter });
|
Head.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter });
|
||||||
MeshInstance3D neckMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter };
|
MeshInstance3D neckMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter };
|
||||||
neckMesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = skin, Roughness = 0.6f });
|
neckMesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = skin, Roughness = 0.6f });
|
||||||
@@ -263,11 +282,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
string n = side == 0 ? "Left" : "Right";
|
string n = side == 0 ? "Left" : "Right";
|
||||||
|
|
||||||
a.Upper = MakeBody(n + "UpperArm", new BoxShape3D { Size = new Vector3(0.085f * s, a.UpperLen, 0.085f * s) }, 0.028f * m, ArmLayer, WorldMask, shirt,
|
a.Upper = MakeBody(n + "UpperArm", new BoxShape3D { Size = new Vector3(0.085f * s, a.UpperLen, 0.085f * s) }, 0.028f * m, ArmLayer, WorldMask, shirt,
|
||||||
c * new Transform3D(bone, shoulder + dir * (a.UpperLen * 0.5f)));
|
c * new Transform3D(bone, shoulder + dir * (a.UpperLen * 0.5f)), friction: LimbFriction);
|
||||||
a.Fore = MakeBody(n + "Forearm", new BoxShape3D { Size = new Vector3(0.07f * s, a.ForeLen - 0.01f, 0.07f * s) }, 0.016f * m, ArmLayer, WorldMask, skin,
|
a.Fore = MakeBody(n + "Forearm", new BoxShape3D { Size = new Vector3(0.07f * s, a.ForeLen - 0.01f, 0.07f * s) }, 0.016f * m, ArmLayer, WorldMask, skin,
|
||||||
c * new Transform3D(bone, elbow + dir * (a.ForeLen * 0.5f)));
|
c * new Transform3D(bone, elbow + dir * (a.ForeLen * 0.5f)), friction: LimbFriction);
|
||||||
a.Hand = MakeBody(n + "Hand", new BoxShape3D { Size = HandSize }, 0.006f * m, HandLayer, WorldMask | HandLayer,
|
a.Hand = MakeBody(n + "Hand", new BoxShape3D { Size = HandSize }, 0.006f * m, HandLayer, WorldMask | HandLayer,
|
||||||
side == 0 ? new Color(0.9f, 0.3f, 0.35f) : new Color(0.3f, 0.5f, 0.9f), c * new Transform3D(handBasis, handOrigin));
|
side == 0 ? new Color(0.9f, 0.3f, 0.35f) : new Color(0.3f, 0.5f, 0.9f), c * new Transform3D(handBasis, handOrigin), friction: LimbFriction);
|
||||||
a.Hand.ContinuousCd = true;
|
a.Hand.ContinuousCd = true;
|
||||||
a.Hand.ContactMonitor = true;
|
a.Hand.ContactMonitor = true;
|
||||||
a.Hand.MaxContactsReported = 8;
|
a.Hand.MaxContactsReported = 8;
|
||||||
@@ -315,9 +334,12 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
return new Basis(x, boneDir, x.Cross(boneDir));
|
return new Basis(x, boneDir, x.Cross(boneDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true)
|
// NOTE (Box3D wrapper): friction is read only when the body's shapes are created — the material
|
||||||
|
// must be set before AddChild (setting it later silently does nothing).
|
||||||
|
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true, float friction = 1f)
|
||||||
{
|
{
|
||||||
RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at, ContactMonitor = true, MaxContactsReported = 4 };
|
RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at, ContactMonitor = true, MaxContactsReported = 4,
|
||||||
|
PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction } };
|
||||||
body.AddChild(new CollisionShape3D { Shape = shape });
|
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||||
if (shape is BoxShape3D box && layer != 0 && visible)
|
if (shape is BoxShape3D box && layer != 0 && visible)
|
||||||
{
|
{
|
||||||
@@ -477,6 +499,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
return;
|
return;
|
||||||
|
_dt = dt;
|
||||||
ComputeTargets(s, left, right, physicalArms: true, dt);
|
ComputeTargets(s, left, right, physicalArms: true, dt);
|
||||||
foreach (RigidBody3D b in _bodies)
|
foreach (RigidBody3D b in _bodies)
|
||||||
{
|
{
|
||||||
@@ -565,6 +588,12 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
a.PrevFlexTarget = target;
|
a.PrevFlexTarget = target;
|
||||||
float w = Mathf.Clamp(a.FlexFf + (target - ElbowFlex(side)) * ElbowKp, -MuscleMaxSpin, MuscleMaxSpin);
|
float w = Mathf.Clamp(a.FlexFf + (target - ElbowFlex(side)) * ElbowKp, -MuscleMaxSpin, MuscleMaxSpin);
|
||||||
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
|
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
|
||||||
|
float cap = (w > 0f ? ElbowFlexTorque : ElbowTorque) * Mathf.Max(0.05f, Strength);
|
||||||
|
if (Mathf.Abs(cap - a.ElbowCap) > 0.5f)
|
||||||
|
{
|
||||||
|
a.ElbowCap = cap;
|
||||||
|
a.Elbow.SetParam(HingeJoint3D.Param.MotorMaxImpulse, cap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyStrength()
|
private void ApplyStrength()
|
||||||
@@ -576,6 +605,8 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
foreach (Arm a in _arms)
|
foreach (Arm a in _arms)
|
||||||
{
|
{
|
||||||
SetAngularCap(a.Shoulder.Joint, ShoulderTorque * k);
|
SetAngularCap(a.Shoulder.Joint, ShoulderTorque * k);
|
||||||
|
a.ShoulderCap = ShoulderTorque * k;
|
||||||
|
a.ElbowCap = ElbowTorque * k;
|
||||||
SetAngularCap(a.Guide.Joint, HandGuideTorque * k);
|
SetAngularCap(a.Guide.Joint, HandGuideTorque * k);
|
||||||
a.Elbow.SetParam(HingeJoint3D.Param.MotorMaxImpulse, ElbowTorque * k);
|
a.Elbow.SetParam(HingeJoint3D.Param.MotorMaxImpulse, ElbowTorque * k);
|
||||||
}
|
}
|
||||||
@@ -614,9 +645,18 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Vector3 hip = pelvis * (-_pelvisCenterLocal);
|
Vector3 hip = pelvis * (-_pelvisCenterLocal);
|
||||||
float minCenter = _footY + MaxStepHeight + _legsHeight * 0.5f;
|
float minCenter = _footY + MaxStepHeight + _legsHeight * 0.5f;
|
||||||
float center = Mathf.Max(hip.Y - _legsHeight * 0.5f, minCenter);
|
float center = Mathf.Max(hip.Y - _legsHeight * 0.5f, minCenter);
|
||||||
// Tucked: deep crouch/crawl, or being lifted by the arms (mantling — the knee comes up).
|
// Tuck (collider off, folded into the pelvis): deep crouch/crawl, or the hands bracing to
|
||||||
_lifted = Braced && Pelvis.GlobalPosition.Y > _supportGoalY + MantleLift;
|
// lift the body (mantle, climbing — the knees come up). It STAYS tucked until it has been
|
||||||
bool solid = center + _legsHeight * 0.5f < hip.Y + LegsOffAbove && !_lifted;
|
// unneeded for a while AND its untucked spot is free: re-enabling inside a ledge face is
|
||||||
|
// what shoved the body off again and again (owner 2.2 "legs trying to get off the
|
||||||
|
// ground and instantly coming back").
|
||||||
|
bool climbingHold = _arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D;
|
||||||
|
_lifted = Braced && (Pelvis.GlobalPosition.Y > _supportGoalY + MantleLift || climbingHold);
|
||||||
|
bool deep = center + _legsHeight * 0.5f >= hip.Y + LegsOffAbove;
|
||||||
|
bool wantTuck = deep || _lifted || (!_legsSolid && Braced);
|
||||||
|
_tuckFree = wantTuck ? 0f : _tuckFree + _dt;
|
||||||
|
Vector3 untucked = new(hip.X, center, hip.Z);
|
||||||
|
bool solid = _legsSolid ? !wantTuck : _tuckFree > UntuckDelay && LegsSpaceFree(untucked, s.YawBasis);
|
||||||
if (solid != _legsSolid)
|
if (solid != _legsSolid)
|
||||||
{
|
{
|
||||||
_legsSolid = solid;
|
_legsSolid = solid;
|
||||||
@@ -654,7 +694,17 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
d.Ff = d.Ff.Lerp(raw, LowPass(dt, MuscleFeedForwardTau));
|
d.Ff = d.Ff.Lerp(raw, LowPass(dt, MuscleFeedForwardTau));
|
||||||
d.PrevTarget = target;
|
d.PrevTarget = target;
|
||||||
d.HasPrev = true;
|
d.HasPrev = true;
|
||||||
SetAngularVelocity(d.Joint, (d.Ff + BodyMath.RotationError(target, current) * ShoulderKp).LimitLength(MuscleMaxSpin));
|
Vector3 spin = (d.Ff + BodyMath.RotationError(target, current) * ShoulderKp).LimitLength(MuscleMaxSpin);
|
||||||
|
SetAngularVelocity(d.Joint, spin);
|
||||||
|
// Which way is the muscle moving the arm? Rotating the bone direction downward = pulling.
|
||||||
|
Vector3 bone = (a.Upper.GlobalTransform.Basis.Y).Normalized();
|
||||||
|
Vector3 boneMotion = (frameA * spin).Cross(bone);
|
||||||
|
float cap = (boneMotion.Y < 0f ? ShoulderPullTorque : ShoulderTorque) * Mathf.Max(0.05f, Strength);
|
||||||
|
if (Mathf.Abs(cap - a.ShoulderCap) > 0.5f)
|
||||||
|
{
|
||||||
|
a.ShoulderCap = cap;
|
||||||
|
SetAngularCap(d.Joint, cap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force-capped pull of the hand onto the controller, reacting into the chest (arm muscles).
|
// Force-capped pull of the hand onto the controller, reacting into the chest (arm muscles).
|
||||||
@@ -670,7 +720,10 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Vector3 axis = a.Hand.GlobalPosition - shoulder;
|
Vector3 axis = a.Hand.GlobalPosition - shoulder;
|
||||||
// Axial share of the error, with a 3 cm floor so a nearly-reached target's noisy error
|
// Axial share of the error, with a 3 cm floor so a nearly-reached target's noisy error
|
||||||
// direction can't flip a held load into "structural".
|
// direction can't flip a held load into "structural".
|
||||||
float align = axis.LengthSquared() > 1e-8f ? Mathf.Abs(err.Dot(axis.Normalized())) / (err.Length() + StructuralDeadband) : 0f;
|
// PUSH only (the hand wants to go further from the shoulder — straight arms pressing on a
|
||||||
|
// ledge): bone in compression. Pulling (shortening the arm: pull-ups, climbing) is muscle
|
||||||
|
// work — the asymmetric shoulder/elbow muscles do it, and one arm can't lift the body.
|
||||||
|
float align = axis.LengthSquared() > 1e-8f ? Mathf.Max(0f, err.Dot(axis.Normalized())) / (err.Length() + StructuralDeadband) : 0f;
|
||||||
// Structure only helps against something: a hand in free air reaching past its arm's
|
// Structure only helps against something: a hand in free air reaching past its arm's
|
||||||
// length must not haul the whole body after it.
|
// length must not haul the whole body after it.
|
||||||
if (a.Held == null && a.Hand.GetContactCount() == 0)
|
if (a.Held == null && a.Hand.GetContactCount() == 0)
|
||||||
@@ -720,7 +773,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// pelvis centre hanging over the lip).
|
// pelvis centre hanging over the lip).
|
||||||
bool clear = point.Y <= _footY + MaxStepHeight || GroundBehindAt(pos, point.Y);
|
bool clear = point.Y <= _footY + MaxStepHeight || GroundBehindAt(pos, point.Y);
|
||||||
bool reachable = point.Y <= _footY + MaxStepHeight
|
bool reachable = point.Y <= _footY + MaxStepHeight
|
||||||
|| ((!wasGrounded || !_legsSolid || pos.Y - point.Y > 0.6f * hipAboveFloor) && clear);
|
|| ((!wasGrounded || _lifted || pos.Y - point.Y > 0.6f * hipAboveFloor) && clear);
|
||||||
Grounded = true;
|
Grounded = true;
|
||||||
if (reachable)
|
if (reachable)
|
||||||
_footY = point.Y;
|
_footY = point.Y;
|
||||||
@@ -737,6 +790,11 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
if (rawVelocity.Length() > 10f)
|
if (rawVelocity.Length() > 10f)
|
||||||
rawVelocity = Vector3.Zero;
|
rawVelocity = Vector3.Zero;
|
||||||
_goalVelocity = _goalVelocity.Lerp(rawVelocity, LowPass(dt, GroundFeedForwardTau));
|
_goalVelocity = _goalVelocity.Lerp(rawVelocity, LowPass(dt, GroundFeedForwardTau));
|
||||||
|
// Held back = the hips can't get to where the pose wants them (knee-high box, table edge,
|
||||||
|
// wall). Measured at the hips, not the head: the body tips over an obstacle, so the head
|
||||||
|
// may be right where the camera is while the hips are stuck.
|
||||||
|
Vector3 hipLag = new(target.Origin.X - pos.X, 0f, target.Origin.Z - pos.Z);
|
||||||
|
HeldBack = Grounded && hipLag.Length() > FreeSlack ? hipLag.Normalized() : Vector3.Zero;
|
||||||
Vector3 targetVelocity = _goalVelocity;
|
Vector3 targetVelocity = _goalVelocity;
|
||||||
|
|
||||||
float weight = BodyMass * 9.8f;
|
float weight = BodyMass * 9.8f;
|
||||||
@@ -750,6 +808,10 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Vector3 force = accel * BodyMass;
|
Vector3 force = accel * BodyMass;
|
||||||
float support = Mathf.Clamp(force.Y + weight, 0f, LegStrength * weight);
|
float support = Mathf.Clamp(force.Y + weight, 0f, LegStrength * weight);
|
||||||
float carry = Mathf.Max(GroundFriction * support, _legsSolid ? 0f : CrawlCarry);
|
float carry = Mathf.Max(GroundFriction * support, _legsSolid ? 0f : CrawlCarry);
|
||||||
|
// Hips held back by an obstacle: lean on it, don't bulldoze — full leg force tipped the
|
||||||
|
// body over knee-high boxes and steep slopes (and the riding view tipped with it).
|
||||||
|
if (HeldBack != Vector3.Zero)
|
||||||
|
carry = Mathf.Min(carry, BlockedPush);
|
||||||
Vector3 horizontal = new Vector3(force.X, 0f, force.Z).LimitLength(carry);
|
Vector3 horizontal = new Vector3(force.X, 0f, force.Z).LimitLength(carry);
|
||||||
Pelvis.ApplyCentralForce(horizontal + Vector3.Up * support);
|
Pelvis.ApplyCentralForce(horizontal + Vector3.Up * support);
|
||||||
}
|
}
|
||||||
@@ -782,12 +844,13 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- play space
|
// ---------------------------------------------------------------- play space
|
||||||
|
|
||||||
// How the driver should move its play-space origin after the last physics step (Bonelab-style
|
// How the driver should move its play-space origin after the last physics step. The view RIDES
|
||||||
// push-back): a sphere swept from the head body's eye to the camera — if geometry is in the way
|
// THE BODY: beyond a small deadzone (normal head motion leads the body a little) the play space
|
||||||
// the camera is somewhere the head can't follow (a wall, a bar, a table it walked into), so the
|
// eases so the camera sits on the character's eye — hanging, mantling, being shoved, standing up
|
||||||
// view is put back at the last free point. With nothing in between, the head may lag or be
|
// under a table. On top, a sphere swept eye→camera puts the view straight back when geometry
|
||||||
// held up to FreeSlack (leaning over a table with the hips blocked) before the view follows.
|
// is in the way (no peeking through walls). The floor follows the ground under the body.
|
||||||
// Plus: the floor follows the ground under the body; airborne, the view rides the body (falls).
|
// Requirement for this not to drift: the body must be able to match the pose (RagdollSpike
|
||||||
|
// "holds poses without drift"); 2.2's "never follow while standing" left the view behind the body.
|
||||||
public Vector3 OriginCorrection(Vector3 camera, float floorY, float dt)
|
public Vector3 OriginCorrection(Vector3 camera, float floorY, float dt)
|
||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
@@ -803,32 +866,24 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
float[] fractions = GetWorld3D().DirectSpaceState.CastMotion(_cameraQuery);
|
float[] fractions = GetWorld3D().DirectSpaceState.CastMotion(_cameraQuery);
|
||||||
float safe = fractions.Length > 0 ? fractions[0] : 1f;
|
float safe = fractions.Length > 0 ? fractions[0] : 1f;
|
||||||
if (safe < 1f)
|
if (safe < 1f)
|
||||||
{
|
|
||||||
correction = -lead * (1f - safe);
|
correction = -lead * (1f - safe);
|
||||||
// Only a climbing body drags the view vertically through geometry; otherwise a
|
Vector3 rest = lead + correction;
|
||||||
// camera-to-eye line grazing a slope would sink the view (and with it the pose).
|
if (Grounded && !Climbing)
|
||||||
if (!Climbing)
|
rest.Y = 0f; // standing: height belongs to the ground follow (and the blocked sweep)
|
||||||
correction.Y = 0f;
|
float deadzone = Climbing ? ClimbViewDeadzone : ViewDeadzone;
|
||||||
}
|
float tau = Climbing ? ClimbViewFollowTau : ViewFollowTau;
|
||||||
else if (!Grounded)
|
if (rest.Length() > deadzone)
|
||||||
{
|
correction -= rest * (1f - deadzone / rest.Length()) * (1f - Mathf.Exp(-dt / tau));
|
||||||
// Free path: the view never chases the body while standing — any pose mismatch
|
// Hard limit: the view is never more than MaxViewLead off the character's head (a
|
||||||
// (a crouch the body can't match exactly) would otherwise walk the play space away
|
// smoothed follow always trails a sudden drop — letting go with one hand, a fall).
|
||||||
// forever (body → view → pose target → body; owner 2.1 "sliding around").
|
rest = lead + correction;
|
||||||
// Airborne it rides the body: vertically in a fall, fully while climbing.
|
if (Grounded && !Climbing)
|
||||||
Vector3 follow = Climbing ? lead : new Vector3(0f, lead.Y, 0f);
|
rest.Y = 0f;
|
||||||
float slack = Climbing ? AirSlack : FallSlack;
|
if (rest.Length() > MaxViewLead)
|
||||||
if (follow.Length() > slack)
|
correction -= rest * (1f - MaxViewLead / rest.Length());
|
||||||
correction = -follow * (1f - slack / follow.Length());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Vector3 flatLead = new(lead.X, 0f, lead.Z);
|
|
||||||
HeldBack = flatLead.Length() > FreeSlack ? flatLead.Normalized() : Vector3.Zero;
|
|
||||||
if (Grounded)
|
if (Grounded)
|
||||||
{
|
|
||||||
correction.Y = 0f;
|
|
||||||
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
|
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
|
||||||
}
|
|
||||||
// The pose target moves with the play space: a correction is not intended motion, so it
|
// The pose target moves with the play space: a correction is not intended motion, so it
|
||||||
// must not show up in ground control's feed-forward (that loop ran away at 7 m/s).
|
// must not show up in ground control's feed-forward (that loop ran away at 7 m/s).
|
||||||
_pelvisTargetPrev += correction;
|
_pelvisTargetPrev += correction;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cab8fhj2rtbpg
|
||||||
+188
-26
@@ -94,13 +94,16 @@ public partial class RagdollSpike : Node3D
|
|||||||
_phases.Add(("a knee-high box blocks walking (no stepping onto it)", () => SetupObstacle(0.6f), _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
_phases.Add(("a knee-high box blocks walking (no stepping onto it)", () => SetupObstacle(0.6f), _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
||||||
_phases.Add(("a table can't be walked over", SetupTableObstacle, _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
_phases.Add(("a table can't be walked over", SetupTableObstacle, _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
|
||||||
_phases.Add(("walks up a 0.25 m step", () => SetupObstacle(0.25f, 8f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 3f, FinishStep));
|
_phases.Add(("walks up a 0.25 m step", () => SetupObstacle(0.25f, 8f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 3f, FinishStep));
|
||||||
_phases.Add(("two-handed mantle onto a 0.9 m ledge", () => SetupObstacle(0.9f), t => TickMantle(both: true), 6f, () => FinishMantle(true)));
|
_phases.Add(("two-handed mantle onto a 0.9 m ledge (crouch, hands on the edge, stand and press)", () => SetupObstacle(0.9f), _ => TickHumanMantle(both: true), 7f, () => FinishMantle(true)));
|
||||||
_phases.Add(("one hand can't mantle", () => SetupObstacle(0.9f), t => TickMantle(both: false), 6f, () => FinishMantle(false)));
|
_phases.Add(("one hand can't mantle", () => SetupObstacle(0.9f), _ => TickHumanMantle(both: false), 7f, () => FinishMantle(false)));
|
||||||
_phases.Add(("two-handed pull-up on a grabbed bar", () => SetupBarHang(), t => TickPullUp(both: true), 3.5f, () => FinishPullUp(true)));
|
_phases.Add(("two-handed pull-up on a grabbed bar", () => SetupBarHang(), t => TickPullUp(both: true), 3.5f, () => FinishPullUp(true)));
|
||||||
_phases.Add(("one-handed pull-up is weaker", () => SetupBarHang(), t => TickPullUp(both: false), 3.5f, () => FinishPullUp(false)));
|
_phases.Add(("one-handed pull-up is weaker", () => SetupBarHang(), t => TickPullUp(both: false), 3.5f, () => FinishPullUp(false)));
|
||||||
_phases.Add(("crawls through a 0.6 m tunnel", SetupTunnel, TickCrawl, 7f, FinishCrawl));
|
_phases.Add(("crawls through a 0.6 m tunnel", SetupTunnel, TickCrawl, 7f, FinishCrawl));
|
||||||
_phases.Add(("holds a crouch (eyes 1.0 m) without sliding or spinning", Rest, t => TickCrouchHold(1.0f), 5f, FinishCrouchHold));
|
_phases.Add(("holds a crouch (eyes 1.0 m) without sliding or spinning", Rest, t => TickCrouchHold(1.0f), 5f, FinishCrouchHold));
|
||||||
_phases.Add(("holds a deep crouch (eyes 0.7 m) without sliding or spinning", Rest, t => TickCrouchHold(0.7f), 5f, FinishCrouchHold));
|
_phases.Add(("holds a deep crouch (eyes 0.7 m) without sliding or spinning", Rest, t => TickCrouchHold(0.7f), 5f, FinishCrouchHold));
|
||||||
|
_phases.Add(("holds poses (stand, crouch, deep crouch, lean, reach, kneel) without view/body drift", Rest, TickPoseHold, 18f, FinishPoseHold));
|
||||||
|
_phases.Add(("hanging from a bar keeps the view on the body", () => SetupBarHang(), TickHang, 4f, FinishHang));
|
||||||
|
_phases.Add(("climbs a wall hand over hand (grab, pull, regrab higher)", SetupClimbWall, TickClimbWall, 6.5f, FinishClimbWall));
|
||||||
_phases.Add(("walks off a 1 m ledge and lands", SetupLedgeTop, _ => _stickVelocity = _t > 0.5f && _t < 2f ? new Vector3(0f, 0f, -1.2f) : Vector3.Zero, 4f, FinishDrop));
|
_phases.Add(("walks off a 1 m ledge and lands", SetupLedgeTop, _ => _stickVelocity = _t > 0.5f && _t < 2f ? new Vector3(0f, 0f, -1.2f) : Vector3.Zero, 4f, FinishDrop));
|
||||||
_phases.Add(("walks up an 18° ramp", () => SetupRamp(18f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(true)));
|
_phases.Add(("walks up an 18° ramp", () => SetupRamp(18f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(true)));
|
||||||
_phases.Add(("can't walk up a 40° ramp", () => SetupRamp(40f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(false)));
|
_phases.Add(("can't walk up a 40° ramp", () => SetupRamp(40f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(false)));
|
||||||
@@ -540,35 +543,13 @@ public partial class RagdollSpike : Node3D
|
|||||||
|
|
||||||
// Put both hands on the ledge, press down (controllers sink as the real arms straighten), then
|
// Put both hands on the ledge, press down (controllers sink as the real arms straighten), then
|
||||||
// walk forward onto it.
|
// walk forward onto it.
|
||||||
// Walk up to the ledge (the legs collider stops you), hands on top just ahead, press down.
|
|
||||||
private void TickMantle(bool both)
|
|
||||||
{
|
|
||||||
float t = _t;
|
|
||||||
_stickVelocity = t < 1f ? new Vector3(0f, 0f, -0.8f) : t > 3.5f ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero;
|
|
||||||
float down = Mathf.Clamp((t - 1.6f) / 1.5f, 0f, 1f);
|
|
||||||
float y = t < 1.6f ? Mathf.Lerp(1.0f, 0.95f, Mathf.Clamp(t - 1f, 0f, 0.6f) / 0.6f) : Mathf.Lerp(0.95f, 0.35f, down);
|
|
||||||
// Hands just past the ledge edge (aimed in the room, like a real player looking at it).
|
|
||||||
float z = t < 1f ? -0.4f : ObstacleFront - 0.1f - _origin.Origin.Z;
|
|
||||||
Vector3 l = new(-0.22f, y, z), r = new(0.22f, y, z);
|
|
||||||
_rightLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward * 0.3f), r);
|
|
||||||
_leftLocal = both ? new Transform3D(Aim(Vector3.Down + Vector3.Forward * 0.3f), l) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
|
||||||
if (t > 3.6f)
|
|
||||||
{
|
|
||||||
// Up and walking on: hands come back to the body like a real player's.
|
|
||||||
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
|
||||||
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f, -0.3f));
|
|
||||||
}
|
|
||||||
if (_ragdoll.Built && !_needBuild)
|
|
||||||
_maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - 0.99f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool FinishMantle(bool both)
|
private bool FinishMantle(bool both)
|
||||||
{
|
{
|
||||||
GD.Print($"RAGDOLL: mantle ({(both ? "two hands" : "one hand")}): pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
|
GD.Print($"RAGDOLL: mantle ({(both ? "two hands" : "one hand")}): pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
|
||||||
bool onTop = _ragdoll.GroundY > 0.85f && _origin.Origin.Y > 0.8f;
|
bool onTop = _ragdoll.GroundY > 0.85f && _origin.Origin.Y > 0.8f;
|
||||||
_lastRise = _maxPelvisRise;
|
_lastRise = _maxPelvisRise;
|
||||||
_maxPelvisRise = 0f;
|
_maxPelvisRise = 0f;
|
||||||
return _finite && NewSnaps == 0 && (both ? onTop : !onTop && _lastRise < 0.25f);
|
return _finite && NewSnaps == 0 && (both ? onTop : !onTop); // standing tall also lifts the hips; one hand never gets you up
|
||||||
}
|
}
|
||||||
|
|
||||||
private StaticBody3D _bar;
|
private StaticBody3D _bar;
|
||||||
@@ -595,7 +576,10 @@ public partial class RagdollSpike : Node3D
|
|||||||
Basis grip = Aim(Vector3.Up + Vector3.Forward * 0.2f);
|
Basis grip = Aim(Vector3.Up + Vector3.Forward * 0.2f);
|
||||||
_rightLocal = new Transform3D(grip, new Vector3(0.22f, y, BarHangZ + 0.03f));
|
_rightLocal = new Transform3D(grip, new Vector3(0.22f, y, BarHangZ + 0.03f));
|
||||||
_leftLocal = both ? new Transform3D(grip, new Vector3(-0.22f, y, BarHangZ + 0.03f)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
_leftLocal = both ? new Transform3D(grip, new Vector3(-0.22f, y, BarHangZ + 0.03f)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
if (!_barGrabbed && t > 0.6f && _ragdoll.Built)
|
bool handsAtBar = _ragdoll.Built && !_needBuild
|
||||||
|
&& _ragdoll.Hand(1).GlobalPosition.DistanceTo(_bar.GlobalPosition + new Vector3(0.22f, 0f, 0f)) < 0.1f
|
||||||
|
&& (!both || _ragdoll.Hand(0).GlobalPosition.DistanceTo(_bar.GlobalPosition + new Vector3(-0.22f, 0f, 0f)) < 0.1f);
|
||||||
|
if (!_barGrabbed && t > 0.3f && handsAtBar)
|
||||||
{
|
{
|
||||||
// The game's own world-grab detection must find the bar under the palm.
|
// The game's own world-grab detection must find the bar under the palm.
|
||||||
if (both)
|
if (both)
|
||||||
@@ -725,6 +709,184 @@ public partial class RagdollSpike : Node3D
|
|||||||
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.05f;
|
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.05f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float _poseDrift, _poseEyeErr;
|
||||||
|
|
||||||
|
// Six held poses, 3 s each (1 s blend + 2 s hold): the view rides the body, so any pose the body
|
||||||
|
// can't match would slowly walk the play space away — that must not happen.
|
||||||
|
private void TickPoseHold(float dt)
|
||||||
|
{
|
||||||
|
_measureFrom = float.MaxValue;
|
||||||
|
(float eye, float pitch, float fwd, Vector3 l, Vector3 r)[] poses =
|
||||||
|
{
|
||||||
|
(EyeHeight, 0f, 0f, new(-0.2f, 1.0f, -0.3f), new(0.2f, 1.0f, -0.3f)),
|
||||||
|
(1.0f, -0.3f, -0.1f, new(-0.2f, 0.4f, -0.35f), new(0.2f, 0.4f, -0.35f)),
|
||||||
|
(0.7f, -0.3f, -0.1f, new(-0.2f, 0.2f, -0.4f), new(0.2f, 0.2f, -0.4f)),
|
||||||
|
(1.35f, -0.6f, -0.35f, new(-0.2f, 0.9f, -0.6f), new(0.2f, 0.9f, -0.6f)),
|
||||||
|
(EyeHeight, 0f, 0f, new(-0.2f, 1.45f, -0.9f), new(0.25f, 1.6f, -0.85f)),
|
||||||
|
(0.9f, 0f, 0.05f, new(-0.25f, 0.6f, -0.2f), new(0.25f, 0.6f, -0.2f)),
|
||||||
|
};
|
||||||
|
int i = Mathf.Min((int)(_t / 3f), poses.Length - 1);
|
||||||
|
int prev = Mathf.Max(i - 1, 0);
|
||||||
|
float k = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((_t - i * 3f) / 1f, 0f, 1f));
|
||||||
|
var a = poses[prev];
|
||||||
|
var b = poses[i];
|
||||||
|
_headLocal = new Transform3D(new Basis(Vector3.Right, Mathf.Lerp(a.pitch, b.pitch, k)), new Vector3(0f, Mathf.Lerp(a.eye, b.eye, k), Mathf.Lerp(a.fwd, b.fwd, k)));
|
||||||
|
_leftLocal = new Transform3D(Basis.Identity, a.l.Lerp(b.l, k));
|
||||||
|
_rightLocal = new Transform3D(Basis.Identity, a.r.Lerp(b.r, k));
|
||||||
|
if (_ragdoll.Built && !_needBuild && _t - i * 3f > 2.5f)
|
||||||
|
_poseEyeErr = Mathf.Max(_poseEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
|
||||||
|
_poseDrift = Mathf.Max(_poseDrift, new Vector2(_origin.Origin.X, _origin.Origin.Z).Length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishPoseHold()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: pose hold: max play-space drift {_poseDrift:F3} m (limit 0.05), eye vs camera at rest max {_poseEyeErr:F3} m (limit 0.08), snaps {NewSnaps}");
|
||||||
|
bool ok = _finite && NewSnaps == 0 && _poseDrift < 0.05f && _poseEyeErr < 0.08f;
|
||||||
|
_poseDrift = _poseEyeErr = 0f;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _hangEyeErr;
|
||||||
|
|
||||||
|
// Grab the bar and just hang (controllers stay up): the view must stay on the character's eye.
|
||||||
|
private void TickHang(float dt)
|
||||||
|
{
|
||||||
|
TickPullUp(both: true);
|
||||||
|
float y = BarY - 0.03f;
|
||||||
|
Basis grip = Aim(Vector3.Up + Vector3.Forward * 0.2f);
|
||||||
|
_rightLocal = new Transform3D(grip, new Vector3(0.22f, y, BarHangZ + 0.03f));
|
||||||
|
_leftLocal = new Transform3D(grip, new Vector3(-0.22f, y, BarHangZ + 0.03f));
|
||||||
|
_headLocal = new Transform3D(Basis.Identity, new Vector3(0f, EyeHeight + 0.15f * Mathf.Sin(_t * 2f), 0.05f * Mathf.Sin(_t * 1.3f)));
|
||||||
|
if (_barGrabbed && _t > 1.5f)
|
||||||
|
_hangEyeErr = Mathf.Max(_hangEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishHang()
|
||||||
|
{
|
||||||
|
GD.Print($"RAGDOLL: hang: eye vs camera max {_hangEyeErr:F3} m (limit 0.08), climbing {_ragdoll.Climbing}, snaps {NewSnaps}");
|
||||||
|
_ragdoll.SetHeld(0, null);
|
||||||
|
_ragdoll.SetHeld(1, null);
|
||||||
|
bool ok = _finite && NewSnaps == 0 && _hangEyeErr < 0.08f;
|
||||||
|
_hangEyeErr = 0f;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk to the waist-high ledge, crouch a little with the hands on the edge close to the body,
|
||||||
|
// then stand up while pressing the hands down, lean in, walk on.
|
||||||
|
private void TickHumanMantle(bool both)
|
||||||
|
{
|
||||||
|
float t = _t;
|
||||||
|
_stickVelocity = t < 1f || t > 4.5f ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero;
|
||||||
|
float crouch = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 1f) / 0.8f, 0f, 1f));
|
||||||
|
float rise = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 2.2f) / 1.6f, 0f, 1f));
|
||||||
|
float eye = EyeHeight - 0.25f * crouch + 0.35f * rise;
|
||||||
|
float lean = 0.25f * rise;
|
||||||
|
_headLocal = new Transform3D(new Basis(Vector3.Right, -0.3f * crouch), new Vector3(0f, eye, -lean));
|
||||||
|
float handZ = ObstacleFront - 0.08f - _origin.Origin.Z;
|
||||||
|
float handY = t < 1f ? 1.0f : Mathf.Lerp(0.95f, 0.55f, rise) - 0.05f * crouch;
|
||||||
|
Basis down = Aim(Vector3.Down + Vector3.Forward * 0.3f);
|
||||||
|
_leftLocal = both ? new Transform3D(down, new Vector3(-0.2f, handY, handZ)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
|
_rightLocal = new Transform3D(down, new Vector3(0.2f, handY, handZ));
|
||||||
|
if (_ragdoll.Built && !_needBuild)
|
||||||
|
_maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - 0.99f);
|
||||||
|
if (t > 4.5f)
|
||||||
|
{
|
||||||
|
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
|
||||||
|
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f, -0.3f));
|
||||||
|
_headLocal = new Transform3D(Basis.Identity, new Vector3(0f, EyeHeight, 0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float ClimbFace = -0.4f;
|
||||||
|
private StaticBody3D _wall;
|
||||||
|
private readonly ConeTwistJoint3D[] _climbWeld = new ConeTwistJoint3D[2];
|
||||||
|
private readonly Vector3[] _gripWorld = new Vector3[2];
|
||||||
|
private float _climbStartPelvis, _climbEyeErr;
|
||||||
|
|
||||||
|
private void SetupClimbWall()
|
||||||
|
{
|
||||||
|
Rest();
|
||||||
|
_wall = MakeStatic(new Vector3(3f, 4f, 0.5f), new Vector3(0f, 2f, ClimbFace - 0.25f), 29);
|
||||||
|
_stage.AddChild(_wall);
|
||||||
|
_climbWeld[0] = _climbWeld[1] = null;
|
||||||
|
_gripWorld[0] = new Vector3(-0.2f, 1.75f, ClimbFace + 0.05f);
|
||||||
|
_gripWorld[1] = new Vector3(0.2f, 1.75f, ClimbFace + 0.05f);
|
||||||
|
_climbEyeErr = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Grip(int side, bool on)
|
||||||
|
{
|
||||||
|
if (on && _climbWeld[side] == null)
|
||||||
|
{
|
||||||
|
RigidBody3D hand = _ragdoll.Hand(side);
|
||||||
|
_climbWeld[side] = new ConeTwistJoint3D { Position = hand.GlobalPosition, NodeA = hand.GetPath(), NodeB = _wall.GetPath(), SwingSpan = 0f, TwistSpan = 0f, Bias = 0f };
|
||||||
|
_stage.AddChild(_climbWeld[side]);
|
||||||
|
_ragdoll.SetHeld(side, _wall);
|
||||||
|
}
|
||||||
|
else if (!on && _climbWeld[side] != null)
|
||||||
|
{
|
||||||
|
_climbWeld[side].QueueFree();
|
||||||
|
_climbWeld[side] = null;
|
||||||
|
_ragdoll.SetHeld(side, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hold targets live in the world (the holds); while gripping, the player pulls the controllers
|
||||||
|
// down in the room. Free hands reach for the next hold 45 cm higher.
|
||||||
|
private void TickClimbWall(float dt)
|
||||||
|
{
|
||||||
|
float t = _t;
|
||||||
|
_measureFrom = float.MaxValue;
|
||||||
|
if (!_ragdoll.Built || _needBuild)
|
||||||
|
return;
|
||||||
|
float pull1 = Mathf.Clamp((t - 1.0f) / 1.2f, 0f, 1f), pull2 = Mathf.Clamp((t - 4.0f) / 1.2f, 0f, 1f);
|
||||||
|
if (t > 0.95f && t < 1.0f)
|
||||||
|
{
|
||||||
|
Grip(0, true);
|
||||||
|
Grip(1, true);
|
||||||
|
_climbStartPelvis = _ragdoll.Pelvis.GlobalPosition.Y;
|
||||||
|
}
|
||||||
|
for (int side = 0; side < 2; side++)
|
||||||
|
{
|
||||||
|
float releaseAt = side == 1 ? 2.3f : 3.1f, regripAt = releaseAt + 0.6f;
|
||||||
|
if (t > releaseAt && t < regripAt && _climbWeld[side] != null)
|
||||||
|
{
|
||||||
|
Grip(side, false);
|
||||||
|
// Next hold: 30 cm above the hand that is still holding (as a climber reaches).
|
||||||
|
_gripWorld[side] = _ragdoll.Hand(1 - side).GlobalPosition + Vector3.Up * 0.3f + Vector3.Right * (side == 0 ? -0.4f : 0.4f);
|
||||||
|
_gripWorld[side].Z = ClimbFace + 0.05f;
|
||||||
|
}
|
||||||
|
if (t >= regripAt && _climbWeld[side] == null && _ragdoll.Hand(side).GlobalPosition.DistanceTo(_gripWorld[side]) < 0.08f)
|
||||||
|
Grip(side, true);
|
||||||
|
// Gripping: the controller is pulled down in the room by the pull amount; reaching: it
|
||||||
|
// goes to the next hold.
|
||||||
|
Vector3 target = _climbWeld[side] != null || t < 1.0f
|
||||||
|
? _gripWorld[side] - _origin.Origin - Vector3.Up * (0.5f * (pull1 + pull2))
|
||||||
|
: _gripWorld[side] - _origin.Origin;
|
||||||
|
// Reach like a person: ease from where the hand is to the hold over ~0.5 s.
|
||||||
|
Vector3 current = side == 0 ? _leftLocal.Origin : _rightLocal.Origin;
|
||||||
|
if (_climbWeld[side] == null)
|
||||||
|
target = current.MoveToward(target, 1.2f * dt);
|
||||||
|
Basis aim = Aim(Vector3.Up + Vector3.Forward * 0.15f);
|
||||||
|
if (side == 0)
|
||||||
|
_leftLocal = new Transform3D(aim, target);
|
||||||
|
else
|
||||||
|
_rightLocal = new Transform3D(aim, target);
|
||||||
|
}
|
||||||
|
if (t > 1f)
|
||||||
|
_climbEyeErr = Mathf.Max(_climbEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FinishClimbWall()
|
||||||
|
{
|
||||||
|
float rise = _ragdoll.Pelvis.GlobalPosition.Y - _climbStartPelvis;
|
||||||
|
GD.Print($"RAGDOLL: wall climb: pelvis rose {rise:F3} m (> 0.6), holding {_climbWeld[0] != null}/{_climbWeld[1] != null}, eye vs camera max {_climbEyeErr:F3} (< {PlayerRagdoll.MaxViewLead + 0.03f:F2}), snaps {NewSnaps}");
|
||||||
|
Grip(0, false);
|
||||||
|
Grip(1, false);
|
||||||
|
// MaxViewLead 0.08 is enforced before each step; this is measured after it (+ one tick of motion).
|
||||||
|
return _finite && NewSnaps == 0 && rise > 0.6f && _climbEyeErr < PlayerRagdoll.MaxViewLead + 0.03f;
|
||||||
|
}
|
||||||
|
|
||||||
// A slope rising away (−Z) from ObstacleFront up to 1 m, then a platform.
|
// A slope rising away (−Z) from ObstacleFront up to 1 m, then a platform.
|
||||||
private void SetupRamp(float degrees)
|
private void SetupRamp(float degrees)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user