I am creating an AR game in where I would like to virtualize an 8-ball pool table. Since semantic labeling is practically useless, I would like to have some manual placement of objects and some automatization. However I can't seem to fix the placement of the virtual pockets for my table. I am placing 3 pockets manually (from my initial perspective these are going to be bottom left, left middle and bottom - the order can be argued, but it is going to be a fixed one), the other 3 pockets are going to be automatically generated. Hovewer most of the time they are weirdly rotated (as group around a pivot) as they are all parented to a parent game object whose parent is the script I am using to place the pockets and autogenerate the missing ones. The first image shows how my ther markers are placed (aligning them is going to be another issue), the second how I imagine it is correct and third one how most of the time the code flips it incorrectly to the right.

The current code that handles the logic is the following:
using Oculus.Interaction; using System.Collections.Generic; using TMPro; using UnityEngine; public class PocketSetupManager : MonoBehaviour { [Header("Marker Placement Settings")] [Tooltip("Prefab for the pocket marker (must have collider, rigidbody, Grabbable, HandGrabInteractable, etc.).")] public GameObject markerPrefab; public GameObject pocketMarkerRoot; //OVR Controls [Tooltip("Reference to left hand OVRHand component (for gesture detection).")] public OVRHand leftHand; [Tooltip("Reference to right hand OVRHand component (for gesture detection).")] public OVRHand rightHand; [Tooltip("Event sources for left and right source.")] public OVRMicrogestureEventSource LeftHandEventSource; public OVRMicrogestureEventSource RightHandEventSource; [Tooltip("Reference to the hand visual components.")] public HandVisual leftHandVisual; public HandVisual rightHandVisual; [Tooltip("Controller button to place a marker (fallback). e.g., Button.One = A (Right) or X (Left).")] public OVRInput.Button placeMarkerButton = OVRInput.Button.One; [Tooltip("Controller button to undo last marker (fallback). e.g., Button.Two = B (Right) or Y (Left).")] public OVRInput.Button undoMarkerButton = OVRInput.Button.Two; [Header("UI")] [Tooltip("Floating instruction text displayed to the user")] public TextMeshProUGUI instructionTextUI; [Header("Privates")] [Tooltip("Floating instruction text displayed to the user")] [SerializeField] private string _instructionText = string.Empty; [SerializeField] private sbyte _totalPockedNeeded = 3; private float _yCoordinateValueForMarkers = 0; private readonly string[] _pocketNames = { PocketName.BottomLeftCorner.ToString(), PocketName.MiddleLeftCorner.ToString(), PocketName.BottomRightCorner.ToString(), PocketName.TopLeftCorner.ToString(), PocketName.MiddleRightCorner.ToString(), PocketName.TopRightCorner.ToString(), }; private List<GameObject> _placedMarkers = new(6); private sbyte _markersPlacedCount = 0; private bool _groupModeActivate = false; private bool _allPocketsCalculated = false; // Start is called once before the first execution of Update after the MonoBehaviour is created private void Awake() { if (instructionTextUI != null) { instructionTextUI.text = _instructionText; UpdateInstructionUI(); } else { Debug.LogWarning("Instruction text UI refence not set."); } } void Start() { if (LeftHandEventSource == null || RightHandEventSource == null) { Debug.LogError($"No {nameof(OVRMicrogestureEventSource)} component attached to this gameobject."); } else { LeftHandEventSource.GestureRecognizedEvent.AddListener(gesture => OnMicrogestureRecognized(leftHand, gesture)); RightHandEventSource.GestureRecognizedEvent.AddListener(gesture => OnMicrogestureRecognized(rightHand, gesture)); } } // Update is called once per frame void Update() { if (_groupModeActivate) return; } void OnApplicationQuit() { LeftHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); RightHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); } private void OnDestroy() { LeftHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); RightHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); } public void OnMicrogestureRecognized(OVRHand hand, OVRHand.MicrogestureType gestureType) { Debug.Log($"Microgesture event: {gestureType} from hand {hand.name}."); //Right hand -> HandType is marked as internal. var isRightHand = hand.name.ToLower().Contains("right"); if (isRightHand) { switch (gestureType) { case OVRHand.MicrogestureType.ThumbTap: PlacePocketMarker(hand); break; case OVRHand.MicrogestureType.SwipeRight: FinalizePlacements(); break; case OVRHand.MicrogestureType.SwipeLeft: case OVRHand.MicrogestureType.SwipeForward: case OVRHand.MicrogestureType.SwipeBackward: default: Debug.Log("Gesture currently not supported."); break; } } UpdateInstructionUI(); } private void FinalizePlacements() { if (_allPocketsCalculated) { _instructionText = "All pockets have been placed at their respected positions. Nothing to do here."; Debug.Log(_instructionText); UpdateInstructionUI(); return; } if (_markersPlacedCount < _totalPockedNeeded) { Debug.LogWarning("Cannot finalize: not all required pocket markers are placed yet."); return; } Vector3 bottomLeft = _placedMarkers[(byte)PocketName.BottomLeftCorner].transform.position; Vector3 middleLeft = _placedMarkers[(byte)PocketName.MiddleLeftCorner].transform.position; Vector3 bottomRight = _placedMarkers[(byte)PocketName.BottomRightCorner].transform.position; //Top left Vector3 bottomLeftToLeftMiddle = middleLeft - bottomLeft; Vector3 topLeft = bottomLeft + 2 * bottomLeftToLeftMiddle; //Right middle Vector3 bottomLeftToBottomRight = bottomRight - bottomLeft; Vector3 middleRight = middleLeft + bottomLeftToBottomRight; //Top Right Vector3 topRight = topLeft + bottomLeftToBottomRight; topLeft.y = _yCoordinateValueForMarkers; middleRight.y = _yCoordinateValueForMarkers; topRight.y = _yCoordinateValueForMarkers; GameObject topLeftMarker = Instantiate(markerPrefab, topLeft, Quaternion.identity); topLeftMarker.name = _pocketNames[(byte)PocketName.TopLeftCorner]; GameObject middleRightMarker = Instantiate(markerPrefab, middleRight, Quaternion.identity); middleRightMarker.name = _pocketNames[(byte)PocketName.MiddleRightCorner]; GameObject topRightMarker = Instantiate(markerPrefab, topRight, Quaternion.identity); topRightMarker.name = _pocketNames[(byte)PocketName.TopRightCorner]; if (pocketMarkerRoot != null) { topLeftMarker.transform.SetParent(pocketMarkerRoot.transform, true); middleRightMarker.transform.SetParent(pocketMarkerRoot.transform, true); topRightMarker.transform.SetParent(pocketMarkerRoot.transform, true); } _placedMarkers[(byte)PocketName.TopLeftCorner] = topLeftMarker; _placedMarkers[(byte)PocketName.MiddleRightCorner] = middleRightMarker; _placedMarkers[(byte)PocketName.TopRightCorner] = topRightMarker; _markersPlacedCount = 6; _instructionText = "All 6 pockets positioned!"; Debug.Log("All pockets placed and finalized."); _allPocketsCalculated = true; UpdateInstructionUI(); } private void PlacePocketMarker(OVRHand hand) { if (_markersPlacedCount == _totalPockedNeeded) { _instructionText = "All of the required pockets have been instantiated. You can grab them and reposition them, before swiping right."; UpdateInstructionUI(); return; } //if (_groupModeActivate) return; if (pocketMarkerRoot == null) { Debug.LogError("No pocket marker root has been instantiated or has been deleted in runtime, so no (furher) pockets can be placed."); return; } var palmPosition = GetPalmWorldPosition(hand); // For the first pocket, store the Y-level if (_markersPlacedCount == 0) { _yCoordinateValueForMarkers = palmPosition.y; Debug.Log($"PalmPosition {palmPosition.y}"); Debug.DrawRay(palmPosition, Vector3.up * 0.1f, Color.green, 2f); } // Override Y to always match the first pocket’s Y palmPosition.y = _yCoordinateValueForMarkers; // Instantiate marker at (X,Z) of your hand + fixed Y GameObject marker = Instantiate(markerPrefab, palmPosition, Quaternion.identity); marker.name = _pocketNames[_markersPlacedCount]; marker.transform.SetParent(pocketMarkerRoot.transform, worldPositionStays: true); _placedMarkers.Add(marker); _markersPlacedCount++; // Feedback message sbyte remaining = (sbyte)(_totalPockedNeeded - _markersPlacedCount); _instructionText = remaining > 0 ? $"<b>{marker.name}</b> placed! {remaining} more to go…" : $"<b>{marker.name}</b> placed! Swipe right to confirm."; UpdateInstructionUI(); } private Vector3 GetPalmWorldPosition(OVRHand hand) { HandVisual handVisual = (hand == leftHand) ? leftHandVisual : rightHandVisual; if (handVisual == null) { Debug.LogWarning("Hand visual component not assigned."); return hand.transform.position; } return handVisual.GetTransformByHandJointId(Oculus.Interaction.Input.HandJointId.HandPalm).position; } private void UpdateInstructionUI() { if (instructionTextUI != null) { instructionTextUI.text = _instructionText; _instructionText = string.Empty; } else { Debug.LogWarning("Instruction text UI not assigned."); } } public void UndoLastMarker() { if (_markersPlacedCount == 0) return; _markersPlacedCount--; _markersPlacedCount = _markersPlacedCount < 0 ? (sbyte)0 : _markersPlacedCount; } } The prefab of the pocket also contains a scripts which limits the interaction to X and Z.
using UnityEngine; [RequireComponent(typeof(Transform))] public class XZOnlyConstraint : MonoBehaviour { [Tooltip("Whether this marker can currently be grabbed by the user.")] public bool GrabbableEnabled = true; private Vector3 _initialPosition; private Quaternion _initialRotation; private bool _isInitialized = false; public void Initialize(Quaternion worldRotation) { _initialPosition = transform.position; _initialRotation = worldRotation; _isInitialized = true; } public void Initialize() => Initialize(transform.rotation); private void LateUpdate() { if (!_isInitialized || !GrabbableEnabled || !this.isActiveAndEnabled) return; // Lock Y height Vector3 pos = transform.position; pos.y = _initialPosition.y; // Lock rotation to preserved transform.SetPositionAndRotation(pos, _initialRotation); } } How to aproach this issue of placing the pockets so that I can tackle other parts of defining the play area and what I am doing wrong? The essential features of the pocket placement part are:
- Placing the pockets, having the manually placed pockets aligned.
- Autogenerated the remaining pockets.
- Undoing some (or all) misplaced pockets.
- Confirming the placement and keep it somewhere so someone from the other Quest can acces it?