Body step 2.1: climbing, mantling, crawling, legs, playground, 144 Hz

- Step rule + lower-body collider: no walking onto tables, steps <= 0.35 m
- Structural hand strength: two-hand mantle/pull-up works, one hand can't
- Arms aim from the physical shoulder horizontally, keep True Body pose vertically
- Crawl pose, crawl carry, 35 deg slope limit, ground feed-forward ignores
  play-space corrections (fixes a runaway feedback loop)
- Grab static world (climbing), procedural visual legs
- Playground sandbox; menu strength slider, spectator (old drag) and turn toggles
- Physics 144 Hz with interpolation; legacy chase-body spikes pin 90 Hz
- RagdollSpike: 27 phases incl. mantle, pull-up, crawl, ledge drop, ramps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 07:56:08 +03:00
1 parent d83273dd2f
commit 2aff2b61da
26 files changed
+1243 -70

No files matched your search

+36 -13
View File
@@ -11,6 +11,8 @@ public partial class PropGrabber : Node
private const float BreakTime = 0.25f;
private const float BreakDistanceBoth = 0.4f;
private const float GripRipDistance = 0.8f;
private const float WorldGrabRadius = 0.05f; // palm sphere for grabbing static world (ledges, bars, holds)
public const string NoGrabGroup = "no_grab"; // static bodies that can't be grabbed (the floor)
private static PropGrabber _instance;
@@ -18,7 +20,7 @@ public partial class PropGrabber : Node
private XRController3D _right;
private PhysicsArms _arms;
private readonly RigidBody3D[] _held = new RigidBody3D[2];
private readonly PhysicsBody3D[] _held = new PhysicsBody3D[2]; // a prop, or static world (climbing)
private readonly ConeTwistJoint3D[] _joints = new ConeTwistJoint3D[2];
private readonly Vector3[] _localAnchor = new Vector3[2];
private readonly uint[] _grabSeq = new uint[2];
@@ -59,7 +61,7 @@ public partial class PropGrabber : Node
if (grip && !_gripPrev[hand] && _held[hand] == null)
{
RigidBody3D prop = FindProp(controller, hand);
PhysicsBody3D prop = FindProp(controller, hand);
RigidBody3D handBody = PhysicsHands.GetBody(controller);
if (prop != null && handBody != null)
{
@@ -68,26 +70,27 @@ public partial class PropGrabber : Node
Vector3 pivot = PalmPoint(handBody, hand);
_joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias);
_localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot;
_arms?.SetHeld(hand, prop);
_arms?.SetHeld(hand, prop as RigidBody3D);
PlayerRig.SetHeld(controller, prop);
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({prop.Mass:0.###} kg, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
string what = prop is RigidBody3D rb ? $"{rb.Mass:0.###} kg" : "static world";
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({what}, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
}
}
if (!grip && _held[hand] != null)
{
RigidBody3D prop = _held[hand];
PhysicsBody3D prop = _held[hand];
_held[hand] = null;
DestroyJoint(hand);
if (IsInstanceValid(prop))
GD.Print($"PropGrabber: released '{prop.Name}' at {prop.LinearVelocity.Length():F2} m/s");
if (prop is RigidBody3D released && IsInstanceValid(released))
GD.Print($"PropGrabber: released '{released.Name}' at {released.LinearVelocity.Length():F2} m/s");
}
_gripPrev[hand] = grip;
}
private void HoldUpdate(XRController3D controller, int hand, float delta)
{
RigidBody3D body = _held[hand];
PhysicsBody3D body = _held[hand];
if (body == null)
return;
if (!IsInstanceValid(body))
@@ -148,7 +151,7 @@ public partial class PropGrabber : Node
}
}
private int PrimaryHand(RigidBody3D body)
private int PrimaryHand(PhysicsBody3D body)
{
int primary = -1;
for (int hand = 0; hand < 2; hand++)
@@ -159,7 +162,7 @@ public partial class PropGrabber : Node
return primary;
}
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, RigidBody3D prop, Vector3 pivot, float bias)
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, PhysicsBody3D prop, Vector3 pivot, float bias)
{
ConeTwistJoint3D weld = new()
{
@@ -175,7 +178,7 @@ public partial class PropGrabber : Node
return weld;
}
private void BreakGrip(int hand, RigidBody3D body, string reason)
private void BreakGrip(int hand, PhysicsBody3D body, string reason)
{
if (_held[hand] != body)
return;
@@ -199,7 +202,7 @@ public partial class PropGrabber : Node
private static float PalmOffset(int hand) => hand == 0 ? PalmGrabOffset : -PalmGrabOffset;
private RigidBody3D FindProp(XRController3D controller, int hand)
private PhysicsBody3D FindProp(XRController3D controller, int hand)
{
RigidBody3D handBody = PhysicsHands.GetBody(controller);
if (handBody == null)
@@ -208,7 +211,27 @@ public partial class PropGrabber : Node
RigidBody3D nearest = NearestProp(palm, GrabRadius);
if (nearest != null)
return nearest;
return NearestProp(handBody.GlobalPosition, CenterGrabRadius);
nearest = NearestProp(handBody.GlobalPosition, CenterGrabRadius);
if (nearest != null)
return nearest;
return WorldUnderPalm(palm, handBody.GetWorld3D());
}
// Climbing: any static body touching the palm (except the "no_grab" group) can be held.
private static StaticBody3D WorldUnderPalm(Vector3 palm, World3D world)
{
PhysicsShapeQueryParameters3D query = new()
{
Shape = new SphereShape3D { Radius = WorldGrabRadius },
Transform = new Transform3D(Basis.Identity, palm),
CollisionMask = 1,
};
foreach (Godot.Collections.Dictionary hit in world.DirectSpaceState.IntersectShape(query, 8))
{
if (hit["collider"].AsGodotObject() is StaticBody3D body && !body.IsInGroup(NoGrabGroup))
return body;
}
return null;
}
private RigidBody3D NearestProp(Vector3 point, float radius)