262 lines
9.6 KiB
C#
262 lines
9.6 KiB
C#
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),
|
|
// 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).
|
|
public partial class WorldPanel : Node3D
|
|
{
|
|
private const float GripThreshold = 0.6f;
|
|
private const float TriggerThreshold = 0.6f;
|
|
private const float DirectGrabRadius = 0.45f;
|
|
private const float SummonDistance = 0.3f;
|
|
private const float HeadRecallDistance = 0.45f;
|
|
private const float HeadRecallDrop = 0.12f;
|
|
|
|
private static WorldPanel _instance;
|
|
|
|
private XRController3D _left;
|
|
private XRController3D _right;
|
|
private XRCamera3D _camera;
|
|
private Locomotion _locomotion;
|
|
private PropShop _propShop;
|
|
private AudioStreamPlayer3D _click;
|
|
private AudioStreamPlayer3D _ping;
|
|
|
|
private MenuScreen _screen;
|
|
private MainMenu _menu;
|
|
|
|
private readonly bool[] _dragging = new bool[2];
|
|
private readonly bool[] _directDrag = new bool[2];
|
|
private readonly float[] _laserDist = new float[2];
|
|
private readonly Vector3[] _directOffset = new Vector3[2];
|
|
private readonly bool[] _summonPrev = new bool[2];
|
|
private readonly MenuScreen.PointerInput[] _inputs = new MenuScreen.PointerInput[2];
|
|
|
|
public override void _Ready()
|
|
{
|
|
_instance = this;
|
|
_left = GetNode<XRController3D>("../LeftHand");
|
|
_right = GetNode<XRController3D>("../RightHand");
|
|
_camera = GetNode<XRCamera3D>("../XRCamera3D");
|
|
_locomotion = GetNode<Locomotion>("../../Locomotion");
|
|
_propShop = GetNode<PropShop>("../../PropShop");
|
|
|
|
_screen = new MenuScreen { Name = "Screen" };
|
|
AddChild(_screen);
|
|
_menu = new MainMenu { Name = "MainMenu" };
|
|
_screen.SetContent(_menu);
|
|
_screen.PointerPressed += OnPointerPressed;
|
|
_screen.Haptic += (hand, amplitude, seconds) =>
|
|
(hand == 0 ? _left : _right).TriggerHapticPulse("haptic", 0.0, amplitude, seconds, 0.0);
|
|
|
|
_menu.ReturnToSpawnRequested += () =>
|
|
{
|
|
_locomotion.ReturnToSpawn();
|
|
ResetInteractionState();
|
|
PlaceInFrontOfHead();
|
|
_menu.Log("Returned to spawn");
|
|
};
|
|
_menu.ResetPropsRequested += () =>
|
|
{
|
|
_propShop.Respawn();
|
|
_menu.Log("Props reset");
|
|
};
|
|
_menu.PingRequested += () =>
|
|
{
|
|
GD.Print("WorldPanel: ping");
|
|
_ping.Play();
|
|
_menu.Log("Ping");
|
|
};
|
|
_menu.CloseRequested += () => SetPanelActive(false);
|
|
_menu.Clicked += () => _click.Play();
|
|
|
|
_click = MakeTone(900f, 25, 0.35f, 1f);
|
|
_ping = MakeTone(1320f, 4, 0.3f, 3f);
|
|
_menu.ClickVolumeSlider.ValueChanged += v => _click.VolumeDb = Mathf.LinearToDb((float)v / 70f);
|
|
|
|
SetPanelActive(false);
|
|
}
|
|
|
|
public static bool IsBlocking(XRController3D controller) =>
|
|
_instance != null && _instance.Visible &&
|
|
_instance.GlobalPosition.DistanceTo(controller.GlobalPosition) < DirectGrabRadius;
|
|
|
|
public static bool IsPanelCollider(Node node) =>
|
|
_instance != null && _instance.Visible && node != null &&
|
|
(node == _instance._screen.Body || node.GetParent() == _instance._screen.Body);
|
|
|
|
// Laser length to the menu for this controller, if the controller is currently using its
|
|
// laser (not touch) on the menu. Locomotion.UpdatePointer draws the beam from this.
|
|
public static bool TryGetLaserDistance(XRController3D controller, out float distance)
|
|
{
|
|
distance = 0f;
|
|
if (_instance == null || !_instance.Visible)
|
|
return false;
|
|
int hand = controller == _instance._left ? 0 : controller == _instance._right ? 1 : -1;
|
|
return hand >= 0 && _instance._screen.TryGetLaserDistance(hand, out distance);
|
|
}
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
PollSummon(_left, 0);
|
|
PollSummon(_right, 1);
|
|
if (Visible)
|
|
{
|
|
UpdateDrag(_left, 0);
|
|
UpdateDrag(_right, 1);
|
|
}
|
|
_inputs[0] = ReadPointer(_left, 0);
|
|
_inputs[1] = ReadPointer(_right, 1);
|
|
_screen.UpdatePointers(_inputs, delta);
|
|
}
|
|
|
|
private MenuScreen.PointerInput ReadPointer(XRController3D controller, int hand) => new()
|
|
{
|
|
// A hand dragging the panel doesn't also point at it.
|
|
Valid = !_dragging[hand] && controller.GetIsActive(),
|
|
Origin = controller.GlobalPosition,
|
|
Direction = -controller.GlobalTransform.Basis.Z,
|
|
Trigger = controller.GetFloat("trigger") > TriggerThreshold,
|
|
Stick = controller.GetVector2("primary"),
|
|
};
|
|
|
|
private void OnPointerPressed(int hand, MenuScreen.PointerMode mode)
|
|
{
|
|
XRController3D controller = hand == 0 ? _left : _right;
|
|
// Short tick on contact: the "physical press" feel for the touch cursor.
|
|
controller.TriggerHapticPulse("haptic", 0.0, mode == MenuScreen.PointerMode.Touch ? 0.35 : 0.2, 0.02, 0.0);
|
|
}
|
|
|
|
private void PollSummon(XRController3D controller, int hand)
|
|
{
|
|
bool summon = controller.IsButtonPressed("ax_button");
|
|
if (summon && !_summonPrev[hand])
|
|
{
|
|
if (!Visible)
|
|
PlaceInFrontOf(controller);
|
|
SetPanelActive(!Visible);
|
|
GD.Print($"WorldPanel: {(Visible ? "summoned" : "dismissed")} via ax_button ({(hand == 0 ? "left" : "right")})");
|
|
}
|
|
_summonPrev[hand] = summon;
|
|
}
|
|
|
|
private void SetPanelActive(bool active)
|
|
{
|
|
ResetInteractionState();
|
|
Visible = active;
|
|
_screen.SetActive(active);
|
|
if (active)
|
|
_menu.RefreshCalibration();
|
|
}
|
|
|
|
private void PlaceInFrontOf(XRController3D controller) =>
|
|
OrientTo(controller.GlobalPosition - controller.GlobalTransform.Basis.Z * SummonDistance);
|
|
|
|
private void PlaceInFrontOfHead()
|
|
{
|
|
Vector3 forward = -_camera.GlobalTransform.Basis.Z;
|
|
forward.Y = 0f;
|
|
forward = forward.LengthSquared() < 0.0001f ? Vector3.Forward : forward.Normalized();
|
|
OrientTo(_camera.GlobalPosition + forward * HeadRecallDistance + Vector3.Down * HeadRecallDrop);
|
|
}
|
|
|
|
private static Basis FacingBasis(Vector3 from, Vector3 head, out bool ok)
|
|
{
|
|
ok = false;
|
|
Vector3 look = head - from;
|
|
if (look.LengthSquared() < 0.0001f)
|
|
return Basis.Identity;
|
|
Vector3 zAxis = look.Normalized();
|
|
Vector3 xAxis = Vector3.Up.Cross(zAxis);
|
|
if (xAxis.LengthSquared() < 0.0001f)
|
|
return Basis.Identity;
|
|
xAxis = xAxis.Normalized();
|
|
Vector3 yAxis = zAxis.Cross(xAxis).Normalized();
|
|
ok = true;
|
|
return new Basis(xAxis, yAxis, zAxis);
|
|
}
|
|
|
|
private void OrientTo(Vector3 pos)
|
|
{
|
|
Basis basis = FacingBasis(pos, _camera.GlobalPosition, out bool ok);
|
|
if (ok)
|
|
GlobalTransform = new Transform3D(basis, pos);
|
|
}
|
|
|
|
private void OrientTowardCamera()
|
|
{
|
|
Basis basis = FacingBasis(GlobalPosition, _camera.GlobalPosition, out bool ok);
|
|
if (ok)
|
|
GlobalBasis = GlobalBasis.Slerp(basis, 0.35f).Orthonormalized();
|
|
}
|
|
|
|
private AudioStreamPlayer3D MakeTone(float frequency, int durationDivisor, float amplitude, float decayPower)
|
|
{
|
|
const int rate = 44100;
|
|
int samples = rate / durationDivisor;
|
|
byte[] data = new byte[samples * 2];
|
|
for (int i = 0; i < samples; i++)
|
|
{
|
|
float envelope = Mathf.Pow(1f - (float)i / samples, decayPower);
|
|
float value = Mathf.Sin(Mathf.Tau * frequency * i / rate) * envelope * amplitude;
|
|
short sample = (short)(value * short.MaxValue);
|
|
data[i * 2] = (byte)sample;
|
|
data[i * 2 + 1] = (byte)(sample >> 8);
|
|
}
|
|
AudioStreamPlayer3D player = new()
|
|
{
|
|
Stream = new AudioStreamWav
|
|
{
|
|
Format = AudioStreamWav.FormatEnum.Format16Bits,
|
|
MixRate = rate,
|
|
Stereo = false,
|
|
Data = data,
|
|
},
|
|
};
|
|
AddChild(player);
|
|
return player;
|
|
}
|
|
|
|
private void UpdateDrag(XRController3D controller, int hand)
|
|
{
|
|
Vector3 from = controller.GlobalPosition;
|
|
Vector3 dir = -controller.GlobalTransform.Basis.Z;
|
|
bool laserOnPanel = _screen.GetMode(hand) == MenuScreen.PointerMode.Laser;
|
|
|
|
bool grip = controller.GetFloat("grip") > GripThreshold
|
|
&& !PropGrabber.IsGrabbing(controller);
|
|
if (grip && !_dragging[hand] && (laserOnPanel || GlobalPosition.DistanceTo(from) < DirectGrabRadius))
|
|
{
|
|
_dragging[hand] = true;
|
|
_directDrag[hand] = !laserOnPanel;
|
|
if (_directDrag[hand])
|
|
_directOffset[hand] = GlobalPosition - from;
|
|
else
|
|
_laserDist[hand] = Mathf.Max(0.2f, (GlobalPosition - from).Dot(dir));
|
|
GD.Print($"WorldPanel: grab started ({(_directDrag[hand] ? "direct" : "laser")}, {(hand == 0 ? "left" : "right")})");
|
|
}
|
|
if (!grip && _dragging[hand])
|
|
{
|
|
_dragging[hand] = false;
|
|
GD.Print("WorldPanel: grab released");
|
|
}
|
|
|
|
if (_dragging[hand])
|
|
{
|
|
GlobalPosition = _directDrag[hand]
|
|
? from + _directOffset[hand]
|
|
: from + dir * _laserDist[hand];
|
|
OrientTowardCamera();
|
|
}
|
|
}
|
|
|
|
private void ResetInteractionState()
|
|
{
|
|
for (int hand = 0; hand < 2; hand++)
|
|
_dragging[hand] = false;
|
|
_screen?.ReleaseAll();
|
|
}
|
|
}
|