diff --git a/research/ragdoll-player-step2.md b/research/ragdoll-player-step2.md index 1de15fb..7360e30 100644 --- a/research/ragdoll-player-step2.md +++ b/research/ragdoll-player-step2.md @@ -241,3 +241,9 @@ engine does all the work"; higher ground "doesn't move my body up … until I pu | **Render-clock tracking timestamps** (`PoseFilter.Sample(..., sampleTime)`, `PlayerRig.TrackingAge`, bible v2 recipe) | At 1 m/s the per-tick speed spread is 1.25 m/s by arrival vs 0.013 m/s render-timed — every feed-forward beat at 18 Hz. Constant ~2-tick lag instead. Toggle in the menu to compare | | **max_physics_steps_per_frame 14 → 6** | One hitch could queue 14 ragdoll ticks in a frame (bible v2) | | Tried and reverted: arm aim from the physical shoulder vertically while climbing/tilted | Free-hand error 6.4 → 6.2 cm only, and it weakened pulls. The "tracking off when tilted" screenshots need a recording to diagnose | + +**2.7 follow-ups (owner playtest):** grab buffer — a squeeze that finds nothing keeps trying for 0.4 s while held +(`PropGrabber.GrabBuffer`; launching up a ladder left too short a window to catch a rung; Nimso's videos only +mention arm collision saving missed grabs). Elbow pole clamp asymmetric: 110° outward, 35° inward +(`ArmSolver.PoleMaxInward`; controller turned out folded the elbow 5.6 cm inward → 2.6 cm; 17° broke the crawl's +pillar arms). Known, minor: the procedural legs lag and jitter during very fast falls. diff --git a/scripts/ArmSolver.cs b/scripts/ArmSolver.cs index 1c8e9fb..2549f69 100644 --- a/scripts/ArmSolver.cs +++ b/scripts/ArmSolver.cs @@ -11,7 +11,8 @@ public sealed class ArmSolver public const float SoftIKFactor = 0.98f; public const float PoleHandWeight = 0.85f; public const float PoleSmoothRate = 12f; - public const float PoleMaxAngle = 1.92f; // 110 deg either way from down/back/out + public const float PoleMaxAngle = 1.92f; // 110 deg outward from down/back/out + public const float PoleMaxInward = 0.6f; // 35 deg inward: a controller turned outward must not fold the elbows toward each other (owner) public const float PoleMaxSpeed = 6f; // rad/s public const float ElbowRiseAllowance = 0.05f; @@ -76,7 +77,10 @@ public sealed class ArmSolver targetAngle = raw * PoleHandWeight * confidence; } } - targetAngle = Mathf.Clamp(targetAngle, -PoleMaxAngle, PoleMaxAngle); + // Asymmetric: the elbow may swing far out, but only a little in toward the body's midline. + float outward = refB.Dot(side); // > 0: a positive angle swings the elbow outward + float inwardLimit = Mathf.Lerp(PoleMaxAngle, PoleMaxInward, Mathf.Clamp(Mathf.Abs(outward) / 0.5f, 0f, 1f)); + targetAngle = outward >= 0f ? Mathf.Clamp(targetAngle, -inwardLimit, PoleMaxAngle) : Mathf.Clamp(targetAngle, -PoleMaxAngle, inwardLimit); float step = (targetAngle - _poleAngle) * (1f - Mathf.Exp(-delta * PoleSmoothRate)); _poleAngle += Mathf.Clamp(step, -PoleMaxSpeed * delta, PoleMaxSpeed * delta); Vector3 pole = refN.Rotated(dir, _poleAngle); diff --git a/scripts/PropGrabber.cs b/scripts/PropGrabber.cs index 1c5e177..afafac5 100644 --- a/scripts/PropGrabber.cs +++ b/scripts/PropGrabber.cs @@ -11,6 +11,10 @@ public partial class PropGrabber : Node private const float BreakTime = 0.25f; private const float BreakDistanceBoth = 0.4f; private const float GripRipDistance = 0.8f; + // Grab buffer (owner: launching up a ladder leaves a short window to catch the next rung): a + // squeeze that finds nothing keeps trying while held, so you can squeeze early and catch the + // hold the moment the palm reaches it. + private const float GrabBuffer = 0.4f; // s private const float WorldGrabRadius = 0.07f; // palm sphere for grabbing static world (ledges, bars, holds) public const string NoGrabGroup = "no_grab"; // static bodies that can't be grabbed (the floor) @@ -26,7 +30,8 @@ public partial class PropGrabber : Node private readonly uint[] _grabSeq = new uint[2]; private readonly float[] _breakTimer = new float[2]; private readonly bool[] _gripPrev = new bool[2]; - private readonly SoftGrab[] _easing = new SoftGrab[2]; // grabbed, being eased into the hand (no weld yet) + private readonly SoftGrab[] _easing = new SoftGrab[2]; + private readonly double[] _bufferUntil = new double[2]; // grabbed, being eased into the hand (no weld yet) private uint _grabCount; public override void _Ready() @@ -86,12 +91,18 @@ public partial class PropGrabber : Node bool grip = controller.GetFloat("grip") > GripThreshold; int other = 1 - hand; - if (grip && !_gripPrev[hand] && _held[hand] == null && _easing[hand] == null) + double now = Time.GetTicksMsec() / 1000.0; + if (grip && !_gripPrev[hand]) + _bufferUntil[hand] = now + GrabBuffer; + if (grip && now <= _bufferUntil[hand] && _held[hand] == null && _easing[hand] == null) { PhysicsBody3D prop = FindProp(controller, hand); RigidBody3D handBody = PhysicsHands.GetBody(controller); if (prop != null && handBody != null) + { _easing[hand] = SoftGrab.Begin(handBody, hand, prop); // welded once it's in the hand (_PhysicsProcess) + _bufferUntil[hand] = 0.0; + } } if (!grip) _easing[hand] = null; diff --git a/scripts/RagdollSpike.cs b/scripts/RagdollSpike.cs index 17321e3..4d2f0cb 100644 --- a/scripts/RagdollSpike.cs +++ b/scripts/RagdollSpike.cs @@ -84,6 +84,7 @@ public partial class RagdollSpike : Node3D _phases.Add(("hinge sign", SetupHinge, TickHinge, 0.3f, FinishHinge)); _phases.Add(("pose filter: render-timed tracking moves evenly (no 18 Hz beat)", Rest, _ => { }, 0.05f, FinishPoseFilter)); + _phases.Add(("elbow: a controller turned outward doesn't fold the elbow inward", Rest, _ => { }, 0.05f, FinishElbowOut)); _phases.Add(("stands still, head on the camera, hands on the controllers", Rest, _ => { }, 4f, FinishStand)); _phases.Add(("reaches forward, out and overhead", Rest, TickReach, 4.5f, FinishReach)); _phases.Add(("fast arm swings stay attached", Rest, TickSwing, 3f, FinishSwing)); @@ -234,6 +235,26 @@ public partial class RagdollSpike : Node3D private int NewSnaps => _ragdoll.Snaps - _snapsAtStart; + // Right arm reaching forward-down, controller yawed 0..90° outward: the elbow must stay on the + // outer side of the shoulder→wrist line (toward +X), never toward the other arm. + private static bool FinishElbowOut() + { + float worst = float.MaxValue; + for (int deg = 0; deg <= 90; deg += 15) + { + ArmSolver arm = new(); + Vector3 shoulder = new(0.2f, 1.45f, 0f), target = new(0.25f, 1.05f, -0.4f); + Basis hand = new Basis(Vector3.Up, -Mathf.DegToRad(deg)) * new Basis(Vector3.Right, -0.6f); + for (int i = 0; i < 200; i++) + arm.Solve(shoulder, target, Basis.Identity, Vector3.Right, hand, 0.3f, 0.27f, 1f / 144f); + Vector3 dir = (arm.Wrist - shoulder).Normalized(); + Vector3 mid = shoulder + dir * (arm.Elbow - shoulder).Dot(dir); + worst = Mathf.Min(worst, arm.Elbow.X - mid.X); // < 0 = elbow toward the midline + } + GD.Print($"RAGDOLL: elbow vs the arm line with the controller turned 0..90° outward: most inward {worst:F3} m (limit -0.04)"); + return worst > -0.04f; + } + // A controller moving at 1 m/s, sampled once per 90 Hz frame (predicted for that frame's render // time), read by 144 Hz ticks. Per-tick output steps should all be 1/144 m: timestamped by // arrival they alternate (1 or 2 ticks late), timestamped by render time they don't. @@ -1117,7 +1138,8 @@ public partial class RagdollSpike : Node3D { float t = _t; // Walk up to a waist-high ledge (stop before its far end); a chest-high one starts within reach. - _stickVelocity = (t < 1f && _ledgeTop < 1f) || (t > 4.5f && t < 5.5f) ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero; + // (A 70 cm box is too small to walk on after climbing it.) + _stickVelocity = (t < 1f && _ledgeTop < 1f) || (t > 4.5f && t < 5.5f && _ledgeBox == null) ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero; // Crouch to load the legs for a waist-high ledge; at a chest-high one you reach up instead. float crouch = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 1f) / 0.8f, 0f, 1f)) * (_ledgeTop < 1f ? 1f : 0f); float rise = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 2.2f) / 1.6f, 0f, 1f));