Ground under the body: sphere footprint, clamber by pressure (Nimso), physics objects climbable, arms keep colliding
- Ground = centre ray + stance-wide sphere cast (face normal from a ray inside the contact): planks and lips keep support. - Higher ground is stood on when the hips are over it and the body presses onto it (hands braced, a just-finished press, legs tucked over it, or airborne) - no hips-well-above rule. - Bracing counts heavy/frozen physics objects (>= 15 kg), not what the hand carries. - Knee up: braced + hips held back tucks the legs collider. - Support goal capped at straight legs (tiptoe/over-tall head floated the body). - Arms never get collision exceptions with held objects (they disabled whole crag rocks). - Spike: heavy-box mantle, 15 cm plank, chest-high ledge two hands yes / one hand no. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e300c7f04
commit
2d986d08ac
2 files changed
+172
-52
No files matched your search
+97
-42
@@ -187,7 +187,7 @@ public partial class PlayerRagdoll : Node3D
|
||||
private float _supportGoalY;
|
||||
private RigidBody3D _groundBody;
|
||||
private Vector3 _groundPoint;
|
||||
private float _tuckFree, _dt;
|
||||
private float _tuckFree, _dt, _sinceBraced = float.MaxValue;
|
||||
private PhysicsShapeQueryParameters3D _legsQuery;
|
||||
|
||||
private bool LegsSpaceFree(Vector3 center, Basis basis)
|
||||
@@ -599,6 +599,7 @@ public partial class PlayerRagdoll : Node3D
|
||||
HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity);
|
||||
Braced |= IsBraced(a);
|
||||
}
|
||||
_sinceBraced = Braced ? 0f : _sinceBraced + dt;
|
||||
GroundControl(pelvisTarget, floorY, dt);
|
||||
HeadGuide(s, dt);
|
||||
LegsDrive(s);
|
||||
@@ -720,15 +721,24 @@ public partial class PlayerRagdoll : Node3D
|
||||
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
|
||||
}
|
||||
|
||||
// A hand pressing on or holding static world (a ledge, a wall, a grabbed bar) — not the floor:
|
||||
// hands on the floor while crawling don't lift the body.
|
||||
// A hand pressing on or holding something solid enough to push yourself up on (a ledge, a wall,
|
||||
// a grabbed bar, a heavy crate) — not the floor: hands on the floor while crawling don't lift the
|
||||
// body. Nimso: physics objects are climbed "the same" as the world. A light prop just gets pushed,
|
||||
// and something you're carrying isn't support.
|
||||
public const float BraceMinMass = 15f;
|
||||
public const float ClamberMemory = 0.6f; // s a hand press still counts after the hands leave
|
||||
|
||||
private static bool IsBraced(Arm a)
|
||||
{
|
||||
if (a.Held is StaticBody3D)
|
||||
if (a.Held is StaticBody3D || a.Held is RigidBody3D { Freeze: true })
|
||||
return true;
|
||||
foreach (Node3D n in a.Hand.GetCollidingBodies())
|
||||
if (n is StaticBody3D && !n.IsInGroup(PropGrabber.NoGrabGroup))
|
||||
{
|
||||
if (n == a.Held || n.IsInGroup(PropGrabber.NoGrabGroup))
|
||||
continue;
|
||||
if (n is StaticBody3D || n is RigidBody3D { Freeze: true } || n is RigidBody3D { Mass: >= BraceMinMass })
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -748,7 +758,10 @@ public partial class PlayerRagdoll : Node3D
|
||||
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);
|
||||
// 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 wantTuck = deep || _lifted || kneeUp || (!_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);
|
||||
@@ -845,16 +858,12 @@ public partial class PlayerRagdoll : Node3D
|
||||
{
|
||||
Vector3 pos = Pelvis.GlobalPosition;
|
||||
float hipAboveFloor = target.Origin.Y - floorY;
|
||||
var query = PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * (hipAboveFloor + GroundProbeExtra + 0.5f), WorldMask);
|
||||
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||
bool wasGrounded = Grounded;
|
||||
Grounded = false;
|
||||
_groundBody = null;
|
||||
if (hit.Count > 0)
|
||||
if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips))
|
||||
{
|
||||
Vector3 point = (Vector3)hit["position"];
|
||||
Vector3 normal = (Vector3)hit["normal"];
|
||||
_groundBody = GodotObject.InstanceFromId((ulong)hit["collider_id"]) as RigidBody3D; // Box3D: "collider" is null
|
||||
_groundBody = groundObject as RigidBody3D;
|
||||
_groundPoint = point;
|
||||
// Standing (or walking down a step) the legs keep contact over GroundProbeExtra; falling,
|
||||
// they touch down only within their reach — the rest of the fall is stopped by the legs.
|
||||
@@ -873,14 +882,17 @@ public partial class PlayerRagdoll : Node3D
|
||||
}
|
||||
else if (inReach && !steep)
|
||||
{
|
||||
// Step rule: ground more than a step above the feet is only stood on once the
|
||||
// hips are already well above it (climbed/mantled/landed there) — otherwise it's an
|
||||
// obstacle the legs collider holds us back from, and the feet stay where they were.
|
||||
// Higher ground climbed onto must be under the hips' back edge too (not just a
|
||||
// pelvis centre hanging over the lip).
|
||||
bool clear = point.Y <= _footY + MaxStepHeight || GroundBehindAt(pos, point.Y);
|
||||
bool reachable = point.Y <= _footY + MaxStepHeight
|
||||
|| ((!wasGrounded || _lifted || pos.Y - point.Y > 0.6f * hipAboveFloor) && clear);
|
||||
// Step rule (Nimso: higher ground "doesn't move my body up … until I push on it with enough
|
||||
// pressure, at which point I actually clamber over"): ground more than a step above the
|
||||
// feet is stood on once the hips are over it AND the body is pressing onto it — hands
|
||||
// braced on anything solid (ledge, crate), or already airborne
|
||||
// (landing, hanging). Otherwise it's an obstacle the legs collider holds us back from.
|
||||
// No "hips well above it" requirement: that made a ledge need a full press-up.
|
||||
bool step = point.Y <= _footY + MaxStepHeight;
|
||||
// The press lasts a moment after the hands leave (you push up, then move over it), and legs
|
||||
// tucked up with the hips over it are already on top of it.
|
||||
bool pressing = !wasGrounded || Braced || _lifted || _sinceBraced < ClamberMemory || !_legsSolid;
|
||||
bool reachable = step || (underHips && pressing);
|
||||
Grounded = true;
|
||||
if (reachable)
|
||||
_footY = point.Y;
|
||||
@@ -915,6 +927,9 @@ public partial class PlayerRagdoll : Node3D
|
||||
{
|
||||
// Pose heights are relative to the tracking floor; stand on the ground actually under us.
|
||||
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
|
||||
// Legs can't hold the hips higher than straight legs (head above the calibrated height —
|
||||
// tiptoes, a reach up — would otherwise float the body off the ground).
|
||||
goal.Y = Mathf.Min(goal.Y, _footY + _standingHip + JumpExtension);
|
||||
_supportGoalY = goal.Y;
|
||||
if (_jump == JumpPhase.Dip)
|
||||
goal.Y = Mathf.Min(goal.Y, _jumpFromY);
|
||||
@@ -1070,17 +1085,65 @@ public partial class PlayerRagdoll : Node3D
|
||||
|
||||
private PhysicsShapeQueryParameters3D _cameraQuery;
|
||||
|
||||
// Second ground ray 12 cm behind the hips (toward the body's back): ground at least `groundY`.
|
||||
private bool GroundBehindAt(Vector3 pelvisPos, float groundY)
|
||||
// Ground under the body (Nimso: "ground detection uses the objects underneath the body"), not
|
||||
// under one point: a sphere the width of the stance is cast down from the hips, plus the
|
||||
// centre ray. One ray lost the ground as soon as the hips swayed off a plank or past a ledge lip.
|
||||
// The sphere's contact can be a corner, whose normal is useless — the surface normal comes from a
|
||||
// short ray just inside the contact. Same-level ground (within a step) from either probe is
|
||||
// stood on; higher ground only from the centre ray (the hips must really be over it), and it's
|
||||
// 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)
|
||||
{
|
||||
Vector3 back = Pelvis.GlobalTransform.Basis.Z;
|
||||
back.Y = 0f;
|
||||
back = back.LengthSquared() > 1e-6f ? back.Normalized() * 0.12f : Vector3.Zero;
|
||||
var query = PhysicsRayQueryParameters3D.Create(pelvisPos + back, pelvisPos + back + Vector3.Down * 3f, WorldMask);
|
||||
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||
return hit.Count > 0 && ((Vector3)hit["position"]).Y > groundY - 0.05f;
|
||||
PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState;
|
||||
point = normal = Vector3.Zero;
|
||||
collider = null;
|
||||
underHips = false;
|
||||
Godot.Collections.Dictionary ray = space.IntersectRay(PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * length, WorldMask));
|
||||
bool haveRay = ray.Count > 0;
|
||||
if (haveRay)
|
||||
{
|
||||
point = (Vector3)ray["position"];
|
||||
normal = (Vector3)ray["normal"];
|
||||
collider = GodotObject.InstanceFromId((ulong)ray["collider_id"]); // Box3D: "collider" is null
|
||||
underHips = true;
|
||||
}
|
||||
_footprintQuery ??= new PhysicsShapeQueryParameters3D { Shape = new SphereShape3D { Radius = FootprintRadius }, CollisionMask = WorldMask };
|
||||
_footprintQuery.Transform = new Transform3D(Basis.Identity, pos);
|
||||
_footprintQuery.Motion = Vector3.Down * length;
|
||||
Godot.Collections.Dictionary rest = space.GetRestInfo(_footprintQuery);
|
||||
if (rest.Count == 0)
|
||||
return haveRay;
|
||||
Vector3 contact = (Vector3)rest["point"];
|
||||
if (contact.Y >= pos.Y - 0.05f)
|
||||
return haveRay; // touching a wall beside the hips, not ground
|
||||
Vector3 inward = new(contact.X - pos.X, 0f, contact.Z - pos.Z);
|
||||
inward = inward.LengthSquared() > 1e-6f ? inward.Normalized() * 0.02f : Vector3.Zero;
|
||||
Vector3 probe = contact + inward;
|
||||
Godot.Collections.Dictionary face = space.IntersectRay(PhysicsRayQueryParameters3D.Create(
|
||||
new Vector3(probe.X, contact.Y + 0.05f, probe.Z), new Vector3(probe.X, contact.Y - 0.05f, probe.Z), WorldMask));
|
||||
if (face.Count == 0)
|
||||
return haveRay;
|
||||
Vector3 sPoint = (Vector3)face["position"], sNormal = (Vector3)face["normal"];
|
||||
if (sNormal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)))
|
||||
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 better = !haveRay || normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)) || sPoint.Y > point.Y + 0.01f;
|
||||
if (sameLevel && better)
|
||||
{
|
||||
point = sPoint;
|
||||
normal = sNormal;
|
||||
collider = GodotObject.InstanceFromId((ulong)face["collider_id"]);
|
||||
underHips = true; // under the footprint
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private PhysicsShapeQueryParameters3D _footprintQuery;
|
||||
|
||||
public void TeleportBy(Transform3D delta)
|
||||
{
|
||||
if (!Built)
|
||||
@@ -1118,23 +1181,15 @@ public partial class PlayerRagdoll : Node3D
|
||||
b.ResetPhysicsInterpolation();
|
||||
}
|
||||
|
||||
// Collision exceptions between a held prop and that arm's segments (its hand is excluded by
|
||||
// the grab weld itself) — a forearm wrapped around a held object must not fight it.
|
||||
// What each hand holds (bracing, climbing). Only the hand itself is excluded from the held object
|
||||
// (by the grab weld).
|
||||
public void SetHeld(int side, PhysicsBody3D prop)
|
||||
{
|
||||
if (!Built)
|
||||
return;
|
||||
Arm a = _arms[side];
|
||||
if (a.Held != null && IsInstanceValid(a.Held) && a.Held != _arms[1 - side].Held)
|
||||
{
|
||||
a.Upper.RemoveCollisionExceptionWith(a.Held);
|
||||
a.Fore.RemoveCollisionExceptionWith(a.Held);
|
||||
}
|
||||
a.Held = prop;
|
||||
if (prop != null)
|
||||
{
|
||||
a.Upper.AddCollisionExceptionWith(prop);
|
||||
a.Fore.AddCollisionExceptionWith(prop);
|
||||
}
|
||||
// The arm keeps colliding with what the hand holds (owner: real arms don't pass through things).
|
||||
// The old per-object exception switched the arm off against a whole crag rock while
|
||||
// gripping one of its holds — arms "randomly" phased through the world mid-climb.
|
||||
_arms[side].Held = prop;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user