Clambering: capsule hips (Nimso), footprint takes the edge while pressing, knee-up memory

The box pelvis caught on ledge lips; a sideways capsule rides over. While clambering
(pressing, just pressed, legs tucked) the stance footprint may take higher ground the hips
are only partly over. Spike measures the free hand's error while climbing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 20:26:22 +03:00
1 parent 0bddcf4517
commit 83186f9676
2 files changed
+39 -10

No files matched your search

+24 -10
View File
@@ -257,8 +257,11 @@ public partial class PlayerRagdoll : Node3D
_eyeLocal -= headCenterFromPivot; // eye relative to the head body's centre
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,
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal), friction: TorsoFriction);
// Hips = a sideways capsule (Nimso: "hip collider: a capsule, ~27 cm diameter"): a box's bottom
// edge caught on ledge lips a centimetre below it, so the hips couldn't slide over onto them.
float hipRadius = Mathf.Min(0.5f * Mathf.Max(pelvisSize.Y, pelvisSize.Z), 0.5f * pelvisSize.X);
Pelvis = MakeBody("Pelvis", new CapsuleShape3D { Radius = hipRadius, Height = pelvisSize.X }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants,
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal), friction: TorsoFriction, shapeBasis: new Basis(Vector3.Back, Mathf.Pi / 2f));
float standingHip = pose.PelvisTarget.Y - (pose.Eye.Y - p.EyeHeight);
_standingHip = standingHip + _pelvisCenterLocal.Y; // of the pelvis BODY (what ground control holds)
_legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight);
@@ -372,14 +375,22 @@ public partial class PlayerRagdoll : Node3D
// 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)
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true, float friction = 1f,
Basis? shapeBasis = null)
{
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 });
if (shape is BoxShape3D box && layer != 0 && visible)
Transform3D shapeAt = new(shapeBasis ?? Basis.Identity, Vector3.Zero);
body.AddChild(new CollisionShape3D { Shape = shape, Transform = shapeAt });
Mesh visual = shape switch
{
MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = box.Size } };
BoxShape3D box => new BoxMesh { Size = box.Size },
CapsuleShape3D cap => new CapsuleMesh { Radius = cap.Radius, Height = cap.Height },
_ => null,
};
if (visual != null && layer != 0 && visible)
{
MeshInstance3D mesh = new() { Mesh = visual, Transform = shapeAt };
mesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f });
body.AddChild(mesh);
}
@@ -762,7 +773,7 @@ public partial class PlayerRagdoll : Node3D
bool deep = center + _legsHeight * 0.5f >= hip.Y + LegsOffAbove;
// Hands braced while the hips are held back by an obstacle = getting a knee up onto it (a
// thigh-high crate or ledge the straight arms can't press down on while standing).
bool kneeUp = Braced && HeldBack != Vector3.Zero;
bool kneeUp = (Braced || _sinceBraced < ClamberMemory) && HeldBack != Vector3.Zero;
bool wantTuck = deep || _lifted || kneeUp || (!_legsSolid && Braced);
_tuckFree = wantTuck ? 0f : _tuckFree + _dt;
Vector3 untucked = new(hip.X, center, hip.Z);
@@ -863,7 +874,8 @@ public partial class PlayerRagdoll : Node3D
bool wasGrounded = Grounded;
Grounded = false;
_groundBody = null;
if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips))
bool clambering = Braced || _lifted || _sinceBraced < ClamberMemory || !_legsSolid;
if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded || clambering ? (clambering ? 2 : 1) : 0, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips))
{
_groundBody = groundObject as RigidBody3D;
_groundPoint = point;
@@ -1099,7 +1111,9 @@ public partial class PlayerRagdoll : Node3D
// reported via underHips for the pressure rule.
public const float FootprintRadius = 0.12f;
private bool ProbeGround(Vector3 pos, float length, bool wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject collider, out bool underHips)
// mode: 0 airborne, 1 standing, 2 clambering (pressing onto something: the footprint may take
// higher ground the hips are only partly over — the capsule hips then ride up onto its edge).
private bool ProbeGround(Vector3 pos, float length, int mode, out Vector3 point, out Vector3 normal, out GodotObject collider, out bool underHips)
{
PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState;
point = normal = Vector3.Zero;
@@ -1135,7 +1149,7 @@ public partial class PlayerRagdoll : Node3D
return haveRay;
// The sphere wins where the centre ray found nothing, nothing walkable, or only lower ground
// within the stance (a plank's edge under the hips) — but never higher ground than a step.
bool sameLevel = sPoint.Y <= _footY + MaxStepHeight || !wasGrounded;
bool sameLevel = sPoint.Y <= _footY + MaxStepHeight || mode != 1;
bool better = !haveRay || normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)) || sPoint.Y > point.Y + 0.01f;
if (sameLevel && better)
{
+15
View File
@@ -1142,6 +1142,15 @@ public partial class RagdollSpike : Node3D
}
if (t > 1f)
_climbEyeErr = Mathf.Max(_climbEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
// The free hand reaching for the next hold while hanging on the other (tipped body): it
// should stay on its controller (owner: "tracking off when the body is tilted").
for (int side = 0; side < 2; side++)
if (_climbWeld[side] == null && _climbStep is 2 or 3 && _ragdoll.Hand(side).GetContactCount() == 0)
{
Vector3 ctrl = (side == 0 ? Left : Right).Origin;
_climbFreeErr += _ragdoll.Hand(side).GlobalPosition.DistanceTo(ctrl);
_climbFreeN++;
}
void Next(int step)
{
@@ -1158,9 +1167,15 @@ public partial class RagdollSpike : Node3D
}
}
private float _climbFreeErr;
private int _climbFreeN;
private bool FinishClimbWall()
{
float rise = _ragdoll.Pelvis.GlobalPosition.Y - _climbStartPelvis;
GD.Print($"RAGDOLL: wall climb: free hand vs its controller while reaching {(_climbFreeN > 0 ? _climbFreeErr / _climbFreeN : 0f):F3} m mean");
_climbFreeErr = 0f;
_climbFreeN = 0;
GD.Print($"RAGDOLL: wall climb: reached step {_climbStep}/5, pelvis rose {rise:F3} m (> 0.35), 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);