From a6e039495dae1da816816799e7212dafad818076 Mon Sep 17 00:00:00 2001 From: misterbubb Date: Sat, 24 Jan 2026 11:10:16 -0600 Subject: [PATCH] Refactor interaction system to support simultaneous multi-hand interactions Refactors the core InteractionManager to support multiple hands interacting with the same object simultaneously, eliminating the architectural limitation where 'two VRInteractors can't interact with the same interaction'. - Eliminates confusing left/right ladder split where hands would lose grip crossing zones - Removes the need for multi-collider workarounds for future interactions - Enables future interactions to leverage true multi-hand support natively --- Source/Physics/Interactions/Ladder.cs | 137 +++++++++----------------- Source/Player/VRInteractor.cs | 102 +++++++++++-------- 2 files changed, 110 insertions(+), 129 deletions(-) diff --git a/Source/Physics/Interactions/Ladder.cs b/Source/Physics/Interactions/Ladder.cs index 6bcadcf..96fb7cc 100644 --- a/Source/Physics/Interactions/Ladder.cs +++ b/Source/Physics/Interactions/Ladder.cs @@ -34,11 +34,9 @@ private void Awake() private void Update() { - if (VRSession.Instance is not { } instance) + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) return; - var player = instance.LocalPlayer.PlayerController; - if (!player.isClimbingLadder || !isActiveLadder) return; @@ -68,8 +66,9 @@ private void Update() totalMovement.z = 0; var maxMovementThisFrame = MAX_CLIMB_SPEED * Time.deltaTime; - if (Mathf.Abs(totalMovement.y) > maxMovementThisFrame) - totalMovement.y = Mathf.Sign(totalMovement.y) * maxMovementThisFrame; + totalMovement.y = Mathf.Abs(totalMovement.y) > maxMovementThisFrame + ? Mathf.Sign(totalMovement.y) * maxMovementThisFrame + : totalMovement.y; if (Mathf.Abs(totalMovement.y) > 0.001f) player.thisPlayerBody.position += totalMovement; @@ -83,27 +82,25 @@ private void Update() if (playerHeadY < topY - 0.3f) return; - Vector3 exitPosition; - - if (ladderTrigger.useRaycastToGetTopPosition) - { - var rayStart = player.transform.position + Vector3.up * 0.5f; - var rayEnd = ladderTrigger.topOfLadderPosition.position + Vector3.up * 0.5f; - - exitPosition = UnityEngine.Physics.Linecast(rayStart, rayEnd, out var hit, - StartOfRound.Instance.collidersAndRoomMaskAndDefault, - QueryTriggerInteraction.Ignore) - ? hit.point - : ladderTrigger.topOfLadderPosition.position; - } - else - { - exitPosition = ladderTrigger.topOfLadderPosition.position; - } + var exitPosition = ladderTrigger.useRaycastToGetTopPosition + ? GetRaycastExitPosition(player) + : ladderTrigger.topOfLadderPosition.position; StartCoroutine(ExitLadder(player, exitPosition)); } + private Vector3 GetRaycastExitPosition(GameNetcodeStuff.PlayerControllerB player) + { + var rayStart = player.transform.position + Vector3.up * 0.5f; + var rayEnd = ladderTrigger.topOfLadderPosition.position + Vector3.up * 0.5f; + + return UnityEngine.Physics.Linecast(rayStart, rayEnd, out var hit, + StartOfRound.Instance.collidersAndRoomMaskAndDefault, + QueryTriggerInteraction.Ignore) + ? hit.point + : ladderTrigger.topOfLadderPosition.position; + } + private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector3 exitPosition) { leftHandGripPoint = null; @@ -156,41 +153,36 @@ private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector public bool OnButtonPress(VRInteractor interactor) { - var player = VRSession.Instance.LocalPlayer.PlayerController; + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) + return false; // Store grip point in ladder's local space if (interactor.IsRightHand) { - rightHandGripPoint = - transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position); + rightHandGripPoint = transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position); rightHandInteractor = interactor; } else { - leftHandGripPoint = - transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position); + leftHandGripPoint = transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position); leftHandInteractor = interactor; } if (!player.isClimbingLadder) { - if (ladderTrigger != null && ladderTrigger.interactable) - { - isActiveLadder = true; - climbStartTime = Time.time; - player.isClimbingLadder = true; - player.thisController.enabled = false; - - player.takingFallDamage = false; - player.fallValue = 0f; - player.fallValueUncapped = 0f; - } - else - { + if (ladderTrigger == null || !ladderTrigger.interactable) return false; - } + + isActiveLadder = true; + climbStartTime = Time.time; + player.isClimbingLadder = true; + player.thisController.enabled = false; + + player.takingFallDamage = false; + player.fallValue = 0f; + player.fallValueUncapped = 0f; } - else if (player.isClimbingLadder && !isActiveLadder) + else if (!isActiveLadder) { return false; } @@ -217,7 +209,8 @@ public void OnButtonRelease(VRInteractor interactor) if (leftHandGripPoint.HasValue || rightHandGripPoint.HasValue || !isActiveLadder) return; - var player = VRSession.Instance.LocalPlayer.PlayerController; + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) + return; isActiveLadder = false; player.isClimbingLadder = false; @@ -234,18 +227,7 @@ public void OnColliderEnter(VRInteractor interactor) { } public void OnColliderExit(VRInteractor interactor) { } } -// Lightweight wrapper that forwards to the shared ladder component -internal class VRLadderInteractable : MonoBehaviour, VRInteractable -{ - public VRLadder ladder; - - public InteractableFlags Flags => InteractableFlags.BothHands; - - public bool OnButtonPress(VRInteractor interactor) => ladder.OnButtonPress(interactor); - public void OnButtonRelease(VRInteractor interactor) => ladder.OnButtonRelease(interactor); - public void OnColliderEnter(VRInteractor interactor) { } - public void OnColliderExit(VRInteractor interactor) { } -} +// Lightweight wrapper no longer needed with multi-hand InteractionManager support [LCVRPatch] [HarmonyPatch] @@ -255,18 +237,11 @@ internal static class LadderPatches [HarmonyPostfix] private static void OnLadderStart(InteractTrigger __instance) { - if (!__instance.isLadder) + if (!__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value) return; - if (Plugin.Config.DisableLadderClimbingInteraction.Value) - return; - - var ladderComponent = __instance.gameObject.AddComponent(); - - // Create two separate colliders offset to left and right - // This allows both hands to interact simultaneously - var leftHandCollider = Object.Instantiate(AssetManager.Interactable, __instance.transform); - var rightHandCollider = Object.Instantiate(AssetManager.Interactable, __instance.transform); + // Create single collider - InteractionManager now supports multiple hands per interactable + var collider = Object.Instantiate(AssetManager.Interactable, __instance.transform); if (__instance.topOfLadderPosition != null && __instance.bottomOfLadderPosition != null) { @@ -275,41 +250,25 @@ private static void OnLadderStart(InteractTrigger __instance) var midPoint = (topPos + bottomPos) / 2f; var height = Mathf.Abs(topPos.y - bottomPos.y); - // Offset left collider to the left side - leftHandCollider.transform.localPosition = midPoint + new Vector3(-0.3f, 0f, 0.3f); - leftHandCollider.transform.localScale = new Vector3(0.8f, height, 0.8f); - - // Offset right collider to the right side - rightHandCollider.transform.localPosition = midPoint + new Vector3(0.3f, 0f, 0.3f); - rightHandCollider.transform.localScale = new Vector3(0.8f, height, 0.8f); + collider.transform.localPosition = midPoint + new Vector3(0f, 0f, 0.3f); + collider.transform.localScale = new Vector3(1.2f, height, 0.8f); } else { - leftHandCollider.transform.localPosition = new Vector3(-0.3f, 0f, 0.3f); - leftHandCollider.transform.localScale = new Vector3(0.8f, 3f, 0.8f); - - rightHandCollider.transform.localPosition = new Vector3(0.3f, 0f, 0.3f); - rightHandCollider.transform.localScale = new Vector3(0.8f, 3f, 0.8f); + collider.transform.localPosition = new Vector3(0f, 0f, 0.3f); + collider.transform.localScale = new Vector3(1.2f, 3f, 0.8f); } - // Both colliders reference the same ladder component - leftHandCollider.AddComponent().ladder = ladderComponent; - rightHandCollider.AddComponent().ladder = ladderComponent; + collider.AddComponent(); - foreach (var collider in __instance.GetComponents()) - collider.enabled = false; + foreach (var existingCollider in __instance.GetComponents()) + existingCollider.enabled = false; } [HarmonyPatch(typeof(InteractTrigger), nameof(InteractTrigger.Interact))] [HarmonyPrefix] private static bool PreventLadderInteract(InteractTrigger __instance) { - if (!__instance.isLadder) - return true; - - if (Plugin.Config.DisableLadderClimbingInteraction.Value) - return true; - - return false; + return !__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value; } } \ No newline at end of file diff --git a/Source/Player/VRInteractor.cs b/Source/Player/VRInteractor.cs index 0f15ecb..be69c75 100644 --- a/Source/Player/VRInteractor.cs +++ b/Source/Player/VRInteractor.cs @@ -1,4 +1,4 @@ -using LCVR.Assets; +using LCVR.Assets; using LCVR.Input; using LCVR.Physics; using System.Collections.Generic; @@ -176,88 +176,110 @@ private readonly struct Offset(Vector3 position, Vector3 scale, Quaternion rotat } } +/// +/// Manages VR interactions between interactors (hands) and interactables (objects). +/// Supports multiple interactors per interactable, allowing both hands to interact with the same object simultaneously. +/// public class InteractionManager { - private readonly Dictionary interactableState = []; + private readonly Dictionary> interactableStates = []; public void ReportInteractables(VRInteractor interactor, VRInteractable[] interactables) { foreach (var interactable in interactables) { - if (!interactableState.ContainsKey(interactable)) - { - // Check if this hand is allowed to interact with this object - if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) - continue; + // Initialize state dictionary for this interactable if needed + if (!interactableStates.ContainsKey(interactable)) + interactableStates[interactable] = []; - if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) - continue; + var states = interactableStates[interactable]; - if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) - continue; + // Check if this hand is allowed to interact + if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) + continue; + + if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) + continue; - interactableState.Add(interactable, new InteractableState(interactor, false)); + if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) + continue; + + // Add this interactor if not already tracking + if (!states.ContainsKey(interactor)) + { + states[interactor] = new InteractableState(false); interactable.OnColliderEnter(interactor); } - // Ignore events from hand if other hand is already interacting - if (interactableState[interactable].interactor != interactor) - continue; + var state = states[interactor]; - if (!interactableState[interactable].isHeld && !interactor.isHeld && interactor.IsPressed()) + // Handle button press + if (!state.isHeld && !interactor.isHeld && interactor.IsPressed()) { var acknowledged = interactable.OnButtonPress(interactor); - interactableState[interactable].isHeld = interactor.isHeld = acknowledged; + state.isHeld = interactor.isHeld = acknowledged; } - else if (interactableState[interactable].isHeld && !interactor.IsPressed()) + // Handle button release + else if (state.isHeld && !interactor.IsPressed()) { interactable.OnButtonRelease(interactor); - interactableState[interactable].isHeld = interactor.isHeld = false; + state.isHeld = interactor.isHeld = false; } } - foreach (var interactable in interactableState.Keys) + // Clean up interactors that left the collision zone + var interactablesToRemove = new List(); + + foreach (var (interactable, states) in interactableStates) { - // Ignore if this state is being managed by another hand - if (interactableState[interactable].interactor != interactor) + if (!states.ContainsKey(interactor)) continue; if (interactables.Contains(interactable)) continue; - // Ignore if button is still being held down - if (interactableState[interactable].isHeld) + var state = states[interactor]; + + // Release if still held + if (state.isHeld) + { if (interactor.IsPressed()) continue; - else { - interactable.OnButtonRelease(interactor); - interactor.isHeld = false; - } + + interactable.OnButtonRelease(interactor); + interactor.isHeld = false; + } - interactableState.Remove(interactable); interactable.OnColliderExit(interactor); + states.Remove(interactor); - // Break to not make C# shit itself, if more need to be removed it'll happen next frame - break; + // Clean up empty state dictionaries + if (states.Count == 0) + interactablesToRemove.Add(interactable); } + + foreach (var interactable in interactablesToRemove) + interactableStates.Remove(interactable); } public void ResetState() { - foreach (var interactable in interactableState.Keys) + foreach (var (interactable, states) in interactableStates) { - var state = interactableState[interactable]; - - if (state.isHeld) - interactable.OnButtonRelease(state.interactor); - - interactable.OnColliderExit(state.interactor); + foreach (var (interactor, state) in states) + { + if (state.isHeld) + interactable.OnButtonRelease(interactor); + + interactable.OnColliderExit(interactor); + } } + + interactableStates.Clear(); } - private class InteractableState(VRInteractor interactor, bool isHeld) + private class InteractableState(bool isHeld) { - public VRInteractor interactor = interactor; public bool isHeld = isHeld; } } \ No newline at end of file