397 lines
13 KiB
C#
397 lines
13 KiB
C#
using Godot;
|
|
using System.Collections.Generic;
|
|
|
|
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 TipHoverRadius = 0.07f;
|
|
private const float PressContactDistance = 0.045f;
|
|
private const ulong PressCooldownMs = 400;
|
|
private const float TipLength = 0.12f;
|
|
private const float RayLength = 50f;
|
|
|
|
private static WorldPanel _instance;
|
|
|
|
private XRController3D _left;
|
|
private XRController3D _right;
|
|
private XRCamera3D _camera;
|
|
private Locomotion _locomotion;
|
|
private PropShop _propShop;
|
|
private AudioStreamPlayer3D _click;
|
|
|
|
private StaticBody3D _panelBody;
|
|
|
|
private struct Knob
|
|
{
|
|
public string Name;
|
|
public Node3D Root;
|
|
public MeshInstance3D Mesh;
|
|
public StandardMaterial3D Material;
|
|
public Callable Action;
|
|
public ulong LastPressMs;
|
|
}
|
|
|
|
private readonly List<Knob> _knobs = new();
|
|
|
|
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[] _triggerPrev = new bool[2];
|
|
private readonly bool[] _summonPrev = new bool[2];
|
|
private readonly int[] _hover = new int[2] { -1, -1 };
|
|
private readonly bool[] _hoverPanel = new bool[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");
|
|
Visible = false;
|
|
BuildPanel();
|
|
BuildClickSound();
|
|
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.HasMeta("knob_index") || node == _instance._panelBody || node.GetParent() == _instance._panelBody);
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
PollSummon(_left, 0);
|
|
PollSummon(_right, 1);
|
|
if (!Visible)
|
|
return;
|
|
UpdateHand(_left, 0);
|
|
UpdateHand(_right, 1);
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!active)
|
|
ResetInteractionState();
|
|
Visible = active;
|
|
SetShapesEnabled(this, active);
|
|
}
|
|
|
|
private static void SetShapesEnabled(Node root, bool enabled)
|
|
{
|
|
foreach (Node child in root.GetChildren())
|
|
{
|
|
if (child is CollisionShape3D shape)
|
|
shape.SetDeferred(CollisionShape3D.PropertyName.Disabled, !enabled);
|
|
SetShapesEnabled(child, enabled);
|
|
}
|
|
}
|
|
|
|
private void PlaceInFrontOf(XRController3D controller) =>
|
|
OrientTo(controller.GlobalPosition - controller.GlobalTransform.Basis.Z * 0.5f);
|
|
|
|
private void PlaceInFrontOfHead() =>
|
|
OrientTo(_camera.GlobalPosition - _camera.GlobalTransform.Basis.Z * 0.5f);
|
|
|
|
private void OrientTo(Vector3 pos)
|
|
{
|
|
Vector3 look = _camera.GlobalPosition - pos;
|
|
if (look.LengthSquared() < 0.0001f)
|
|
return;
|
|
Vector3 zAxis = look.Normalized();
|
|
Vector3 xAxis = Vector3.Up.Cross(zAxis);
|
|
if (xAxis.LengthSquared() < 0.0001f)
|
|
return;
|
|
xAxis = xAxis.Normalized();
|
|
Vector3 yAxis = zAxis.Cross(xAxis).Normalized();
|
|
GlobalTransform = new Transform3D(new Basis(xAxis, yAxis, zAxis), pos);
|
|
}
|
|
|
|
private void OrientTowardCamera()
|
|
{
|
|
Vector3 look = _camera.GlobalPosition - GlobalPosition;
|
|
if (look.LengthSquared() < 0.0001f)
|
|
return;
|
|
Vector3 zAxis = look.Normalized();
|
|
Vector3 xAxis = Vector3.Up.Cross(zAxis);
|
|
if (xAxis.LengthSquared() < 0.0001f)
|
|
return;
|
|
xAxis = xAxis.Normalized();
|
|
Vector3 yAxis = zAxis.Cross(xAxis).Normalized();
|
|
GlobalBasis = GlobalBasis.Slerp(new Basis(xAxis, yAxis, zAxis), 0.35f).Orthonormalized();
|
|
}
|
|
|
|
private void BuildPanel()
|
|
{
|
|
Node3D plate = new() { Name = "Plate" };
|
|
AddChild(plate);
|
|
|
|
StandardMaterial3D plateMaterial = new()
|
|
{
|
|
AlbedoColor = new Color(0.08f, 0.09f, 0.12f),
|
|
EmissionEnabled = true,
|
|
Emission = new Color(0.1f, 0.2f, 0.35f),
|
|
EmissionEnergyMultiplier = 0.4f,
|
|
};
|
|
MeshInstance3D plateMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.6f, 0.34f, 0.02f) } };
|
|
plateMesh.SetSurfaceOverrideMaterial(0, plateMaterial);
|
|
plate.AddChild(plateMesh);
|
|
|
|
_panelBody = new StaticBody3D
|
|
{
|
|
Name = "PanelBody",
|
|
CollisionLayer = 2,
|
|
CollisionMask = 0,
|
|
};
|
|
_panelBody.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.6f, 0.34f, 0.06f) } });
|
|
plate.AddChild(_panelBody);
|
|
|
|
Vector3[] positions =
|
|
{
|
|
new(-0.21f, 0.06f, 0.03f),
|
|
new(-0.07f, 0.06f, 0.03f),
|
|
new(0.07f, 0.06f, 0.03f),
|
|
new(0.21f, 0.06f, 0.03f),
|
|
};
|
|
string[] names = { "Spawn", "Ping", "Reset", "Close" };
|
|
Color[] colors =
|
|
{
|
|
new(0.25f, 0.6f, 1f),
|
|
new(0.95f, 0.75f, 0.25f),
|
|
new(0.2f, 0.8f, 0.35f),
|
|
new(0.9f, 0.3f, 0.3f),
|
|
};
|
|
Callable[] actions =
|
|
{
|
|
Callable.From(() =>
|
|
{
|
|
_locomotion.ReturnToSpawn();
|
|
ResetInteractionState();
|
|
PlaceInFrontOfHead();
|
|
}),
|
|
Callable.From(Ping),
|
|
Callable.From(() => _propShop.Respawn()),
|
|
Callable.From(Close),
|
|
};
|
|
|
|
for (int i = 0; i < names.Length; i++)
|
|
{
|
|
StandardMaterial3D material = new()
|
|
{
|
|
AlbedoColor = colors[i],
|
|
EmissionEnabled = true,
|
|
Emission = colors[i],
|
|
EmissionEnergyMultiplier = 1.2f,
|
|
};
|
|
MeshInstance3D mesh = new()
|
|
{
|
|
Mesh = new CylinderMesh { TopRadius = 0.032f, BottomRadius = 0.032f, Height = 0.025f },
|
|
RotationDegrees = new Vector3(90, 0, 0),
|
|
};
|
|
mesh.SetSurfaceOverrideMaterial(0, material);
|
|
|
|
StaticBody3D body = new()
|
|
{
|
|
CollisionLayer = 2,
|
|
CollisionMask = 0,
|
|
};
|
|
body.SetMeta("knob_index", i);
|
|
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f, 0.09f, 0.06f) } });
|
|
|
|
Node3D root = new() { Position = positions[i] };
|
|
root.AddChild(mesh);
|
|
root.AddChild(body);
|
|
|
|
Label3D label = new()
|
|
{
|
|
Text = names[i],
|
|
FontSize = 96,
|
|
PixelSize = 0.00025f,
|
|
OutlineSize = 6,
|
|
Position = new Vector3(0, -0.08f, 0.015f),
|
|
};
|
|
root.AddChild(label);
|
|
|
|
plate.AddChild(root);
|
|
_knobs.Add(new Knob
|
|
{
|
|
Name = names[i],
|
|
Root = root,
|
|
Mesh = mesh,
|
|
Material = material,
|
|
Action = actions[i],
|
|
});
|
|
}
|
|
}
|
|
|
|
private void BuildClickSound()
|
|
{
|
|
const int rate = 44100;
|
|
const int samples = rate / 25;
|
|
byte[] data = new byte[samples * 2];
|
|
for (int i = 0; i < samples; i++)
|
|
{
|
|
float envelope = 1f - (float)i / samples;
|
|
float value = Mathf.Sin(Mathf.Tau * 900f * i / rate) * envelope * 0.35f;
|
|
short sample = (short)(value * short.MaxValue);
|
|
data[i * 2] = (byte)sample;
|
|
data[i * 2 + 1] = (byte)(sample >> 8);
|
|
}
|
|
_click = new AudioStreamPlayer3D
|
|
{
|
|
Stream = new AudioStreamWav
|
|
{
|
|
Format = AudioStreamWav.FormatEnum.Format16Bits,
|
|
MixRate = rate,
|
|
Stereo = false,
|
|
Data = data,
|
|
},
|
|
};
|
|
AddChild(_click);
|
|
}
|
|
|
|
private void UpdateHand(XRController3D controller, int hand)
|
|
{
|
|
Vector3 from = controller.GlobalPosition;
|
|
Vector3 dir = -controller.GlobalTransform.Basis.Z;
|
|
Vector3 tip = from + dir * TipLength;
|
|
|
|
UpdateHover(controller, hand, from, dir, tip);
|
|
|
|
bool grip = controller.GetFloat("grip") > GripThreshold
|
|
&& !PropGrabber.IsGrabbing(controller);
|
|
if (grip && !_dragging[hand] && (_hoverPanel[hand] || GlobalPosition.DistanceTo(from) < DirectGrabRadius))
|
|
{
|
|
_dragging[hand] = true;
|
|
_directDrag[hand] = !_hoverPanel[hand];
|
|
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();
|
|
}
|
|
|
|
bool trigger = controller.GetFloat("trigger") > TriggerThreshold;
|
|
if (trigger && !_triggerPrev[hand] && _hover[hand] >= 0)
|
|
Press(_hover[hand]);
|
|
_triggerPrev[hand] = trigger;
|
|
|
|
if (_hover[hand] >= 0)
|
|
{
|
|
int hovered = _hover[hand];
|
|
Knob knob = _knobs[hovered];
|
|
if (knob.Root.GlobalPosition.DistanceTo(tip) < PressContactDistance)
|
|
{
|
|
ulong now = Time.GetTicksMsec();
|
|
if (now - knob.LastPressMs > PressCooldownMs)
|
|
{
|
|
knob.LastPressMs = now;
|
|
_knobs[hovered] = knob;
|
|
Press(hovered);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateHover(XRController3D controller, int hand, Vector3 from, Vector3 dir, Vector3 tip)
|
|
{
|
|
int knob = -1;
|
|
bool panel = false;
|
|
|
|
var query = PhysicsRayQueryParameters3D.Create(from, from + dir * RayLength);
|
|
var hit = GetViewport().GetWorld3D().DirectSpaceState.IntersectRay(query);
|
|
if (hit.TryGetValue("collider", out Variant collider) && collider.As<Node>() is Node node)
|
|
{
|
|
if (node.HasMeta("knob_index"))
|
|
knob = (int)node.GetMeta("knob_index");
|
|
else
|
|
panel = IsPanelCollider(node) || node.GetParent() == _panelBody;
|
|
}
|
|
|
|
if (knob < 0)
|
|
{
|
|
for (int i = 0; i < _knobs.Count; i++)
|
|
{
|
|
if (_knobs[i].Root.GlobalPosition.DistanceTo(tip) < TipHoverRadius)
|
|
{
|
|
knob = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (knob != _hover[hand])
|
|
{
|
|
if (_hover[hand] >= 0)
|
|
_knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f;
|
|
_hover[hand] = knob;
|
|
if (knob >= 0)
|
|
_knobs[knob].Material.EmissionEnergyMultiplier = 2.8f;
|
|
}
|
|
_hoverPanel[hand] = panel;
|
|
}
|
|
|
|
private void Press(int index)
|
|
{
|
|
Knob knob = _knobs[index];
|
|
GD.Print($"WorldPanel: pressed '{knob.Name}'");
|
|
_click.Play();
|
|
Tween tween = CreateTween();
|
|
tween.TweenProperty(knob.Mesh, "position:z", -0.014f, 0.05);
|
|
tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 4.0f, 0.05);
|
|
tween.TweenProperty(knob.Mesh, "position:z", 0f, 0.12);
|
|
tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 1.2f, 0.3);
|
|
knob.Action.Call();
|
|
}
|
|
|
|
private void Ping() => GD.Print("WorldPanel: ping");
|
|
|
|
private void Close() => SetPanelActive(false);
|
|
|
|
private void ResetInteractionState()
|
|
{
|
|
for (int hand = 0; hand < 2; hand++)
|
|
{
|
|
_dragging[hand] = false;
|
|
_hoverPanel[hand] = false;
|
|
_triggerPrev[hand] = false;
|
|
if (_hover[hand] >= 0)
|
|
_knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f;
|
|
_hover[hand] = -1;
|
|
}
|
|
}
|
|
}
|