Body step 2.6: landing dip, jump, stick crouch, no flip looking down, smooth view

- Look-down flip: body facing from BodyMath.HeadFacing (visor + crown), robust past vertical;
  stick direction uses it too; crouch lean pitch measured against the facing.
- Landing: falling legs touch down only within their reach, so leg strength stops the body
  below its stance (dip scales with impact, big falls fold the legs). Rise eased so gravity
  can stop it at the stance; legs untuck speed-limited. Fixed the vertical play-space
  correction leaking into ground control's feed-forward (a bounce).
- Jump on right A: countermovement dip + leg push over the stroke (crouch = higher).
- Stick crouch on right stick Y (RagdollDriver.StickCrouch: virtual floor offset).
- Menu summon on left X only.
- XR origin drawn with the bodies' physics interpolation (camera/body desync).
- RagdollSpike: landing/fall/jump/look-down/stick-crouch phases, `-- only=` filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 17:25:15 +03:00
1 parent 84df772f38
commit e821728fba
7 files changed
+374 -38

No files matched your search

+20
View File
@@ -20,6 +20,26 @@ public static class BodyMath
return v * (2f * Mathf.Atan2(s, q.W) / s);
}
// The horizontal direction a head faces, robust past vertical. The visor (-Z) projected flat
// flips 180° once you look down (or up) beyond vertical — chin to chest while crouching, or
// looking at your own feet — and everything built on it (torso yaw, stick direction) turned
// around. Looking down, the top of the head (+Y) points the way you face; looking up it points
// back. So: when the flat visor disagrees with the flat crown, the head is past vertical and the
// visor is reversed; near vertical the crown alone carries the direction. Zero if undefined.
public static Vector3 HeadFacing(Basis head)
{
Vector3 forward = -head.Z, up = head.Y;
Vector3 visor = new(forward.X, 0f, forward.Z);
Vector3 crown = new Vector3(up.X, 0f, up.Z) * -forward.Y;
if (visor.Dot(crown) < 0f)
visor = -visor;
Vector3 f = visor + crown * 0.5f;
return f.LengthSquared() > 1e-6f ? f.Normalized() : Vector3.Zero;
}
// Yaw (about +Y, Godot convention: 0 = facing -Z) of a horizontal direction.
public static float YawOf(Vector3 flat) => Mathf.Atan2(-flat.X, -flat.Z);
public static bool IsFinite(RigidBody3D body) =>
body.GlobalTransform.IsFinite() && body.LinearVelocity.IsFinite() && body.AngularVelocity.IsFinite();
+108 -8
View File
@@ -57,6 +57,22 @@ public partial class PlayerRagdoll : Node3D
public const float GroundProbeExtra = 0.3f; // ground counts while within hip height + this
public const float MaxStepHeight = 0.35f;
public const float MaxRiseSpeed = 1.2f; // m/s the legs lift the body toward its stance
// Landing (owner 2.6: the sink is feedback, it just had to be followed by the view): falling legs
// only touch down when the hips are within their real reach of the ground. Before, support
// began GroundProbeExtra early and braked the fall in the air — no dip at all. Now LegStrength
// stops the body BELOW its stance, so the dip scales with the impact: ~0.13 m from a hop,
// ~0.3 m from 1 m, legs fully folded from a big fall (Boneworks) — then the normal rise.
public const float LandingReach = 0.05f; // m past the pose's hip height
public const float AbsorbTime = 0.4f; // s after a hard touchdown the head guide stays off (it would hold the dip up)
// Jump (right A): a short countermovement dip, then the legs push JumpForce over the stroke up to
// full extension. Takeoff speed is physics — v² = 2·(F/m − g)·stroke — so a crouch before
// the press (real or stick) jumps higher: ~0.45 m standing, ~0.7 m from a deep crouch.
public const float JumpForce = 2.4f; // × body weight
public const float JumpDip = 0.2f; // m the hips drop first when pressed from standing
public const float JumpExtension = 0.06f; // m above the standing hips at toe-off
public const float JumpMaxStroke = 0.5f;
public const float JumpBuffer = 0.15f; // s a press is remembered before touchdown
public const float CoyoteTime = 0.12f; // s after leaving the ground a jump still works
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 CrawlCarry = 300f;
@@ -67,7 +83,7 @@ public partial class PlayerRagdoll : Node3D
// 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).
public const float LegsMass = 6f;
public const float LegsKp = 30f, LegsForce = 3000f, LegsTorque = 400f;
public const float LegsKp = 30f, LegsForce = 3000f, LegsTorque = 400f, LegsMaxSpeed = 1.5f;
public const float LegsOffAbove = 0.25f; // collider disabled once slid this far above the hips
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
@@ -150,6 +166,7 @@ public partial class PlayerRagdoll : Node3D
// 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.
public Vector3 HeldBack { get; private set; }
public bool Jumping => _jump != JumpPhase.None;
// Really climbing: a hand holds static world, or the arms are lifting the body (mantle).
public bool Climbing => Built && (_arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D || _lifted);
public RigidBody3D Legs { get; private set; }
@@ -180,6 +197,14 @@ public partial class PlayerRagdoll : Node3D
return GetWorld3D().DirectSpaceState.IntersectShape(_legsQuery, 1).Count == 0;
}
private Vector3 _goalVelocity;
private enum JumpPhase { None, Dip, Push, Flight }
private JumpPhase _jump;
private float _jumpT, _jumpRequest, _airT, _jumpFromY, _sinceLanding = float.MaxValue;
// Right A. Buffered, so a press just before touchdown still jumps.
public void Jump() => _jumpRequest = JumpBuffer;
private float _standingHip; // hips of the standing pose above the feet = the legs' full extension
private static float LowPass(float dt, float tau) => 1f - Mathf.Exp(-dt / tau);
private bool _lifted;
@@ -233,6 +258,7 @@ public partial class PlayerRagdoll : Node3D
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);
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);
Legs = MakeBody("Legs", new BoxShape3D { Size = new Vector3(0.28f * s, _legsHeight, 0.2f * s) }, LegsMass, BodyLayer, WorldMask, pants,
c * new Transform3D(Basis.Identity, hip - new Vector3(0f, _legsHeight * 0.5f, 0f)), visible: false);
@@ -321,6 +347,9 @@ public partial class PlayerRagdoll : Node3D
for (int i = 0; i < 2; i++)
_arms[i] = new Arm();
Built = false;
_jump = JumpPhase.None;
_jumpRequest = 0f;
_sinceLanding = float.MaxValue;
}
// Twist axis (joint X) along `dir`.
@@ -618,7 +647,7 @@ public partial class PlayerRagdoll : Node3D
_headGoalVelocity = _headGoalVelocity.Lerp(raw, LowPass(dt, GroundFeedForwardTau));
// Only on your feet: hands bracing (mantling, pushing yourself) or crawling move the body by
// other means, and the view simply follows it.
if (!Grounded || Climbing || Braced)
if (!Grounded || Climbing || Braced || _jump != JumpPhase.None || _sinceLanding < AbsorbTime)
return;
float w = Mathf.Tau * HeadGuideFrequency;
Vector3 force = ((goal - Eye) * (w * w) + (_headGoalVelocity - Head.LinearVelocity) * (2f * w)) * HeadGuideMass;
@@ -701,7 +730,9 @@ public partial class PlayerRagdoll : Node3D
center = hip.Y; // tucked into the pelvis, out of the way
Vector3 desired = new(hip.X, center, hip.Z);
Quaternion frameA = Rot(Pelvis) * _legsDrive.FrameA;
SetLinearVelocity(_legsDrive.Joint, frameA.Inverse() * ((desired - Legs.GlobalPosition) * LegsKp));
// Speed-limited: the legs dropping back out of the tuck at full drive speed kicked the pelvis
// up (the reaction) just as a landing dip was rising — a 5 cm bounce.
SetLinearVelocity(_legsDrive.Joint, frameA.Inverse() * ((desired - Legs.GlobalPosition) * LegsKp).LimitLength(LegsMaxSpeed));
Quaternion target = (Rot(Pelvis) * _legsDrive.FrameA).Inverse() * (s.YawBasis.GetRotationQuaternion() * _legsDrive.FrameB);
Quaternion current = frameA.Inverse() * (Rot(Legs) * _legsDrive.FrameB);
SetAngularVelocity(_legsDrive.Joint, (BodyMath.RotationError(target, current) * LegsKp).LimitLength(MuscleMaxSpin));
@@ -793,9 +824,16 @@ public partial class PlayerRagdoll : Node3D
Vector3 normal = (Vector3)hit["normal"];
_groundBody = GodotObject.InstanceFromId((ulong)hit["collider_id"]) as RigidBody3D; // Box3D: "collider" is null
_groundPoint = point;
bool inReach = pos.Y - point.Y < hipAboveFloor + GroundProbeExtra;
// 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.
float reach = wasGrounded && _jump != JumpPhase.Flight ? GroundProbeExtra : LandingReach;
bool inReach = pos.Y - point.Y < hipAboveFloor + reach;
bool steep = normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees));
if (inReach && steep && wasGrounded && point.Y <= _footY + MaxStepHeight)
if (_jump == JumpPhase.Flight && Pelvis.LinearVelocity.Y > 0f)
{
// Still rising off the ground: nothing to stand on yet.
}
else if (inReach && steep && wasGrounded && point.Y <= _footY + MaxStepHeight)
{
// The foot of a too-steep slope: keep standing on the ground we're on (the legs
// collider stops us); dropping support here would collapse the body.
@@ -835,20 +873,34 @@ public partial class PlayerRagdoll : Node3D
Vector3 targetVelocity = _goalVelocity;
float weight = BodyMass * 9.8f;
if (Grounded && !wasGrounded && Pelvis.LinearVelocity.Y < -1.5f)
_sinceLanding = 0f;
_sinceLanding += dt;
UpdateJump(dt, wasGrounded);
if (_jump == JumpPhase.Push)
Grounded = true; // the feet push off the ground they were on (also over a just-left edge)
if (Grounded)
{
// Pose heights are relative to the tracking floor; stand on the ground actually under us.
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
_supportGoalY = goal.Y;
if (_jump == JumpPhase.Dip)
goal.Y = Mathf.Min(goal.Y, _jumpFromY);
float w = Mathf.Tau * GroundFrequency;
Vector3 accel = (goal - pos) * (w * w) + (targetVelocity - Pelvis.LinearVelocity) * (2f * GroundDamping * w);
Vector3 force = accel * BodyMass;
// Vertical as a rate-limited velocity servo: full leg strength brakes a landing, but
// the legs raise the body at most MaxRiseSpeed — they can push, not pull, so a hard push
// toward a higher stance (onto a ledge) would otherwise overshoot into a jump.
float riseVelocity = Mathf.Clamp((goal.Y - pos.Y) * w, -3f, MaxRiseSpeed) + targetVelocity.Y;
// Rising, the legs can only stop the body by letting gravity do it (support ≥ 0): ease the
// rise so it can still be stopped at the stance with ~0.7 g, or it overshoots (a bounce).
float below = goal.Y - pos.Y;
float stoppable = below > 0f ? Mathf.Sqrt(2f * 0.7f * 9.8f * below) : 0f;
float riseVelocity = Mathf.Clamp(below * w, -3f, Mathf.Min(MaxRiseSpeed, stoppable)) + targetVelocity.Y;
float verticalAccel = (riseVelocity - Pelvis.LinearVelocity.Y) * (2f * GroundDamping * w);
float support = Mathf.Clamp(verticalAccel * BodyMass + weight, 0f, LegStrength * weight);
if (_jump == JumpPhase.Push)
support = JumpForce * weight;
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).
@@ -885,6 +937,50 @@ public partial class PlayerRagdoll : Node3D
}
}
// Jump state: request → Dip (countermovement, the support goal drops) → Push (full leg force up to
// extension) → Flight (no ground until falling again) → None on touchdown.
private void UpdateJump(float dt, bool wasGrounded)
{
_airT = Grounded ? 0f : _airT + dt;
_jumpRequest -= dt;
_jumpT += dt;
float pos = Pelvis.GlobalPosition.Y;
float top = _footY + _standingHip + JumpExtension;
switch (_jump)
{
case JumpPhase.None:
if (_jumpRequest > 0f && (Grounded || _airT < CoyoteTime) && !Climbing)
{
_jumpRequest = 0f;
_jumpT = 0f;
// Stroke at least JumpDip + JumpExtension: from standing, dip first.
_jumpFromY = Mathf.Max(top - JumpMaxStroke, Mathf.Min(pos, top - JumpDip - JumpExtension));
_jump = pos > _jumpFromY + 0.02f && Grounded ? JumpPhase.Dip : JumpPhase.Push;
_jumpFromY = Mathf.Min(_jumpFromY, pos);
}
break;
case JumpPhase.Dip:
if (pos <= _jumpFromY + 0.02f || _jumpT > 0.2f || !Grounded)
{
_jump = JumpPhase.Push;
_jumpFromY = pos;
_jumpT = 0f;
}
break;
case JumpPhase.Push:
if (pos >= top || pos - _jumpFromY >= JumpMaxStroke || _jumpT > 0.35f)
{
_jump = JumpPhase.Flight;
_jumpT = 0f;
}
break;
case JumpPhase.Flight:
if (Grounded && _jumpT > 0.05f)
_jump = JumpPhase.None;
break;
}
}
private Drive _balance;
private float _balanceCap;
@@ -931,8 +1027,12 @@ public partial class PlayerRagdoll : Node3D
correction.Y += (_footY - floorY) * (1f - Mathf.Exp(-dt / FloorRecenterTau));
// 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).
_pelvisTargetPrev += correction;
_headGoalPrev += correction;
// Horizontally only — vertically the goals are re-based on the real ground (− floorY), so
// they don't move with the play space; shifting them too turned the view riding a landing
// dip into a ~1 m/s upward feed-forward (a 15 cm bounce).
Vector3 flat = new(correction.X, 0f, correction.Z);
_pelvisTargetPrev += flat;
_headGoalPrev += flat;
return correction;
}
+60 -4
View File
@@ -4,7 +4,8 @@ using Godot;
// Boneworks-style stick locomotion and the play-space reconciliation. Runs everything in one
// physics tick, in this order:
// 1. discrete origin jumps made by other code (spawn, teleport markers) carry the body along;
// 2. stick: left = move (head-relative), right X = turn (snap by default, SmoothTurn for smooth);
// 2. stick: left = move (head-relative), right X = turn (snap by default, SmoothTurn for smooth),
// right Y = crouch (down lowers, up raises — RagdollDriver.StickCrouch); right A = jump;
// 3. the ragdoll's OriginCorrection (push-back at walls, floor follows the ground, falls);
// 4. True Body solve from the corrected camera/controllers, then the ragdoll's muscles.
// Waits for the calibrated BodyProfile like everything else; rebuilds on recalibration.
@@ -18,6 +19,8 @@ public partial class PlayerRig : Node
public const float SmoothTurnSpeed = 2.5f; // rad/s
public const float SnapTurnThreshold = 0.7f;
public const float SnapTurnRearm = 0.4f;
public const float StickCrouchSpeed = 1.2f; // m/s at full stick
public const float StickCrouchHeadroom = 0.5f; // the stick can lower the eyes to this height (a crawl)
public const float JumpDistance = 0.5f; // an origin move this big in one tick = teleport, not motion
public const float JumpAngle = 0.1f; // rad
private const float GhostLagDistance = 0.1f;
@@ -33,9 +36,17 @@ public partial class PlayerRig : Node
private bool _haveLastOrigin;
private Vector3 _moveVelocity;
private bool _snapArmed = true;
private bool _jumpPrev;
private readonly MeshInstance3D[] _ghosts = new MeshInstance3D[2];
private bool _rebuild = true;
private RagdollDriver _driver;
// Render interpolation of the play space (smooth-camera bible, VR edition): physics moves the
// origin every tick, but the body is DRAWN interpolated between the last two ticks. A camera
// stepping with the raw origin while the body glides a tick behind looks like the body is in
// two places at once when you move and look at it. So physics owns _physOrigin and every
// frame the visible origin is the same interpolation of it the bodies get.
private Transform3D _physOrigin, _prevPhysOrigin, _shownOrigin;
private bool _interpolateOrigin;
public PlayerRagdoll Ragdoll => _ragdoll;
public static bool Spectator { get; private set; }
@@ -70,6 +81,8 @@ public partial class PlayerRig : Node
public override void _Ready()
{
_instance = this;
ProcessPriority = -100; // place the interpolated origin before anything reads it this frame
ProcessPhysicsPriority = -100; // restore the physics origin before anything uses it this tick
_origin = GetNode<XROrigin3D>("../XROrigin3D");
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
@@ -114,8 +127,10 @@ public partial class PlayerRig : Node
if (profile == null || Spectator)
{
_haveLastOrigin = false;
_interpolateOrigin = false;
return;
}
RestorePhysicsOrigin();
// 1. Someone else moved the play space discretely: bring the body along.
Transform3D origin = _origin.GlobalTransform;
@@ -134,10 +149,34 @@ public partial class PlayerRig : Node
_origin.GlobalTransform = o;
_lastOrigin = _origin.GlobalTransform;
_haveLastOrigin = true;
_prevPhysOrigin = _interpolateOrigin && !IsJump(_physOrigin, _lastOrigin) ? _physOrigin : _lastOrigin;
_physOrigin = _shownOrigin = _lastOrigin;
_interpolateOrigin = true;
}
// The origin on screen is interpolated; physics must run on the real one. Anyone else who moved
// the origin since (teleport marker, menu) is respected as the new real one.
private void RestorePhysicsOrigin()
{
if (!_interpolateOrigin)
return;
Transform3D current = _origin.GlobalTransform;
if (current.IsEqualApprox(_shownOrigin))
_origin.GlobalTransform = _physOrigin;
else
_physOrigin = _prevPhysOrigin = current;
}
public override void _Process(double delta)
{
if (_interpolateOrigin && !Spectator)
{
Transform3D current = _origin.GlobalTransform;
if (!current.IsEqualApprox(_shownOrigin))
_physOrigin = _prevPhysOrigin = current; // moved by someone else: show it as is
_shownOrigin = _prevPhysOrigin.InterpolateWith(_physOrigin, (float)Engine.GetPhysicsInterpolationFraction());
_origin.GlobalTransform = _shownOrigin;
}
for (int i = 0; i < 2; i++)
{
XRController3D c = i == 0 ? _left : _right;
@@ -178,9 +217,9 @@ public partial class PlayerRig : Node
Vector3 desired = Vector3.Zero;
if (move.Length() > StickDeadzone)
{
Vector3 fwd = -_camera.GlobalTransform.Basis.Z;
fwd.Y = 0f;
fwd = fwd.LengthSquared() > 1e-6f ? fwd.Normalized() : Vector3.Forward;
Vector3 fwd = BodyMath.HeadFacing(_camera.GlobalTransform.Basis.Orthonormalized());
if (fwd == Vector3.Zero)
fwd = -Solver.YawBasis.Z;
Vector3 rightDir = fwd.Cross(Vector3.Up);
float mag = Mathf.Clamp((move.Length() - StickDeadzone) / (1f - StickDeadzone), 0f, 1f);
desired = (fwd * move.Y + rightDir * move.X).Normalized() * (mag * MoveSpeed);
@@ -192,6 +231,23 @@ public partial class PlayerRig : Node
if (_moveVelocity != Vector3.Zero)
_origin.GlobalPosition += _moveVelocity * dt;
// Jump: right A, on the press.
bool jump = _right.IsButtonPressed("ax_button");
if (jump && !_jumpPrev)
_ragdoll.Jump();
_jumpPrev = jump;
// Crouch: right stick held down lowers the body, up raises it back (the stick's dominant
// axis decides between crouch and turn).
bool vertical = Mathf.Abs(turn.Y) > Mathf.Abs(turn.X);
if (vertical && Mathf.Abs(turn.Y) > StickDeadzone)
{
float max = Mathf.Max(0f, BodyProfile.Current.EyeHeight - StickCrouchHeadroom);
_driver.StickCrouch = Mathf.Clamp(_driver.StickCrouch - turn.Y * StickCrouchSpeed * dt, 0f, max);
}
if (vertical)
turn.X = 0f;
// Turn about the head.
float angle = 0f;
if (SmoothTurn)
+9 -3
View File
@@ -68,6 +68,11 @@ public sealed class RagdollDriver
public TrueBodySolver Solver;
public PlayerRagdoll Ragdoll;
public bool RebuildRequested = true;
// Stick crouch (right stick down), metres: the play-space floor is treated as this far ABOVE where
// it is. The True Body then reads the head as that much lower (crouch pose), ground control and
// the view's re-centering settle the play space that far below the ground — so the camera rides
// down with the crouching body, and nothing feeds back (same play-space-relative rule as 2.5).
public float StickCrouch;
private double _now;
// origin = play space (modified by the correction); raw* = origin-local tracked poses as last
@@ -79,15 +84,16 @@ public sealed class RagdollDriver
_now += dt;
Transform3D head = Head.Sample(rawHead, _now), left = Left.Sample(rawLeft, _now), right = Right.Sample(rawRight, _now);
if (Ragdoll.Built && !RebuildRequested)
origin.Origin += Ragdoll.OriginCorrection((origin * head).Origin, origin.Origin.Y, dt);
origin.Origin += Ragdoll.OriginCorrection((origin * head).Origin, origin.Origin.Y + StickCrouch, dt);
float floorY = origin.Origin.Y + StickCrouch;
Transform3D h = origin * head, l = origin * left, r = origin * right;
Solver.Solve(profile, h, l, r, origin.Origin.Y, dt);
Solver.Solve(profile, h, l, r, floorY, dt);
if (RebuildRequested || !Ragdoll.Built)
{
Ragdoll.Build(profile, Solver, l, r);
RebuildRequested = false;
}
Vector3 lv = origin.Basis * Left.Velocity + moveVelocity, rv = origin.Basis * Right.Velocity + moveVelocity;
Ragdoll.Step(Solver, l, r, lv, rv, origin.Origin.Y, dt);
Ragdoll.Step(Solver, l, r, lv, rv, floorY, dt);
}
}
+164 -14
View File
@@ -38,6 +38,7 @@ public partial class RagdollSpike : Node3D
private Transform3D Right => _origin * Tracked(2, _rightLocal);
private bool _realInput = true, _noiseOn = true, _holdOn = true;
private string[] _only; // `-- only=jump,fall`: run just the phases whose names contain one of these
private const float TrackingHz = 90f;
private readonly Transform3D[] _held = new Transform3D[3];
private readonly long[] _heldFrame = { -1, -1, -1 };
@@ -71,6 +72,8 @@ public partial class RagdollSpike : Node3D
_noiseOn = false;
if (arg == "nohold")
_holdOn = false;
if (arg.StartsWith("only="))
_only = arg[5..].Split(',');
}
GD.Print($"RAGDOLL: input mode {(_realInput ? "REAL (90 Hz tracking, jitter)" : "ideal")} at {Engine.PhysicsTicksPerSecond} Hz physics");
StaticBody3D floor = MakeStatic(new Vector3(40f, 1f, 40f), new Vector3(0f, -0.5f, 0f), 29);
@@ -106,11 +109,18 @@ public partial class RagdollSpike : Node3D
_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, 8f, FinishClimbWall));
_phases.Add(("walks off a 1 m ledge and lands without sinking away from the view", SetupLedgeTop, TickDrop, 4f, FinishDrop));
_phases.Add(("walks off a 1 m ledge and lands: a dip the view follows, no bounce", () => SetupLedgeTop(1f), TickDrop, 4f, () => FinishDrop(1f)));
_phases.Add(("big fall (5 m): legs fold fully, then it recovers", () => SetupLedgeTop(5f), TickDrop, 5f, () => FinishDrop(5f)));
_phases.Add(("standing jump", Rest, t => TickJump(0f), 4f, () => FinishJump(crouched: false)));
_phases.Add(("jump from a crouch goes higher", Rest, t => TickJump(0.45f), 4.5f, () => FinishJump(crouched: true)));
_phases.Add(("looks down past vertical while crouched (body doesn't flip around)", Rest, TickLookDown, 5f, FinishLookDown));
_phases.Add(("stick crouch lowers the body and the view, and stands back up", Rest, TickStickCrouch, 6f, FinishStickCrouch));
_phases.Add(("standing on a seesaw tips it (body weight reaches movable ground)", SetupSeesaw, _ => { }, 3f, FinishSeesaw));
_phases.Add(("playground: swing board hangs, seesaw on its pivot, big plank grabbable", SetupPlayground, _ => { }, 2f, FinishPlayground));
_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)));
if (_only != null)
_phases.RemoveAll(ph => ph.Name != "hinge sign" && !Array.Exists(_only, o => ph.Name.Contains(o)));
Next();
}
@@ -153,6 +163,8 @@ public partial class RagdollSpike : Node3D
_finite = true;
_measureFrom = 1f;
_stickVelocity = Vector3.Zero;
if (_driver != null)
_driver.StickCrouch = 0f;
_phases[_phase].Setup();
_snapsAtStart = _ragdoll.Snaps;
}
@@ -662,38 +674,176 @@ public partial class RagdollSpike : Node3D
}
// Start standing on a 1 m platform whose edge is 0.5 m ahead.
private void SetupLedgeTop()
// Start standing on a platform whose edge is 0.5 m ahead.
private void SetupLedgeTop(float height)
{
Rest();
_origin = new Transform3D(Basis.Identity, new Vector3(0f, 1f, 0f));
_stage.AddChild(MakeStatic(new Vector3(3f, 1f, 3f), new Vector3(0f, 0.5f, 1f), 29));
_origin = new Transform3D(Basis.Identity, new Vector3(0f, height, 0f));
_stage.AddChild(MakeStatic(new Vector3(3f, height, 3f), new Vector3(0f, height * 0.5f, 1f), 29));
_dropEyeErr = _dropEyeErrLate = _dropRebound = 0f;
_dropMinPelvis = float.MaxValue;
_dropStand = 0f;
}
private float _dropEyeErr, _dropMinPelvis = float.MaxValue;
private float _dropEyeErr, _dropEyeErrLate, _dropMinPelvis = float.MaxValue, _dropStand, _dropRebound;
private void TickDrop(float dt)
{
_stickVelocity = _t > 0.5f && _t < 2f ? new Vector3(0f, 0f, -1.2f) : Vector3.Zero;
if (_ragdoll.Built && !_needBuild && _t > 0.5f)
if (!_ragdoll.Built || _needBuild)
return;
float pelvis = _ragdoll.Pelvis.GlobalPosition.Y;
if (_t < 0.5f)
_dropStand = pelvis - _ragdoll.GroundY; // standing hips above the feet
else
{
_dropEyeErr = Mathf.Max(_dropEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
if (_ragdoll.Grounded && _ragdoll.GroundY < 0.5f)
_dropMinPelvis = Mathf.Min(_dropMinPelvis, _ragdoll.Pelvis.GlobalPosition.Y);
{
if (pelvis < _dropMinPelvis)
{
_dropMinPelvis = pelvis;
_dropRebound = pelvis;
}
_dropRebound = Mathf.Max(_dropRebound, pelvis);
}
if (_dropMinPelvis < float.MaxValue && _ragdoll.Pelvis.LinearVelocity.Y > -0.5f)
_dropEyeErrLate = Mathf.Max(_dropEyeErrLate, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishDrop()
private bool FinishDrop(float height)
{
GD.Print($"RAGDOLL: landing: eye vs camera max {_dropEyeErr:F3} m (limit 0.15), lowest pelvis after touchdown {_dropMinPelvis:F3} m (standing 0.99, limit > 0.7)");
bool landed = _dropEyeErr < 0.15f && _dropMinPelvis > 0.7f;
_dropEyeErr = 0f;
_dropMinPelvis = float.MaxValue;
float dip = _dropStand - _dropMinPelvis, overshoot = _dropRebound - _dropStand;
// The dip grows with the impact; a big fall folds the legs (hips near the ground). Rebound
// past the stance would be a bounce. The view rides the body throughout.
bool dipOk = height < 2f ? dip > 0.12f && dip < 0.5f : dip > 0.45f;
GD.Print($"RAGDOLL: landing from {height} m: dip {dip:F3} m (stance {_dropStand:F3}), rebound above the stance {overshoot:F3} (limit 0.05), eye vs camera max {_dropEyeErr:F3} / after the impact {_dropEyeErrLate:F3} (limit 0.1)");
bool landed = dipOk && overshoot < 0.05f && _dropEyeErrLate < 0.1f;
if (!landed)
return false;
GD.Print($"RAGDOLL: drop: feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, pelvis {_ragdoll.Pelvis.GlobalPosition}, eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
return _finite && NewSnaps == 0 && Mathf.Abs(_ragdoll.GroundY) < 0.05f && Mathf.Abs(_origin.Origin.Y) < 0.1f
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > -3f;
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > -3f
&& Mathf.Abs(_ragdoll.Pelvis.GlobalPosition.Y - _ragdoll.GroundY - _dropStand) < 0.05f;
}
// ---------------------------------------------------------------- jump
private float _jumpStand, _jumpApex, _jumpEyeErr, _standJumpApex;
private bool _jumpPressed;
// Optionally squat (eyes `crouch` lower) first, then press jump at 2 s; the player keeps the pose.
private void TickJump(float crouch)
{
float k = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((_t - 0.8f) / 0.8f, 0f, 1f)) * (crouch > 0f ? 1f : 0f);
_headLocal = new Transform3D(new Basis(Vector3.Right, -0.2f * k), new Vector3(0f, EyeHeight - crouch * k, -0.05f * k));
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f - crouch * k, -0.3f));
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f - crouch * k, -0.3f));
_measureFrom = float.MaxValue;
if (!_ragdoll.Built || _needBuild)
return;
float pelvis = _ragdoll.Pelvis.GlobalPosition.Y;
if (_t < 0.7f)
{
_jumpStand = pelvis;
_jumpApex = 0f;
_jumpEyeErr = 0f;
_jumpPressed = false;
}
if (_t >= 2f && !_jumpPressed)
{
_jumpPressed = true;
_ragdoll.Jump();
}
if (_jumpPressed)
{
_jumpApex = Mathf.Max(_jumpApex, pelvis - _jumpStand);
_jumpEyeErr = Mathf.Max(_jumpEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishJump(bool crouched)
{
GD.Print($"RAGDOLL: {(crouched ? "crouched" : "standing")} jump: hips rose {_jumpApex:F3} m above the standing stance, eye vs camera max {_jumpEyeErr:F3}, grounded {_ragdoll.Grounded}, jumping {_ragdoll.Jumping}, snaps {NewSnaps}");
bool ok = _finite && NewSnaps == 0 && _ragdoll.Grounded && !_ragdoll.Jumping && _jumpEyeErr < 0.12f && _origin.Origin.Length() < 0.15f;
if (!crouched)
{
_standJumpApex = _jumpApex;
return ok && _jumpApex > 0.3f && _jumpApex < 0.65f;
}
return ok && _jumpApex > _standJumpApex + 0.1f;
}
// ---------------------------------------------------------------- looking down past vertical
private float _lookYaw0, _lookYawMax, _lookChestMax, _lookPitchMax;
private static float YawOfBody(RigidBody3D b)
{
Vector3 f = BodyMath.HeadFacing(b.GlobalTransform.Basis.Orthonormalized());
return f == Vector3.Zero ? 0f : BodyMath.YawOf(f);
}
// Crouch (eyes 1.0 m), then look down to 125° below the horizon (chin on the chest, looking
// behind the feet) and back, hands down near the floor — interacting with stuff on the ground.
private void TickLookDown(float dt)
{
float k = Mathf.SmoothStep(0f, 1f, Mathf.Min(_t / 1f, 1f));
float pitch = Mathf.DegToRad(125f) * Mathf.Sin(Mathf.Pi * Mathf.Clamp((_t - 1.5f) / 3f, 0f, 1f));
_lookPitchMax = Mathf.Max(_lookPitchMax, pitch);
Basis head = new(Vector3.Right, -pitch);
Vector3 pivot = new(0f, Mathf.Lerp(EyeHeight, 1.0f, k) + TrueBodySolver.EyeToNeckPivot.Y, -0.1f * k + TrueBodySolver.EyeToNeckPivot.Z);
_headLocal = new Transform3D(head, pivot - head * TrueBodySolver.EyeToNeckPivot);
_leftLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward), new Vector3(-0.2f, Mathf.Lerp(1.0f, 0.3f, k), -0.4f));
_rightLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward), new Vector3(0.2f, Mathf.Lerp(1.0f, 0.3f, k), -0.4f));
_measureFrom = float.MaxValue;
if (!_ragdoll.Built || _needBuild)
return;
if (_t < 1.4f)
{
_lookYaw0 = YawOfBody(_ragdoll.Pelvis);
_lookYawMax = _lookChestMax = _lookPitchMax = 0f;
return;
}
_lookYawMax = Mathf.Max(_lookYawMax, Mathf.Abs(Mathf.AngleDifference(_lookYaw0, YawOfBody(_ragdoll.Pelvis))));
_lookChestMax = Mathf.Max(_lookChestMax, Mathf.Abs(Mathf.AngleDifference(_lookYaw0, YawOfBody(_ragdoll.Chest))));
}
private bool FinishLookDown()
{
GD.Print($"RAGDOLL: look down to {Mathf.RadToDeg(_lookPitchMax):F0}°: pelvis yaw change max {Mathf.RadToDeg(_lookYawMax):F1}°, chest {Mathf.RadToDeg(_lookChestMax):F1}° (limit 15), eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
return _finite && NewSnaps == 0 && _lookYawMax < Mathf.DegToRad(15f) && _lookChestMax < Mathf.DegToRad(15f)
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.08f;
}
// ---------------------------------------------------------------- stick crouch
private float _stickEyeLow, _stickOriginLow, _stickEyeErr;
private void TickStickCrouch(float dt)
{
_measureFrom = float.MaxValue;
if (_driver == null)
return;
// Hold the stick down for 1 s (1.2 m/s → capped at 0.6 m here), hold, then back up.
float c = _t < 0.5f ? 0f : _t < 3.5f ? Mathf.Min(0.6f, (_t - 0.5f) * 1.2f) : Mathf.Max(0f, 0.6f - (_t - 3.5f) * 1.2f);
_driver.StickCrouch = c;
if (_t > 2.5f && _t < 3.4f && _ragdoll.Built)
{
_stickEyeLow = _ragdoll.Eye.Y;
_stickOriginLow = _origin.Origin.Y;
_stickEyeErr = Mathf.Max(_stickEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishStickCrouch()
{
GD.Print($"RAGDOLL: stick crouch 0.6 m: eye y {_stickEyeLow:F3} (standing {EyeHeight}), origin y {_stickOriginLow:F3}, eye vs camera {_stickEyeErr:F3}; back up: eye y {_ragdoll.Eye.Y:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
bool ok = _finite && NewSnaps == 0 && Mathf.Abs(_stickEyeLow - (EyeHeight - 0.6f)) < 0.1f && Mathf.Abs(_stickOriginLow + 0.6f) < 0.08f
&& _stickEyeErr < 0.06f && Mathf.Abs(_ragdoll.Eye.Y - EyeHeight) < 0.06f && Mathf.Abs(_origin.Origin.Y) < 0.06f;
_stickEyeErr = 0f;
return ok;
}
private Vector3 _crouchPelvis;
+11 -6
View File
@@ -14,6 +14,7 @@ public sealed class TrueBodySolver
public const float ChestLeanMax = 0.75f; // rad (~43°) at a deep crouch
public const float CrouchRange = 0.5f; // crouch factor 1 when eyes are this fraction of standing eye height lower
public const float HeadPitchToChest = 0.25f;
public const float MaxHeadPitch = 2.4f; // rad; chin-to-chest looking at your feet goes past 90°
// Crawl: when the head gets so low that the hips would go through the floor, the trunk tips
// further (pivoting at the hips, hips kicking back) up to nearly horizontal (Nimso: lower = crawl).
public const float CrawlLeanMax = 1.45f; // rad (~83°)
@@ -79,16 +80,19 @@ public sealed class TrueBodySolver
WristTarget[1] = right * new Vector3(0f, 0f, WristOffset);
// Torso yaw: HMD yaw blended toward the hands when they're out in front (decoupled from the head).
// Facing from BodyMath.HeadFacing: looking down past vertical must not turn the body around.
Vector3 headFwd = -headBasis.Z;
float hmdYaw = Mathf.Atan2(-headFwd.X, -headFwd.Z);
Vector3 flatFwd = BodyMath.HeadFacing(headBasis);
if (flatFwd == Vector3.Zero)
flatFwd = new Basis(Vector3.Up, _yaw) * Vector3.Forward;
float hmdYaw = BodyMath.YawOf(flatFwd);
float targetYaw = hmdYaw;
Vector3 toHands = (WristTarget[0] + WristTarget[1]) * 0.5f - Eye;
toHands.Y = 0f;
Vector3 flatFwd = new Vector3(headFwd.X, 0f, headFwd.Z);
if (toHands.LengthSquared() > 1e-4f && flatFwd.LengthSquared() > 1e-6f)
if (toHands.LengthSquared() > 1e-4f)
{
float handsYaw = Mathf.Atan2(-toHands.X, -toHands.Z);
float forwardness = Mathf.Clamp(toHands.Dot(flatFwd.Normalized()) / YawForwardness, 0f, 1f);
float handsYaw = BodyMath.YawOf(toHands);
float forwardness = Mathf.Clamp(toHands.Dot(flatFwd) / YawForwardness, 0f, 1f);
targetYaw = Mathf.LerpAngle(hmdYaw, handsYaw, forwardness * YawHandsWeight);
}
if (!Initialized)
@@ -102,7 +106,8 @@ public sealed class TrueBodySolver
float pivotAboveFloor = NeckPivot.Y - floorY;
float standingPivot = p.EyeHeight + EyeToNeckPivot.Y;
Crouch = Mathf.Clamp((standingPivot - pivotAboveFloor) / (CrouchRange * p.EyeHeight), 0f, 1f);
float headPitchDown = Mathf.Max(0f, -Mathf.Asin(Mathf.Clamp(headFwd.Y, -1f, 1f)));
// Pitch against the facing, so it keeps growing past vertical (asin peaks at 90°).
float headPitchDown = Mathf.Clamp(Mathf.Atan2(-headFwd.Y, headFwd.Dot(flatFwd)), 0f, MaxHeadPitch);
float lean = Crouch * ChestLeanMax + headPitchDown * HeadPitchToChest;
float trunk = TrunkRatio * p.Stature;
float neckLen = Mathf.Max(0.05f, p.ShoulderDrop + EyeToNeckPivot.Y - NeckBaseAboveShoulders);
+2 -3
View File
@@ -1,7 +1,7 @@
using Godot;
// The in-VR menu: a MenuScreen (Godot Control UI in a SubViewport on a thin 0.36x0.24 m
// panel) showing MainMenu. This node is the XR glue: summon/dismiss with ax_button (A/X),
// panel) showing MainMenu. This node is the XR glue: summon/dismiss with the LEFT ax_button (X; right A is jump),
// grip-drag (direct or along the laser), re-orient toward the head, feed both controllers'
// pointer rays into the screen, click sound + haptics. PARENTED UNDER XROrigin3D (fixed in the
// player's room; game locomotion carries it).
@@ -114,8 +114,7 @@ public partial class WorldPanel : Node3D
public override void _Process(double delta)
{
PollSummon(_left, 0);
PollSummon(_right, 1);
PollSummon(_left, 0); // left X only: right A is jump
if (Visible)
{
UpdateDrag(_left, 0);