add unity doc.

This commit is contained in:
2026-05-30 16:04:37 +08:00
parent ce52895ec2
commit f4ee53d9c3
1538 changed files with 221228 additions and 2192 deletions

View File

@ -0,0 +1,374 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using UnityEngine.XR;
public class PicoControllerUdpSender : MonoBehaviour
{
private const string HostPrefsKey = "xr_rm_udp_sender.host";
private const string PortPrefsKey = "xr_rm_udp_sender.port";
private const string SendHzPrefsKey = "xr_rm_udp_sender.send_hz";
private const string ConvertPrefsKey = "xr_rm_udp_sender.convert_coordinates";
[Header("ROS UDP Target")]
public string host = "192.168.9.99";
public int port = 15000;
[Header("Send")]
public float sendHz = 60.0f;
public bool convertUnityToProjectCoordinates = true;
public bool sendOnStart = true;
private UdpClient client;
private IPEndPoint endPoint;
private float nextSendTime;
private bool sendEnabled;
private readonly Packet packet = new Packet();
private readonly List<InputDevice> deviceBuffer = new List<InputDevice>();
public bool SendEnabled => sendEnabled;
public bool HasEndpoint => endPoint != null;
public bool LeftDeviceValid { get; private set; }
public bool RightDeviceValid { get; private set; }
public bool LeftGripPressed { get; private set; }
public bool RightGripPressed { get; private set; }
public string LastError { get; private set; } = string.Empty;
public double LastSendTime { get; private set; }
public uint SentPackets { get; private set; }
[Serializable]
private class Packet
{
public double t;
public string frame_id = "xr_world";
public Controllers controllers = new Controllers();
}
[Serializable]
private class Controllers
{
public ControllerPayload left = new ControllerPayload();
public ControllerPayload right = new ControllerPayload();
}
[Serializable]
private class ControllerPayload
{
public bool grip;
public float trigger;
public float[] pos = new float[] { 0.0f, 1.0f, 0.0f };
public float[] quat = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
}
private void OnEnable()
{
LoadConfig();
ReopenClient();
sendEnabled = sendOnStart;
nextSendTime = 0.0f;
}
private void OnDisable()
{
SendSafeStopPacket();
CloseClient();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SendSafeStopPacket();
return;
}
nextSendTime = 0.0f;
}
private void OnApplicationFocus(bool hasFocus)
{
if (!hasFocus)
{
SendSafeStopPacket();
return;
}
nextSendTime = 0.0f;
}
private void Update()
{
if (!sendEnabled)
{
return;
}
if (Time.unscaledTime < nextSendTime)
{
return;
}
nextSendTime = Time.unscaledTime + 1.0f / Mathf.Max(sendHz, 1.0f);
packet.t = Time.realtimeSinceStartupAsDouble;
FillController(XRNode.LeftHand, packet.controllers.left);
FillController(XRNode.RightHand, packet.controllers.right);
SendPacket();
}
public bool ApplyConfig(string newHost, int newPort, float newSendHz, bool newConvertUnityToProjectCoordinates, bool save)
{
string trimmedHost = newHost == null ? string.Empty : newHost.Trim();
if (!TryParseTargetAddress(trimmedHost, out IPAddress address))
{
LastError = $"Invalid IPv4 address: {newHost}";
return false;
}
if (newPort < 1 || newPort > 65535)
{
LastError = $"Invalid UDP port: {newPort}";
return false;
}
host = trimmedHost;
port = newPort;
sendHz = Mathf.Clamp(newSendHz, 1.0f, 120.0f);
convertUnityToProjectCoordinates = newConvertUnityToProjectCoordinates;
ReopenClient(address);
if (endPoint == null)
{
return false;
}
if (save)
{
SaveConfig();
}
LastError = string.Empty;
return true;
}
public void SetSending(bool enabled)
{
if (!enabled && sendEnabled)
{
SendSafeStopPacket();
}
sendEnabled = enabled;
nextSendTime = 0.0f;
}
private void LoadConfig()
{
string savedHost = PlayerPrefs.GetString(HostPrefsKey, host);
if (TryParseTargetAddress(savedHost, out _))
{
host = savedHost.Trim();
}
port = Mathf.Clamp(PlayerPrefs.GetInt(PortPrefsKey, port), 1, 65535);
sendHz = Mathf.Clamp(PlayerPrefs.GetFloat(SendHzPrefsKey, sendHz), 1.0f, 120.0f);
convertUnityToProjectCoordinates = PlayerPrefs.GetInt(ConvertPrefsKey, convertUnityToProjectCoordinates ? 1 : 0) != 0;
}
private void SaveConfig()
{
PlayerPrefs.SetString(HostPrefsKey, host);
PlayerPrefs.SetInt(PortPrefsKey, port);
PlayerPrefs.SetFloat(SendHzPrefsKey, sendHz);
PlayerPrefs.SetInt(ConvertPrefsKey, convertUnityToProjectCoordinates ? 1 : 0);
PlayerPrefs.Save();
}
private void ReopenClient()
{
if (TryParseTargetAddress(host, out IPAddress address))
{
ReopenClient(address);
return;
}
LastError = $"Invalid IPv4 address: {host}";
CloseClient();
endPoint = null;
}
private void ReopenClient(IPAddress address)
{
try
{
CloseClient();
client = new UdpClient();
endPoint = new IPEndPoint(address, port);
LastError = string.Empty;
}
catch (Exception exception)
{
LastError = exception.Message;
CloseClient();
endPoint = null;
}
}
private void FillController(XRNode node, ControllerPayload payload)
{
InputDevice device = GetControllerDevice(node);
bool isLeft = node == XRNode.LeftHand;
if (isLeft)
{
LeftDeviceValid = device.isValid;
}
else
{
RightDeviceValid = device.isValid;
}
if (!device.isValid)
{
payload.grip = false;
payload.trigger = 0.0f;
SetGripState(isLeft, false);
return;
}
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
float trigger = 0.0f;
float gripValue = 0.0f;
bool gripButton = false;
device.TryGetFeatureValue(CommonUsages.devicePosition, out position);
device.TryGetFeatureValue(CommonUsages.deviceRotation, out rotation);
device.TryGetFeatureValue(CommonUsages.trigger, out trigger);
device.TryGetFeatureValue(CommonUsages.grip, out gripValue);
device.TryGetFeatureValue(CommonUsages.gripButton, out gripButton);
payload.grip = gripButton || gripValue > 0.5f;
SetGripState(isLeft, payload.grip);
payload.trigger = Mathf.Clamp01(trigger);
if (convertUnityToProjectCoordinates)
{
payload.pos[0] = position.x;
payload.pos[1] = position.y;
payload.pos[2] = -position.z;
payload.quat[0] = -rotation.x;
payload.quat[1] = -rotation.y;
payload.quat[2] = rotation.z;
payload.quat[3] = rotation.w;
}
else
{
payload.pos[0] = position.x;
payload.pos[1] = position.y;
payload.pos[2] = position.z;
payload.quat[0] = rotation.x;
payload.quat[1] = rotation.y;
payload.quat[2] = rotation.z;
payload.quat[3] = rotation.w;
}
}
private InputDevice GetControllerDevice(XRNode node)
{
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
if (device.isValid)
{
return device;
}
InputDeviceCharacteristics handedness = node == XRNode.LeftHand
? InputDeviceCharacteristics.Left
: InputDeviceCharacteristics.Right;
device = FindControllerByCharacteristics(
InputDeviceCharacteristics.Controller | InputDeviceCharacteristics.HeldInHand | handedness);
if (device.isValid)
{
return device;
}
return FindControllerByCharacteristics(InputDeviceCharacteristics.Controller | handedness);
}
private InputDevice FindControllerByCharacteristics(InputDeviceCharacteristics characteristics)
{
deviceBuffer.Clear();
InputDevices.GetDevicesWithCharacteristics(characteristics, deviceBuffer);
for (int i = 0; i < deviceBuffer.Count; i++)
{
if (deviceBuffer[i].isValid)
{
return deviceBuffer[i];
}
}
return default;
}
private static bool TryParseTargetAddress(string value, out IPAddress address)
{
string trimmed = value == null ? string.Empty : value.Trim();
return IPAddress.TryParse(trimmed, out address) && address.AddressFamily == AddressFamily.InterNetwork;
}
private void SetGripState(bool isLeft, bool pressed)
{
if (isLeft)
{
LeftGripPressed = pressed;
}
else
{
RightGripPressed = pressed;
}
}
private void SendSafeStopPacket()
{
packet.t = Time.realtimeSinceStartupAsDouble;
packet.controllers.left.grip = false;
packet.controllers.left.trigger = 0.0f;
packet.controllers.right.grip = false;
packet.controllers.right.trigger = 0.0f;
LeftGripPressed = false;
RightGripPressed = false;
SendPacket();
}
private void SendPacket()
{
if (client == null || endPoint == null)
{
return;
}
string json = JsonUtility.ToJson(packet);
byte[] bytes = Encoding.UTF8.GetBytes(json);
try
{
client.Send(bytes, bytes.Length, endPoint);
LastSendTime = Time.realtimeSinceStartupAsDouble;
SentPackets++;
LastError = string.Empty;
}
catch (Exception exception)
{
LastError = exception.Message;
}
}
private void CloseClient()
{
client?.Close();
client = null;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4c7a11d85854eddb40a6a614bfb129c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,372 @@
using System;
using Unity.XR.PICO.TOBSupport;
using UnityEngine;
[DisallowMultipleComponent]
public class PicoKeepAwake : MonoBehaviour
{
[Header("Policy")]
public bool keepAwake = true;
public bool applyWhenSendingOnly = true;
[Header("Backends")]
public bool usePicoEnterpriseApi = true;
public bool useAndroidWindowFlag = true;
public float picoRefreshSeconds = 30.0f;
private const int AndroidFlagKeepScreenOn = 128;
private PicoControllerUdpSender sender;
private bool enterpriseInitialized;
private bool enterpriseBound;
private bool enterpriseBindingRequested;
private bool picoSettingsApplied;
#if UNITY_ANDROID && !UNITY_EDITOR
private bool picoSystemDelaysCaptured;
private SleepDelayTimeEnum previousSleepDelay;
private ScreenOffDelayTimeEnum previousScreenOffDelay;
#endif
private float nextPicoRefreshTime;
private int lastScreenOffDelayResult = int.MinValue;
private string lastPicoError = string.Empty;
public bool KeepAwakeActive { get; private set; }
public bool EnterpriseBound => enterpriseBound;
public string StatusText { get; private set; } = "KeepAwake idle";
private void Awake()
{
sender = GetComponent<PicoControllerUdpSender>();
ApplyUnityKeepAwakeDefaults();
}
private void OnEnable()
{
if (usePicoEnterpriseApi)
{
EnsureEnterpriseBinding();
}
RefreshDesiredState(true);
}
private void Update()
{
RefreshDesiredState(false);
}
private void OnDisable()
{
DisableKeepAwake();
}
private void OnDestroy()
{
DisableKeepAwake();
}
private void OnApplicationPause(bool pauseStatus)
{
if (!pauseStatus)
{
nextPicoRefreshTime = 0.0f;
RefreshDesiredState(true);
}
}
private void OnApplicationFocus(bool hasFocus)
{
if (hasFocus)
{
nextPicoRefreshTime = 0.0f;
RefreshDesiredState(true);
}
}
private void RefreshDesiredState(bool force)
{
bool shouldKeepAwake = keepAwake && (!applyWhenSendingOnly || sender == null || sender.SendEnabled);
if (!shouldKeepAwake)
{
if (KeepAwakeActive)
{
DisableKeepAwake();
}
else
{
RefreshStatus();
}
return;
}
if (!KeepAwakeActive)
{
EnableKeepAwake();
return;
}
if ((force || Time.unscaledTime >= nextPicoRefreshTime) && usePicoEnterpriseApi)
{
ApplyPicoEnterpriseKeepAwake();
}
RefreshStatus();
}
private void EnableKeepAwake()
{
ApplyUnityKeepAwakeDefaults();
SetAndroidKeepScreenOn(true);
KeepAwakeActive = true;
nextPicoRefreshTime = 0.0f;
if (usePicoEnterpriseApi)
{
EnsureEnterpriseBinding();
ApplyPicoEnterpriseKeepAwake();
}
RefreshStatus();
}
private void DisableKeepAwake()
{
if (!KeepAwakeActive)
{
RefreshStatus();
return;
}
if (usePicoEnterpriseApi)
{
ReleasePicoEnterpriseWakeLock();
}
SetAndroidKeepScreenOn(false);
Screen.sleepTimeout = SleepTimeout.SystemSetting;
KeepAwakeActive = false;
RefreshStatus();
}
private static void ApplyUnityKeepAwakeDefaults()
{
Application.runInBackground = true;
Screen.sleepTimeout = SleepTimeout.NeverSleep;
}
private void EnsureEnterpriseBinding()
{
if (!usePicoEnterpriseApi || enterpriseBindingRequested)
{
return;
}
enterpriseBindingRequested = true;
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
enterpriseInitialized = PXR_Enterprise.InitEnterpriseService(false);
PXR_Enterprise.BindEnterpriseService(OnEnterpriseBound);
lastPicoError = enterpriseInitialized ? string.Empty : "PICO enterprise init failed";
}
catch (Exception exception)
{
enterpriseInitialized = false;
lastPicoError = exception.Message;
}
#else
enterpriseInitialized = true;
enterpriseBound = true;
#endif
RefreshStatus();
}
private void OnEnterpriseBound(bool success)
{
enterpriseBound = success;
if (!success)
{
lastPicoError = "PICO enterprise bind failed";
RefreshStatus();
return;
}
lastPicoError = string.Empty;
nextPicoRefreshTime = 0.0f;
if (KeepAwakeActive)
{
ApplyPicoEnterpriseKeepAwake();
}
RefreshStatus();
}
private void ApplyPicoEnterpriseKeepAwake()
{
nextPicoRefreshTime = Time.unscaledTime + Mathf.Max(5.0f, picoRefreshSeconds);
#if UNITY_ANDROID && !UNITY_EDITOR
if (!enterpriseInitialized)
{
EnsureEnterpriseBinding();
return;
}
try
{
CapturePicoSystemDelays();
PXR_Enterprise.AcquireWakeLock();
PXR_Enterprise.PropertySetSleepDelay(SleepDelayTimeEnum.NEVER);
PXR_Enterprise.PropertySetScreenOffDelay(ScreenOffDelayTimeEnum.NEVER, OnScreenOffDelaySet);
PXR_Enterprise.SwitchSystemFunction(SystemFunctionSwitchEnum.SFS_AUTOSLEEP, SwitchEnum.S_OFF);
picoSettingsApplied = true;
lastPicoError = string.Empty;
}
catch (Exception exception)
{
lastPicoError = exception.Message;
}
#else
picoSettingsApplied = true;
#endif
RefreshStatus();
}
private void CapturePicoSystemDelays()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (picoSystemDelaysCaptured || !enterpriseBound)
{
return;
}
previousSleepDelay = PXR_Enterprise.GetSleepDelay();
previousScreenOffDelay = PXR_Enterprise.GetScreenOffDelay();
picoSystemDelaysCaptured = true;
#endif
}
private void OnScreenOffDelaySet(int result)
{
lastScreenOffDelayResult = result;
RefreshStatus();
}
private void ReleasePicoEnterpriseWakeLock()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!enterpriseInitialized)
{
return;
}
try
{
PXR_Enterprise.ReleaseWakeLock();
RestorePicoSystemDelays();
}
catch (Exception exception)
{
lastPicoError = exception.Message;
}
#endif
picoSettingsApplied = false;
lastScreenOffDelayResult = int.MinValue;
}
private void RestorePicoSystemDelays()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!picoSystemDelaysCaptured)
{
PXR_Enterprise.SwitchSystemFunction(SystemFunctionSwitchEnum.SFS_AUTOSLEEP, SwitchEnum.S_ON);
return;
}
PXR_Enterprise.PropertySetSleepDelay(previousSleepDelay);
PXR_Enterprise.PropertySetScreenOffDelay(previousScreenOffDelay, null);
PXR_Enterprise.SwitchSystemFunction(SystemFunctionSwitchEnum.SFS_AUTOSLEEP, SwitchEnum.S_ON);
#endif
}
private void RefreshStatus()
{
if (!keepAwake)
{
StatusText = "KeepAwake disabled";
return;
}
if (applyWhenSendingOnly && sender != null && !sender.SendEnabled)
{
StatusText = "KeepAwake waiting";
return;
}
string activeState = KeepAwakeActive ? "ON" : "OFF";
string picoState = "PICO --";
if (usePicoEnterpriseApi)
{
if (!string.IsNullOrEmpty(lastPicoError))
{
picoState = "PICO error";
}
else if (enterpriseBindingRequested && !enterpriseInitialized)
{
picoState = "PICO init";
}
else if (enterpriseBound)
{
picoState = picoSettingsApplied ? "PICO applied" : "PICO bound";
}
else if (enterpriseBindingRequested)
{
picoState = "PICO binding";
}
}
string screenState = lastScreenOffDelayResult == int.MinValue
? string.Empty
: " screen " + lastScreenOffDelayResult.ToString();
StatusText = "KeepAwake " + activeState + " | " + picoState + screenState;
}
private void SetAndroidKeepScreenOn(bool enabled)
{
if (!useAndroidWindowFlag)
{
return;
}
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
AndroidJavaObject window = activity.Call<AndroidJavaObject>("getWindow");
if (enabled)
{
window.Call("addFlags", AndroidFlagKeepScreenOn);
}
else
{
window.Call("clearFlags", AndroidFlagKeepScreenOn);
}
}));
}
}
catch (Exception exception)
{
lastPicoError = exception.Message;
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09740726046b89c1b89aad001e0caa83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,638 @@
using System;
using System.Globalization;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;
public class PicoUdpConfigPanel : MonoBehaviour
{
private const int RowCount = 6;
private const float AxisRepeatDelay = 0.22f;
public int targetFrameRate = 90;
public bool showOnStart = true;
public bool togglePanelWithMenuButton = true;
private readonly Text[] rowTexts = new Text[RowCount];
private readonly Color normalColor = new Color(0.86f, 0.91f, 0.96f, 1.0f);
private readonly Color selectedColor = new Color(0.35f, 0.82f, 1.0f, 1.0f);
private readonly Color mutedColor = new Color(0.58f, 0.66f, 0.74f, 1.0f);
private PicoControllerUdpSender sender;
private Canvas canvas;
private GameObject configPanelObject;
private GameObject runHudObject;
private Text titleText;
private Text statusText;
private Text footerText;
private Text hudTitleText;
private Text hudStatusText;
private Text hudDetailText;
private Text hudHintText;
private TouchScreenKeyboard keyboard;
private KeyboardTarget keyboardTarget;
private string hostDraft;
private int portDraft;
private float sendHzDraft;
private bool convertDraft;
private PicoKeepAwake keepAwake;
private int selectedRow;
private float nextAxisTime;
private bool wasActivatePressed;
private bool wasApplyPressed;
private bool wasMenuPressed;
private string panelMessage = "Ready";
private enum KeyboardTarget
{
None,
Host,
Port,
SendHz
}
private void Awake()
{
sender = GetComponent<PicoControllerUdpSender>();
if (sender == null)
{
sender = gameObject.AddComponent<PicoControllerUdpSender>();
}
keepAwake = GetComponent<PicoKeepAwake>();
if (keepAwake == null)
{
keepAwake = gameObject.AddComponent<PicoKeepAwake>();
}
}
private void Start()
{
ApplyRuntimeDefaults();
EnsurePanel();
LoadDraftsFromSender();
canvas.gameObject.SetActive(true);
SetConfigPanelVisible(showOnStart, false);
RefreshPanel();
}
private void Update()
{
HandleKeyboard();
HandleControllerInput();
RefreshPanel();
}
private void ApplyRuntimeDefaults()
{
Application.targetFrameRate = targetFrameRate;
Application.runInBackground = true;
QualitySettings.vSyncCount = 0;
Screen.sleepTimeout = SleepTimeout.NeverSleep;
XRSettings.eyeTextureResolutionScale = 1.0f;
}
private void EnsurePanel()
{
Camera panelCamera = Camera.main;
if (panelCamera == null)
{
GameObject cameraObject = new GameObject("XR-RM Main Camera");
cameraObject.tag = "MainCamera";
panelCamera = cameraObject.AddComponent<Camera>();
panelCamera.nearClipPlane = 0.05f;
panelCamera.farClipPlane = 100.0f;
panelCamera.clearFlags = CameraClearFlags.SolidColor;
panelCamera.backgroundColor = new Color(0.02f, 0.025f, 0.03f, 1.0f);
if (FindObjectOfType<AudioListener>() == null)
{
cameraObject.AddComponent<AudioListener>();
}
AddTrackedPoseDriverIfAvailable(cameraObject);
}
GameObject canvasObject = new GameObject("XR-RM UDP Config Canvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler));
canvasObject.transform.SetParent(panelCamera.transform, false);
canvasObject.transform.localPosition = new Vector3(0.0f, 0.0f, 2.0f);
canvasObject.transform.localRotation = Quaternion.identity;
canvasObject.transform.localScale = Vector3.one * 0.0014f;
canvas = canvasObject.GetComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.worldCamera = panelCamera;
canvas.sortingOrder = 10;
RectTransform canvasRect = canvasObject.GetComponent<RectTransform>();
canvasRect.sizeDelta = new Vector2(980.0f, 640.0f);
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.dynamicPixelsPerUnit = 12.0f;
scaler.referencePixelsPerUnit = 100.0f;
configPanelObject = new GameObject("Config Panel", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
configPanelObject.transform.SetParent(canvasObject.transform, false);
RectTransform backgroundRect = configPanelObject.GetComponent<RectTransform>();
backgroundRect.anchorMin = Vector2.zero;
backgroundRect.anchorMax = Vector2.one;
backgroundRect.offsetMin = Vector2.zero;
backgroundRect.offsetMax = Vector2.zero;
Image background = configPanelObject.GetComponent<Image>();
background.color = new Color(0.045f, 0.06f, 0.075f, 0.94f);
titleText = CreateText("Title", configPanelObject.transform, 38, FontStyle.Bold, TextAnchor.MiddleLeft);
SetRect(titleText.rectTransform, new Vector2(0.06f, 0.82f), new Vector2(0.94f, 0.96f), Vector2.zero, Vector2.zero);
statusText = CreateText("Status", configPanelObject.transform, 22, FontStyle.Normal, TextAnchor.MiddleLeft);
statusText.color = mutedColor;
SetRect(statusText.rectTransform, new Vector2(0.06f, 0.72f), new Vector2(0.94f, 0.82f), Vector2.zero, Vector2.zero);
for (int i = 0; i < RowCount; i++)
{
Text rowText = CreateText("Row " + i, configPanelObject.transform, 27, FontStyle.Normal, TextAnchor.MiddleLeft);
float top = 0.68f - i * 0.09f;
SetRect(rowText.rectTransform, new Vector2(0.08f, top - 0.075f), new Vector2(0.92f, top), Vector2.zero, Vector2.zero);
rowTexts[i] = rowText;
}
footerText = CreateText("Footer", configPanelObject.transform, 23, FontStyle.Normal, TextAnchor.MiddleCenter);
footerText.color = normalColor;
SetRect(footerText.rectTransform, new Vector2(0.05f, 0.025f), new Vector2(0.95f, 0.15f), Vector2.zero, Vector2.zero);
CreateRunHud(canvasObject.transform);
}
private void CreateRunHud(Transform canvasParent)
{
runHudObject = new GameObject("Run HUD", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
runHudObject.transform.SetParent(canvasParent, false);
RectTransform hudRect = runHudObject.GetComponent<RectTransform>();
hudRect.anchorMin = Vector2.zero;
hudRect.anchorMax = Vector2.one;
hudRect.offsetMin = Vector2.zero;
hudRect.offsetMax = Vector2.zero;
Image hudBackground = runHudObject.GetComponent<Image>();
hudBackground.color = new Color(0.018f, 0.026f, 0.032f, 0.88f);
GameObject headerObject = CreateImage("HUD Header", runHudObject.transform, new Color(0.035f, 0.05f, 0.06f, 0.88f));
SetRect(headerObject.GetComponent<RectTransform>(), new Vector2(0.05f, 0.70f), new Vector2(0.95f, 0.92f), Vector2.zero, Vector2.zero);
hudTitleText = CreateText("HUD Title", headerObject.transform, 34, FontStyle.Bold, TextAnchor.MiddleLeft);
SetRect(hudTitleText.rectTransform, new Vector2(0.04f, 0.54f), new Vector2(0.96f, 0.95f), Vector2.zero, Vector2.zero);
hudStatusText = CreateText("HUD Status", headerObject.transform, 28, FontStyle.Bold, TextAnchor.MiddleLeft);
hudStatusText.color = selectedColor;
SetRect(hudStatusText.rectTransform, new Vector2(0.04f, 0.15f), new Vector2(0.96f, 0.56f), Vector2.zero, Vector2.zero);
hudDetailText = CreateText("HUD Detail", runHudObject.transform, 25, FontStyle.Normal, TextAnchor.MiddleCenter);
SetRect(hudDetailText.rectTransform, new Vector2(0.07f, 0.36f), new Vector2(0.93f, 0.64f), Vector2.zero, Vector2.zero);
hudHintText = CreateText("HUD Hint", runHudObject.transform, 23, FontStyle.Normal, TextAnchor.MiddleCenter);
hudHintText.color = mutedColor;
SetRect(hudHintText.rectTransform, new Vector2(0.07f, 0.08f), new Vector2(0.93f, 0.20f), Vector2.zero, Vector2.zero);
GameObject verticalLine = CreateImage("HUD Vertical Reticle", runHudObject.transform, new Color(0.35f, 0.82f, 1.0f, 0.38f));
SetRect(verticalLine.GetComponent<RectTransform>(), new Vector2(0.498f, 0.27f), new Vector2(0.502f, 0.36f), Vector2.zero, Vector2.zero);
GameObject horizontalLine = CreateImage("HUD Horizontal Reticle", runHudObject.transform, new Color(0.35f, 0.82f, 1.0f, 0.38f));
SetRect(horizontalLine.GetComponent<RectTransform>(), new Vector2(0.47f, 0.313f), new Vector2(0.53f, 0.319f), Vector2.zero, Vector2.zero);
}
private static void AddTrackedPoseDriverIfAvailable(GameObject cameraObject)
{
string[] typeNames =
{
"UnityEngine.SpatialTracking.TrackedPoseDriver, UnityEngine.SpatialTracking",
"UnityEngine.InputSystem.XR.TrackedPoseDriver, Unity.InputSystem"
};
for (int i = 0; i < typeNames.Length; i++)
{
Type driverType = Type.GetType(typeNames[i]);
if (driverType != null && cameraObject.GetComponent(driverType) == null)
{
cameraObject.AddComponent(driverType);
return;
}
}
}
private Text CreateText(string name, Transform parent, int fontSize, FontStyle fontStyle, TextAnchor alignment)
{
GameObject textObject = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Text));
textObject.transform.SetParent(parent, false);
Text text = textObject.GetComponent<Text>();
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = fontSize;
text.fontStyle = fontStyle;
text.alignment = alignment;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Truncate;
text.resizeTextForBestFit = true;
text.resizeTextMinSize = Mathf.Max(12, fontSize - 8);
text.resizeTextMaxSize = fontSize;
text.color = normalColor;
return text;
}
private static void SetRect(RectTransform rectTransform, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax)
{
rectTransform.anchorMin = anchorMin;
rectTransform.anchorMax = anchorMax;
rectTransform.offsetMin = offsetMin;
rectTransform.offsetMax = offsetMax;
}
private static GameObject CreateImage(string name, Transform parent, Color color)
{
GameObject imageObject = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
imageObject.transform.SetParent(parent, false);
Image image = imageObject.GetComponent<Image>();
image.color = color;
return imageObject;
}
private void LoadDraftsFromSender()
{
hostDraft = sender.host;
portDraft = sender.port;
sendHzDraft = sender.sendHz;
convertDraft = sender.convertUnityToProjectCoordinates;
}
private void HandleKeyboard()
{
if (keyboard == null)
{
return;
}
if (keyboard.status == TouchScreenKeyboard.Status.Done || keyboard.status == TouchScreenKeyboard.Status.LostFocus)
{
CommitKeyboardText(keyboard.text);
keyboard = null;
keyboardTarget = KeyboardTarget.None;
}
else if (keyboard.status == TouchScreenKeyboard.Status.Canceled)
{
panelMessage = "Edit canceled";
keyboard = null;
keyboardTarget = KeyboardTarget.None;
}
}
private void CommitKeyboardText(string value)
{
string trimmed = value == null ? string.Empty : value.Trim();
if (keyboardTarget == KeyboardTarget.Host)
{
hostDraft = trimmed;
panelMessage = "Target IP edited";
}
else if (keyboardTarget == KeyboardTarget.Port)
{
if (int.TryParse(trimmed, out int parsedPort))
{
portDraft = Mathf.Clamp(parsedPort, 1, 65535);
panelMessage = "Target port edited";
}
else
{
panelMessage = "Invalid port text";
}
}
else if (keyboardTarget == KeyboardTarget.SendHz)
{
if (TryParseFloat(trimmed, out float parsedHz))
{
sendHzDraft = Mathf.Clamp(parsedHz, 1.0f, 120.0f);
panelMessage = "Send rate edited";
}
else
{
panelMessage = "Invalid send rate text";
}
}
}
private void HandleControllerInput()
{
bool menuPressed = IsButtonPressed(CommonUsages.menuButton);
if (togglePanelWithMenuButton && menuPressed && !wasMenuPressed)
{
SetConfigPanelVisible(!IsConfigPanelVisible(), true);
}
wasMenuPressed = menuPressed;
if (!IsConfigPanelVisible())
{
return;
}
Vector2 axis = ReadPrimaryAxis();
if (Time.unscaledTime >= nextAxisTime)
{
if (axis.y > 0.55f)
{
selectedRow = (selectedRow + RowCount - 1) % RowCount;
nextAxisTime = Time.unscaledTime + AxisRepeatDelay;
}
else if (axis.y < -0.55f)
{
selectedRow = (selectedRow + 1) % RowCount;
nextAxisTime = Time.unscaledTime + AxisRepeatDelay;
}
else if (axis.x > 0.65f)
{
AdjustSelectedValue(1);
nextAxisTime = Time.unscaledTime + AxisRepeatDelay;
}
else if (axis.x < -0.65f)
{
AdjustSelectedValue(-1);
nextAxisTime = Time.unscaledTime + AxisRepeatDelay;
}
}
bool activatePressed = IsActivatePressed();
if (activatePressed && !wasActivatePressed)
{
ActivateSelectedRow();
}
wasActivatePressed = activatePressed;
bool applyPressed = IsButtonPressed(CommonUsages.secondaryButton);
if (applyPressed && !wasApplyPressed)
{
ApplyDrafts();
}
wasApplyPressed = applyPressed;
}
private void AdjustSelectedValue(int direction)
{
if (selectedRow == 1)
{
portDraft = Mathf.Clamp(portDraft + direction, 1, 65535);
panelMessage = "Target port adjusted";
}
else if (selectedRow == 2)
{
sendHzDraft = Mathf.Clamp(sendHzDraft + direction * 5.0f, 1.0f, 120.0f);
panelMessage = "Send rate adjusted";
}
else if (selectedRow == 3)
{
convertDraft = !convertDraft;
panelMessage = "Coordinate mode toggled";
}
else if (selectedRow == 4)
{
ToggleSending();
}
}
private void ActivateSelectedRow()
{
if (selectedRow == 0)
{
OpenKeyboard(KeyboardTarget.Host, hostDraft, TouchScreenKeyboardType.URL);
}
else if (selectedRow == 1)
{
OpenKeyboard(KeyboardTarget.Port, portDraft.ToString(), TouchScreenKeyboardType.NumberPad);
}
else if (selectedRow == 2)
{
OpenKeyboard(KeyboardTarget.SendHz, Mathf.RoundToInt(sendHzDraft).ToString(), TouchScreenKeyboardType.NumberPad);
}
else if (selectedRow == 3)
{
convertDraft = !convertDraft;
panelMessage = "Coordinate mode toggled";
}
else if (selectedRow == 4)
{
ToggleSending();
}
else if (selectedRow == 5)
{
ApplyDrafts();
}
}
private void OpenKeyboard(KeyboardTarget target, string text, TouchScreenKeyboardType keyboardType)
{
keyboardTarget = target;
keyboard = TouchScreenKeyboard.Open(text, keyboardType, false, false, false, false, "XR-RM UDP");
panelMessage = "Editing " + target;
}
private void ToggleSending()
{
sender.SetSending(!sender.SendEnabled);
panelMessage = sender.SendEnabled ? "UDP sending enabled" : "UDP sending paused";
}
private void ApplyDrafts()
{
bool applied = sender.ApplyConfig(hostDraft, portDraft, sendHzDraft, convertDraft, true);
panelMessage = applied ? "Saved and applied" : sender.LastError;
}
private bool IsActivatePressed()
{
return IsButtonPressed(CommonUsages.primaryButton) || IsButtonPressed(CommonUsages.triggerButton) || ReadTriggerValue() > 0.75f;
}
private static Vector2 ReadPrimaryAxis()
{
if (TryReadPrimaryAxis(XRNode.RightHand, out Vector2 rightAxis))
{
return rightAxis;
}
if (TryReadPrimaryAxis(XRNode.LeftHand, out Vector2 leftAxis))
{
return leftAxis;
}
return Vector2.zero;
}
private static bool TryReadPrimaryAxis(XRNode node, out Vector2 axis)
{
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
if (device.isValid && device.TryGetFeatureValue(CommonUsages.primary2DAxis, out axis))
{
return true;
}
axis = Vector2.zero;
return false;
}
private static bool IsButtonPressed(InputFeatureUsage<bool> usage)
{
return TryReadButton(XRNode.RightHand, usage) || TryReadButton(XRNode.LeftHand, usage);
}
private static bool TryReadButton(XRNode node, InputFeatureUsage<bool> usage)
{
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
return device.isValid && device.TryGetFeatureValue(usage, out bool pressed) && pressed;
}
private static float ReadTriggerValue()
{
float rightValue = TryReadTriggerValue(XRNode.RightHand);
if (rightValue > 0.0f)
{
return rightValue;
}
return TryReadTriggerValue(XRNode.LeftHand);
}
private static float TryReadTriggerValue(XRNode node)
{
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
if (device.isValid && device.TryGetFeatureValue(CommonUsages.trigger, out float value))
{
return value;
}
return 0.0f;
}
private void RefreshPanel()
{
if (canvas == null)
{
return;
}
RefreshConfigPanel();
RefreshRunHud();
}
private void RefreshConfigPanel()
{
titleText.text = "XR-RM PICO UDP Sender";
statusText.text = BuildConfigStatusText();
SetRow(0, "Target IP", hostDraft);
SetRow(1, "Target Port", portDraft.ToString());
SetRow(2, "Send Rate", Mathf.RoundToInt(sendHzDraft) + " Hz");
SetRow(3, "Coordinates", convertDraft ? "Project (+Z forward)" : "Unity raw");
SetRow(4, "UDP Sending", sender.SendEnabled ? "ON" : "OFF");
SetRow(5, "Save & Apply", "press trigger");
footerText.text = "Stick: select / adjust Trigger, A or X: edit\nB or Y: save Menu: run view";
}
private void RefreshRunHud()
{
if (runHudObject == null)
{
return;
}
hudTitleText.text = "XR-RM UDP Sender";
hudStatusText.text = (sender.SendEnabled ? "SENDING" : "PAUSED") + " " + BuildEndpointText();
hudDetailText.text = BuildTrackingStateText() + " " + BuildGripStateText() + "\n"
+ BuildPacketStateText(false) + " " + BuildKeepAwakeStateText();
hudHintText.text = "Menu: config panel";
}
private string BuildConfigStatusText()
{
string endpointState = BuildEndpointText();
string sendState = sender.SendEnabled ? "ON" : "OFF";
string trackingState = BuildTrackingStateText();
string packetState = BuildPacketStateText(true);
string keepAwakeState = BuildKeepAwakeStateText();
string error = string.IsNullOrEmpty(sender.LastError) ? panelMessage : sender.LastError;
return endpointState + " | " + sendState + " | " + trackingState + " | " + packetState + " | " + keepAwakeState + " | " + error;
}
private string BuildEndpointText()
{
return sender.HasEndpoint ? sender.host + ":" + sender.port : "not configured";
}
private string BuildTrackingStateText()
{
return "L " + (sender.LeftDeviceValid ? "ok" : "--") + " / R " + (sender.RightDeviceValid ? "ok" : "--");
}
private string BuildGripStateText()
{
return "Grip L " + (sender.LeftGripPressed ? "ON" : "--") + " / R " + (sender.RightGripPressed ? "ON" : "--");
}
private string BuildPacketStateText(bool compact)
{
if (sender.SentPackets == 0)
{
return compact ? "0 pkts" : "Packets 0";
}
double age = Math.Max(0.0, Time.realtimeSinceStartupAsDouble - sender.LastSendTime);
string packetCount = sender.SentPackets.ToString(CultureInfo.InvariantCulture);
string lastAge = age.ToString("0.0", CultureInfo.InvariantCulture) + "s";
return compact ? packetCount + " pkts " + lastAge : "Packets " + packetCount + " Last " + lastAge;
}
private string BuildKeepAwakeStateText()
{
if (keepAwake == null)
{
return "KeepAwake --";
}
return keepAwake.StatusText;
}
private void SetRow(int index, string label, string value)
{
Text rowText = rowTexts[index];
bool selected = index == selectedRow;
rowText.color = selected ? selectedColor : normalColor;
rowText.fontStyle = selected ? FontStyle.Bold : FontStyle.Normal;
rowText.text = (selected ? "> " : " ") + label.PadRight(14) + value;
}
private static bool TryParseFloat(string value, out float parsedValue)
{
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedValue)
|| float.TryParse(value, out parsedValue);
}
private bool IsConfigPanelVisible()
{
return configPanelObject != null && configPanelObject.activeSelf;
}
private void SetConfigPanelVisible(bool visible, bool updateMessage)
{
if (configPanelObject != null)
{
configPanelObject.SetActive(visible);
}
if (runHudObject != null)
{
runHudObject.SetActive(!visible);
}
if (updateMessage)
{
panelMessage = visible ? "Config panel" : "Run view";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d53b4ec62e44220a350bfe97f87d681
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: