Spike now runs real input (90 Hz sample-and-hold tracking + jitter under 144 Hz physics), which reproduced the playtest. Root causes fixed: - PoseFilter/RagdollDriver: timestamped extrapolation of tracking, one tick pipeline shared by PlayerRig and the spike - low-pass all feed-forwards (ground control amplified jitter by body mass) - arm muscles back to local targets (physical-frame aim fed chest rotation back) - structural hand strength only when braced or holding - no free-path view chasing while grounded (8 m crouch slide); stick hold-back - explicit inertia for the Legs body (shape-less tuck tumbled the pelvis) - world grab: Box3D shape queries return a null collider, use collider_id - new crouch-hold phases; world-grab detection asserted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
9.0 KiB
C#
220 lines
9.0 KiB
C#
using Godot;
|
|
|
|
// In-game driver of the ragdoll player (body step 2): XR input → True Body → PlayerRagdoll, plus
|
|
// 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);
|
|
// 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.
|
|
public partial class PlayerRig : Node
|
|
{
|
|
public const float MoveSpeed = 2.2f; // m/s at full stick (Boneworks-ish walk)
|
|
public const float MoveAcceleration = 8f; // m/s²
|
|
public const float StickDeadzone = 0.2f;
|
|
public static bool SmoothTurn = false;
|
|
public const float SnapTurnDegrees = 30f;
|
|
public const float SmoothTurnSpeed = 2.5f; // rad/s
|
|
public const float SnapTurnThreshold = 0.7f;
|
|
public const float SnapTurnRearm = 0.4f;
|
|
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;
|
|
|
|
private static PlayerRig _instance;
|
|
|
|
private XROrigin3D _origin;
|
|
private XRCamera3D _camera;
|
|
private XRController3D _left, _right;
|
|
private TrueBody _trueBody;
|
|
private PlayerRagdoll _ragdoll;
|
|
private Transform3D _lastOrigin;
|
|
private bool _haveLastOrigin;
|
|
private Vector3 _moveVelocity;
|
|
private bool _snapArmed = true;
|
|
private readonly MeshInstance3D[] _ghosts = new MeshInstance3D[2];
|
|
private bool _rebuild = true;
|
|
private RagdollDriver _driver;
|
|
|
|
public PlayerRagdoll Ragdoll => _ragdoll;
|
|
public static bool Spectator { get; private set; }
|
|
|
|
// Spectator = no body (fly with the old grab-drag movement); leaving it rebuilds the body at
|
|
// the current pose.
|
|
public static void SetSpectator(bool on)
|
|
{
|
|
Spectator = on;
|
|
AnchorMoveMode.Enabled = on;
|
|
if (_instance == null)
|
|
return;
|
|
if (on)
|
|
_instance._ragdoll.Clear();
|
|
_instance._rebuild = true;
|
|
GD.Print($"PlayerRig: spectator {(on ? "on" : "off")}");
|
|
}
|
|
|
|
public static RigidBody3D GetHandBody(XRController3D controller)
|
|
{
|
|
if (_instance == null || !_instance._ragdoll.Built)
|
|
return null;
|
|
return _instance._ragdoll.Hand(controller == _instance._left ? 0 : 1);
|
|
}
|
|
|
|
public static void SetHeld(XRController3D controller, PhysicsBody3D prop)
|
|
{
|
|
if (_instance != null)
|
|
_instance._ragdoll.SetHeld(controller == _instance._left ? 0 : 1, prop);
|
|
}
|
|
|
|
public override void _Ready()
|
|
{
|
|
_instance = this;
|
|
_origin = GetNode<XROrigin3D>("../XROrigin3D");
|
|
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
|
|
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
|
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
|
_trueBody = GetNodeOrNull<TrueBody>("../TrueBody");
|
|
if (_trueBody != null)
|
|
{
|
|
_trueBody.ExternallySolved = true;
|
|
TrueBody.ShowDebug = false;
|
|
}
|
|
// PlayerRig itself is excluded from physics interpolation (ghosts follow controllers per
|
|
// frame); the body is simulated, so it interpolates.
|
|
_ragdoll = new PlayerRagdoll { Name = "Ragdoll", PhysicsInterpolationMode = PhysicsInterpolationModeEnum.On };
|
|
AddChild(_ragdoll);
|
|
_driver = new RagdollDriver { Solver = Solver, Ragdoll = _ragdoll };
|
|
BodyProfile.Changed += _ => _rebuild = true;
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
_ghosts[i] = new MeshInstance3D
|
|
{
|
|
Mesh = new BoxMesh { Size = PlayerRagdoll.HandSize },
|
|
Visible = false,
|
|
CastShadow = GeometryInstance3D.ShadowCastingSetting.Off,
|
|
};
|
|
_ghosts[i].SetSurfaceOverrideMaterial(0, new StandardMaterial3D
|
|
{
|
|
AlbedoColor = new Color(1f, 1f, 1f, 0.3f),
|
|
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
|
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
|
});
|
|
AddChild(_ghosts[i]);
|
|
}
|
|
}
|
|
|
|
private TrueBodySolver Solver => _trueBody != null ? _trueBody.Solver : _ownSolver;
|
|
private readonly TrueBodySolver _ownSolver = new();
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
{
|
|
float dt = (float)delta;
|
|
BodyProfile profile = BodyProfile.Current;
|
|
if (profile == null || Spectator)
|
|
{
|
|
_haveLastOrigin = false;
|
|
return;
|
|
}
|
|
|
|
// 1. Someone else moved the play space discretely: bring the body along.
|
|
Transform3D origin = _origin.GlobalTransform;
|
|
if (_haveLastOrigin && _ragdoll.Built && IsJump(_lastOrigin, origin))
|
|
Carry(origin * _lastOrigin.AffineInverse());
|
|
|
|
// 2. Stick locomotion.
|
|
if (_ragdoll.Built && !_driver.RebuildRequested)
|
|
Locomote(dt);
|
|
|
|
// 3. Filtered tracking → play-space correction → True Body → muscles (shared with RagdollSpike).
|
|
_driver.RebuildRequested |= _rebuild;
|
|
_rebuild = false;
|
|
Transform3D o = _origin.GlobalTransform;
|
|
_driver.Tick(profile, ref o, _camera.Transform, _left.Transform, _right.Transform, _moveVelocity, dt);
|
|
_origin.GlobalTransform = o;
|
|
_lastOrigin = _origin.GlobalTransform;
|
|
_haveLastOrigin = true;
|
|
}
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
XRController3D c = i == 0 ? _left : _right;
|
|
RigidBody3D hand = _ragdoll.Built ? _ragdoll.Hand(i) : null;
|
|
bool lagging = hand != null && hand.GlobalPosition.DistanceTo(c.GlobalPosition) > GhostLagDistance;
|
|
_ghosts[i].Visible = lagging;
|
|
if (lagging)
|
|
_ghosts[i].GlobalTransform = c.GlobalTransform;
|
|
}
|
|
}
|
|
|
|
// The body is held back (hips against a table, a wall the head clears): stop the stick from
|
|
// carrying the view further that way.
|
|
public static Vector3 HoldBack(Vector3 velocity, Vector3 heldBack)
|
|
{
|
|
float into = velocity.Dot(heldBack);
|
|
return into > 0f ? velocity - heldBack * into : velocity;
|
|
}
|
|
|
|
private static bool IsJump(Transform3D a, Transform3D b) =>
|
|
a.Origin.DistanceTo(b.Origin) > JumpDistance ||
|
|
BodyMath.RotationError(b.Basis.GetRotationQuaternion(), a.Basis.GetRotationQuaternion()).Length() > JumpAngle;
|
|
|
|
private void Carry(Transform3D delta)
|
|
{
|
|
_ragdoll.TeleportBy(delta);
|
|
Solver.Shift(delta);
|
|
}
|
|
|
|
private void Locomote(float dt)
|
|
{
|
|
// A stick whose hand points at the menu scrolls it instead.
|
|
Vector2 move = WorldPanel.IsPointing(_left) ? Vector2.Zero : _left.GetVector2("primary");
|
|
Vector2 turn = WorldPanel.IsPointing(_right) ? Vector2.Zero : _right.GetVector2("primary");
|
|
|
|
// Move: head-relative on the ground plane, acceleration-limited. The body follows the
|
|
// camera physically, so a wall stops it and the push-back stops the view.
|
|
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 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);
|
|
}
|
|
if (!_ragdoll.Grounded)
|
|
desired = _moveVelocity; // no air control
|
|
_moveVelocity = _moveVelocity.MoveToward(desired, MoveAcceleration * dt);
|
|
_moveVelocity = HoldBack(_moveVelocity, _ragdoll.HeldBack);
|
|
if (_moveVelocity != Vector3.Zero)
|
|
_origin.GlobalPosition += _moveVelocity * dt;
|
|
|
|
// Turn about the head.
|
|
float angle = 0f;
|
|
if (SmoothTurn)
|
|
{
|
|
if (Mathf.Abs(turn.X) > StickDeadzone)
|
|
angle = -turn.X * SmoothTurnSpeed * dt;
|
|
}
|
|
else if (_snapArmed && Mathf.Abs(turn.X) > SnapTurnThreshold)
|
|
{
|
|
angle = -Mathf.Sign(turn.X) * Mathf.DegToRad(SnapTurnDegrees);
|
|
_snapArmed = false;
|
|
}
|
|
else if (Mathf.Abs(turn.X) < SnapTurnRearm)
|
|
_snapArmed = true;
|
|
if (angle != 0f)
|
|
{
|
|
Vector3 pivot = _camera.GlobalPosition;
|
|
Transform3D rot = new Transform3D(Basis.Identity, pivot) * new Transform3D(new Basis(Vector3.Up, angle), Vector3.Zero)
|
|
* new Transform3D(Basis.Identity, -pivot);
|
|
_origin.GlobalTransform = rot * _origin.GlobalTransform;
|
|
if (!SmoothTurn)
|
|
Carry(rot);
|
|
}
|
|
}
|
|
}
|