// ThrottleLever.cs // Zweck: XR-Hebel entlang definierter Rail bewegen, Output 0..1 als throttle. Stabil gegen XRI/Physik. using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; [DefaultExecutionOrder(1000)] // nach den meisten Systemen laufen [RequireComponent(typeof(XRGrabInteractable))] public class ThrottleLever : MonoBehaviour { [SerializeField] private bool forceStartAtZero = true; [Header("References")] public Transform handle; // visueller Hebel public Transform startPoint; // Rail-Anfang public Transform endPoint; // Rail-Ende public Transform space; // Referenzrahmen der Rail [SerializeField] private Transform shipRoot; [Header("Output")] [Range(0f, 1f)] public float throttle = 0f; [Header("Diagnostics")] public bool selfTestMode = false; public float selfTestSpeed = 0.5f; private XRGrabInteractable grab; private bool isGrabbed; private Transform follow; // Attach des Interactors private float cachedT; private void Awake() { grab = GetComponent(); if (!handle || !startPoint || !endPoint) { Debug.LogError("[ThrottleLever] handle/start/end zuweisen."); enabled = false; return; } if (!space) space = startPoint && startPoint.parent ? startPoint.parent : handle.parent; // sicherstellen, dass space mit dem Schiff mitfährt if (shipRoot && space && !space.IsChildOf(shipRoot)) { Vector3 wp = space.position; Quaternion wq = space.rotation; space.SetParent(shipRoot, worldPositionStays: true); space.position = wp; space.rotation = wq; Debug.LogWarning("[ThrottleLever] 'space' unter shipRoot angehängt (fährt nun mit)."); } // Rail-Validierung var a = space.InverseTransformPoint(startPoint.position); var b = space.InverseTransformPoint(endPoint.position); if ((b - a).sqrMagnitude < 1e-6f) { Debug.LogError("[ThrottleLever] startPoint und endPoint liegen aufeinander."); enabled = false; return; } // Physik am Handle (statisch, wir bewegen ihn selbst) var rb = handle.GetComponent() ?? handle.gameObject.AddComponent(); rb.useGravity = false; rb.isKinematic = true; if (!handle.GetComponent()) handle.gameObject.AddComponent(); // XRI soll unsere Pose NICHT direkt treiben grab.trackPosition = false; grab.trackRotation = false; grab.throwOnDetach = false; grab.selectEntered.AddListener(OnGrab); grab.selectExited.AddListener(OnRelease); Application.onBeforeRender += OnBeforeRenderPose; } private void OnDestroy() { if (grab) { grab.selectEntered.RemoveListener(OnGrab); grab.selectExited.RemoveListener(OnRelease); } Application.onBeforeRender -= OnBeforeRenderPose; } private void Start() { if (forceStartAtZero) { cachedT = 0f; throttle = 0f; SetHandleByT(cachedT); } else { ProjectHandleOntoRailFromCurrent(); } } private void Update() { if (!space) return; Vector3 a = space.InverseTransformPoint(startPoint.position); Vector3 b = space.InverseTransformPoint(endPoint.position); Vector3 ab = b - a; float abLen2 = ab.sqrMagnitude; if (abLen2 < 1e-6f) return; if (selfTestMode) { cachedT = 0.5f + 0.5f * Mathf.Sin(Time.time * selfTestSpeed * Mathf.PI * 2f); } else if (isGrabbed && follow) { Vector3 pLocal = space.InverseTransformPoint(follow.position); cachedT = Mathf.Clamp01(Vector3.Dot(pLocal - a, ab) / abLen2); } else { Vector3 hLocal = space.InverseTransformPoint(handle.position); cachedT = Mathf.Clamp01(Vector3.Dot(hLocal - a, ab) / abLen2); } throttle = cachedT; // Vorläufig schreiben… SetHandleByT(cachedT); // …und später noch einmal absichern } private void LateUpdate() => SetHandleByT(cachedT); private void OnBeforeRenderPose() => SetHandleByT(cachedT); private void SetHandleByT(float t) { Vector3 a = space.InverseTransformPoint(startPoint.position); Vector3 b = space.InverseTransformPoint(endPoint.position); Vector3 targetLocal = Vector3.Lerp(a, b, Mathf.Clamp01(t)); handle.position = space.TransformPoint(targetLocal); } public void SetNormalized(float t) { t = Mathf.Clamp01(t); cachedT = throttle = t; SetHandleByT(t); } public void ResetToZero() => SetNormalized(0f); private void OnGrab(SelectEnterEventArgs args) { isGrabbed = true; if (args.interactorObject is XRBaseInteractor xrInteractor) follow = xrInteractor.attachTransform; else follow = args.interactorObject.GetAttachTransform(args.interactableObject); if (!follow) Debug.LogWarning("[ThrottleLever] attachTransform nicht gefunden."); if (space && handle.parent != space) handle.SetParent(space, true); grab.trackPosition = false; grab.trackRotation = false; } private void OnRelease(SelectExitEventArgs args) { isGrabbed = false; follow = null; ProjectHandleOntoRailFromCurrent(); if (space && handle.parent != space) handle.SetParent(space, true); } private void ProjectHandleOntoRailFromCurrent() { Vector3 a = space.InverseTransformPoint(startPoint.position); Vector3 b = space.InverseTransformPoint(endPoint.position); Vector3 ab = b - a; float abLen2 = ab.sqrMagnitude; if (abLen2 < 1e-6f) return; Vector3 hLocal = space.InverseTransformPoint(handle.position); float t = Mathf.Clamp01(Vector3.Dot(hLocal - a, ab) / abLen2); throttle = cachedT = t; SetHandleByT(t); } public void SetGrabEnabled(bool enabled) { if (!grab) grab = GetComponent(); if (grab) grab.enabled = enabled; } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!startPoint || !endPoint) return; Gizmos.color = Color.yellow; Gizmos.DrawLine(startPoint.position, endPoint.position); } #endif }