226 lines
7.5 KiB
C#
226 lines
7.5 KiB
C#
using Godot;
|
|
|
|
public partial class PropGrabber : Node
|
|
{
|
|
private const float GripThreshold = 0.6f;
|
|
private const float PalmGrabOffset = 0.045f;
|
|
private const float GrabRadius = 0.16f;
|
|
private const float CenterGrabRadius = 0.12f;
|
|
private const float RigidWeldBias = 0f;
|
|
private const float BreakDistance = 0.25f;
|
|
private const float BreakTime = 0.25f;
|
|
private const float BreakDistanceBoth = 0.4f;
|
|
private const float GripRipDistance = 0.8f;
|
|
|
|
private static PropGrabber _instance;
|
|
|
|
private XRController3D _left;
|
|
private XRController3D _right;
|
|
|
|
private readonly RigidBody3D[] _held = new RigidBody3D[2];
|
|
private readonly ConeTwistJoint3D[] _joints = new ConeTwistJoint3D[2];
|
|
private readonly Vector3[] _localAnchor = new Vector3[2];
|
|
private readonly uint[] _grabSeq = new uint[2];
|
|
private readonly float[] _breakTimer = new float[2];
|
|
private readonly bool[] _gripPrev = new bool[2];
|
|
private uint _grabCount;
|
|
|
|
public override void _Ready()
|
|
{
|
|
_instance = this;
|
|
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
|
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
|
}
|
|
|
|
public static bool IsGrabbing(XRController3D controller) =>
|
|
_instance != null &&
|
|
((_instance._left == controller && _instance._held[0] != null) ||
|
|
(_instance._right == controller && _instance._held[1] != null));
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
UpdateHand(_left, 0);
|
|
UpdateHand(_right, 1);
|
|
}
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
{
|
|
float dt = (float)delta;
|
|
HoldUpdate(_left, 0, dt);
|
|
HoldUpdate(_right, 1, dt);
|
|
}
|
|
|
|
private void UpdateHand(XRController3D controller, int hand)
|
|
{
|
|
bool grip = controller.GetFloat("grip") > GripThreshold;
|
|
int other = 1 - hand;
|
|
|
|
if (grip && !_gripPrev[hand] && _held[hand] == null)
|
|
{
|
|
RigidBody3D prop = FindProp(controller, hand);
|
|
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
|
if (prop != null && handBody != null)
|
|
{
|
|
_held[hand] = prop;
|
|
_grabSeq[hand] = _grabCount++;
|
|
Vector3 pivot = PalmPoint(handBody, hand);
|
|
_joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias);
|
|
_localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot;
|
|
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({prop.Mass:0.###} kg, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
|
|
}
|
|
}
|
|
|
|
if (!grip && _held[hand] != null)
|
|
{
|
|
RigidBody3D prop = _held[hand];
|
|
_held[hand] = null;
|
|
DestroyJoint(hand);
|
|
if (IsInstanceValid(prop))
|
|
GD.Print($"PropGrabber: released '{prop.Name}' at {prop.LinearVelocity.Length():F2} m/s");
|
|
}
|
|
_gripPrev[hand] = grip;
|
|
}
|
|
|
|
private void HoldUpdate(XRController3D controller, int hand, float delta)
|
|
{
|
|
RigidBody3D body = _held[hand];
|
|
if (body == null)
|
|
return;
|
|
if (!IsInstanceValid(body))
|
|
{
|
|
_held[hand] = null;
|
|
DestroyJoint(hand);
|
|
return;
|
|
}
|
|
|
|
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
|
ConeTwistJoint3D joint = _joints[hand];
|
|
if (handBody == null || !IsInstanceValid(handBody) || joint == null || !IsInstanceValid(joint))
|
|
{
|
|
_held[hand] = null;
|
|
DestroyJoint(hand);
|
|
return;
|
|
}
|
|
|
|
if (handBody.GlobalPosition.DistanceTo(controller.GlobalPosition) > PhysicsHands.SnapDistance)
|
|
{
|
|
BreakGrip(hand, body, "grip dragged off");
|
|
return;
|
|
}
|
|
|
|
float separation = (body.GlobalTransform * _localAnchor[hand]).DistanceTo(PalmPoint(handBody, hand));
|
|
|
|
int other = 1 - hand;
|
|
bool twoHanded = _held[other] == body && IsInstanceValid(_held[other]) &&
|
|
_joints[other] != null && IsInstanceValid(_joints[other]);
|
|
if (twoHanded)
|
|
{
|
|
RigidBody3D otherHandBody = PhysicsHands.GetBody(other == 0 ? _left : _right);
|
|
float otherSeparation = otherHandBody == null
|
|
? 0f
|
|
: (body.GlobalTransform * _localAnchor[other]).DistanceTo(PalmPoint(otherHandBody, other));
|
|
if (separation > BreakDistanceBoth && otherSeparation > BreakDistanceBoth)
|
|
{
|
|
BreakGrip(hand, body, $"both hands stretched ({separation:0.###} m)");
|
|
BreakGrip(other, body, "both hands stretched");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (separation > GripRipDistance)
|
|
{
|
|
BreakGrip(hand, body, $"ripped ({separation:0.###} m)");
|
|
return;
|
|
}
|
|
|
|
if (separation > BreakDistance)
|
|
_breakTimer[hand] += delta;
|
|
else
|
|
_breakTimer[hand] = 0f;
|
|
if (_breakTimer[hand] > BreakTime)
|
|
{
|
|
BreakGrip(hand, body, $"stretched ({separation:0.###} m for {BreakTime:0.#} s)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
private int PrimaryHand(RigidBody3D body)
|
|
{
|
|
int primary = -1;
|
|
for (int hand = 0; hand < 2; hand++)
|
|
{
|
|
if (_held[hand] == body && (primary == -1 || _grabSeq[hand] < _grabSeq[primary]))
|
|
primary = hand;
|
|
}
|
|
return primary;
|
|
}
|
|
|
|
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, RigidBody3D prop, Vector3 pivot, float bias)
|
|
{
|
|
ConeTwistJoint3D weld = new()
|
|
{
|
|
ExcludeNodesFromCollision = true,
|
|
Position = pivot,
|
|
NodeA = handBody.GetPath(),
|
|
NodeB = prop.GetPath(),
|
|
SwingSpan = 0f,
|
|
TwistSpan = 0f,
|
|
Bias = bias,
|
|
};
|
|
AddChild(weld);
|
|
return weld;
|
|
}
|
|
|
|
private void BreakGrip(int hand, RigidBody3D body, string reason)
|
|
{
|
|
if (_held[hand] != body)
|
|
return;
|
|
_held[hand] = null;
|
|
DestroyJoint(hand);
|
|
GD.Print($"PropGrabber: grip broke on '{body.Name}' ({reason})");
|
|
}
|
|
|
|
private void DestroyJoint(int hand)
|
|
{
|
|
if (_joints[hand] != null && IsInstanceValid(_joints[hand]))
|
|
_joints[hand].QueueFree();
|
|
_joints[hand] = null;
|
|
_breakTimer[hand] = 0f;
|
|
}
|
|
|
|
private static Vector3 PalmPoint(RigidBody3D handBody, int hand) =>
|
|
handBody.GlobalPosition + handBody.GlobalTransform.Basis * new Vector3(PalmOffset(hand), 0f, 0f);
|
|
|
|
private static float PalmOffset(int hand) => hand == 0 ? PalmGrabOffset : -PalmGrabOffset;
|
|
|
|
private RigidBody3D FindProp(XRController3D controller, int hand)
|
|
{
|
|
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
|
if (handBody == null)
|
|
return null;
|
|
Vector3 palm = PalmPoint(handBody, hand);
|
|
RigidBody3D nearest = NearestProp(palm, GrabRadius);
|
|
if (nearest != null)
|
|
return nearest;
|
|
return NearestProp(handBody.GlobalPosition, CenterGrabRadius);
|
|
}
|
|
|
|
private RigidBody3D NearestProp(Vector3 point, float radius)
|
|
{
|
|
RigidBody3D nearest = null;
|
|
float nearestDist = radius;
|
|
foreach (Node node in GetTree().GetNodesInGroup("prop"))
|
|
{
|
|
if (node is not RigidBody3D prop)
|
|
continue;
|
|
float dist = prop.GlobalPosition.DistanceTo(point);
|
|
if (dist < nearestDist)
|
|
{
|
|
nearestDist = dist;
|
|
nearest = prop;
|
|
}
|
|
}
|
|
return nearest;
|
|
}
|
|
}
|