// Copyright 2017 Google Inc. All rights reserved. // // Licensed under the MIT License, you may not use this file except in // compliance with the License. You may obtain a copy of the License at // // http://www.opensource.org/licenses/mit-license.php // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using UnityEngine; using UnityEngine.EventSystems; #if UNITY_2017_2_OR_NEWER using UnityEngine.XR; #else using XRSettings = UnityEngine.VR.VRSettings; #endif // UNITY_2017_2_OR_NEWER /// Implementation of _GvrPointerInputModule_ public class GvrPointerInputModuleImpl { /// Interface for controlling the actual InputModule. public IGvrInputModuleController ModuleController { get; set; } /// Interface for executing events. public IGvrEventExecutor EventExecutor { get; set; } /// Determines whether pointer input is active in VR Mode only (`true`), or all of the /// time (`false`). Set to false if you plan to use direct screen taps or other /// input when not in VR Mode. public bool VrModeOnly { get; set; } /// The GvrPointerScrollInput used to route Scroll Events through _EventSystem_ public GvrPointerScrollInput ScrollInput { get; set; } /// PointerEventData from the most recent frame. public PointerEventData CurrentEventData { get; private set; } /// The GvrBasePointer which will be responding to pointer events. public GvrBasePointer Pointer { get { return pointer; } set { if (pointer == value) { return; } TryExitPointer(); pointer = value; } } private GvrBasePointer pointer; private Vector2 lastPose; private bool isPointerHovering = false; // Active state private bool isActive = false; public bool ShouldActivateModule() { bool isVrModeEnabled = !VrModeOnly; isVrModeEnabled |= XRSettings.enabled; bool activeState = ModuleController.ShouldActivate() && isVrModeEnabled; if (activeState != isActive) { isActive = activeState; } return activeState; } public void DeactivateModule() { TryExitPointer(); ModuleController.Deactivate(); if (CurrentEventData != null) { HandlePendingClick(); HandlePointerExitAndEnter(CurrentEventData, null); CurrentEventData = null; } ModuleController.eventSystem.SetSelectedGameObject(null, ModuleController.GetBaseEventData()); } public bool IsPointerOverGameObject(int pointerId) { return CurrentEventData != null && CurrentEventData.pointerEnter != null; } public void Process() { // If the pointer is inactive, make sure it is exited if necessary. if (!IsPointerActiveAndAvailable()) { TryExitPointer(); } // Save the previous Game Object GameObject previousObject = GetCurrentGameObject(); CastRay(); UpdateCurrentObject(previousObject); UpdatePointer(previousObject); // True during the frame that the trigger has been pressed. bool triggerDown = false; // True if the trigger is held down. bool triggering = false; if (IsPointerActiveAndAvailable()) { triggerDown = Pointer.TriggerDown; triggering = Pointer.Triggering; } bool handlePendingClickRequired = !triggering; // Handle input if (!triggerDown && triggering) { HandleDrag(); } else if (triggerDown && !CurrentEventData.eligibleForClick) { // New trigger action. HandleTriggerDown(); } else if (handlePendingClickRequired) { // Check if there is a pending click to handle. HandlePendingClick(); } ScrollInput.HandleScroll(GetCurrentGameObject(), CurrentEventData, Pointer, EventExecutor); } private void CastRay() { Vector2 currentPose = lastPose; if (IsPointerActiveAndAvailable()) { currentPose = GvrMathHelpers.NormalizedCartesianToSpherical(Pointer.PointerTransform.forward); } if (CurrentEventData == null) { CurrentEventData = new PointerEventData(ModuleController.eventSystem); lastPose = currentPose; } // Store the previous raycast result. RaycastResult previousRaycastResult = CurrentEventData.pointerCurrentRaycast; // The initial cast must use the enter radius. if (IsPointerActiveAndAvailable()) { Pointer.ShouldUseExitRadiusForRaycast = false; } // Cast a ray into the scene CurrentEventData.Reset(); // Set the position to the center of the camera. // This is only necessary if using the built-in Unity raycasters. RaycastResult raycastResult; CurrentEventData.position = GvrVRHelpers.GetViewportCenter(); bool isPointerActiveAndAvailable = IsPointerActiveAndAvailable(); if (isPointerActiveAndAvailable) { RaycastAll(); raycastResult = ModuleController.FindFirstRaycast(ModuleController.RaycastResultCache); if (Pointer.ControllerInputDevice == null || Pointer.ControllerInputDevice.IsDominantHand) { CurrentEventData.pointerId = (int)GvrControllerHand.Dominant; } else { CurrentEventData.pointerId = (int)GvrControllerHand.NonDominant; } } else { raycastResult = new RaycastResult(); raycastResult.Clear(); } // If we were already pointing at an object we must check that object against the exit radius // to make sure we are no longer pointing at it to prevent flicker. if (previousRaycastResult.gameObject != null && raycastResult.gameObject != previousRaycastResult.gameObject && isPointerActiveAndAvailable) { Pointer.ShouldUseExitRadiusForRaycast = true; RaycastAll(); RaycastResult firstResult = ModuleController.FindFirstRaycast(ModuleController.RaycastResultCache); if (firstResult.gameObject == previousRaycastResult.gameObject) { raycastResult = firstResult; } } if (raycastResult.gameObject != null && raycastResult.worldPosition == Vector3.zero) { raycastResult.worldPosition = GvrMathHelpers.GetIntersectionPosition(CurrentEventData.enterEventCamera, raycastResult); } CurrentEventData.pointerCurrentRaycast = raycastResult; // Find the real screen position associated with the raycast // Based on the results of the hit and the state of the pointerData. if (raycastResult.gameObject != null) { CurrentEventData.position = raycastResult.screenPosition; } else if (IsPointerActiveAndAvailable() && CurrentEventData.enterEventCamera != null) { Vector3 pointerPos = Pointer.MaxPointerEndPoint; CurrentEventData.position = CurrentEventData.enterEventCamera.WorldToScreenPoint(pointerPos); } ModuleController.RaycastResultCache.Clear(); CurrentEventData.delta = currentPose - lastPose; lastPose = currentPose; // Check to make sure the Raycaster being used is a GvrRaycaster. if (raycastResult.module != null && !(raycastResult.module is GvrPointerGraphicRaycaster) && !(raycastResult.module is GvrPointerPhysicsRaycaster)) { Debug.LogWarning("Using Raycaster (Raycaster: " + raycastResult.module.GetType() + ", Object: " + raycastResult.module.name + "). It is recommended to use " + "GvrPointerPhysicsRaycaster or GvrPointerGrahpicRaycaster with GvrPointerInputModule."); } } private void UpdateCurrentObject(GameObject previousObject) { if (CurrentEventData == null) { return; } // Send enter events and update the highlight. GameObject currentObject = GetCurrentGameObject(); // Get the pointer target HandlePointerExitAndEnter(CurrentEventData, currentObject); // Update the current selection, or clear if it is no longer the current object. var selected = EventExecutor.GetEventHandler(currentObject); if (selected == ModuleController.eventSystem.currentSelectedGameObject) { EventExecutor.Execute(ModuleController.eventSystem.currentSelectedGameObject, ModuleController.GetBaseEventData(), ExecuteEvents.updateSelectedHandler); } else { ModuleController.eventSystem.SetSelectedGameObject(null, CurrentEventData); } // Execute hover event. if (currentObject != null && currentObject == previousObject) { EventExecutor.ExecuteHierarchy(currentObject, CurrentEventData, GvrExecuteEventsExtension.pointerHoverHandler); } } private void UpdatePointer(GameObject previousObject) { if (CurrentEventData == null) { return; } GameObject currentObject = GetCurrentGameObject(); // Get the pointer target bool isPointerActiveAndAvailable = IsPointerActiveAndAvailable(); bool isInteractive = CurrentEventData.pointerPress != null || EventExecutor.GetEventHandler(currentObject) != null || EventExecutor.GetEventHandler(currentObject) != null; if (isPointerHovering && currentObject != null && currentObject == previousObject) { if (isPointerActiveAndAvailable) { Pointer.OnPointerHover(CurrentEventData.pointerCurrentRaycast, isInteractive); } } else { // If the object's don't match or the hovering object has been destroyed // then the pointer has exited. if (previousObject != null || (currentObject == null && isPointerHovering)) { if (isPointerActiveAndAvailable) { Pointer.OnPointerExit(previousObject); } isPointerHovering = false; } if (currentObject != null) { if (isPointerActiveAndAvailable) { Pointer.OnPointerEnter(CurrentEventData.pointerCurrentRaycast, isInteractive); } isPointerHovering = true; } } } private static bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold) { if (!useDragThreshold) return true; return (pressPos - currentPos).sqrMagnitude >= threshold * threshold; } private void HandleDrag() { bool moving = CurrentEventData.IsPointerMoving(); bool shouldStartDrag = ShouldStartDrag(CurrentEventData.pressPosition, CurrentEventData.position, ModuleController.eventSystem.pixelDragThreshold, CurrentEventData.useDragThreshold); if (moving && shouldStartDrag && CurrentEventData.pointerDrag != null && !CurrentEventData.dragging) { EventExecutor.Execute(CurrentEventData.pointerDrag, CurrentEventData, ExecuteEvents.beginDragHandler); CurrentEventData.dragging = true; } // Drag notification if (CurrentEventData.dragging && moving && CurrentEventData.pointerDrag != null) { // Before doing drag we should cancel any pointer down state // And clear selection! if (CurrentEventData.pointerPress != CurrentEventData.pointerDrag) { EventExecutor.Execute(CurrentEventData.pointerPress, CurrentEventData, ExecuteEvents.pointerUpHandler); CurrentEventData.eligibleForClick = false; CurrentEventData.pointerPress = null; CurrentEventData.rawPointerPress = null; } EventExecutor.Execute(CurrentEventData.pointerDrag, CurrentEventData, ExecuteEvents.dragHandler); } } private void HandlePendingClick() { if (CurrentEventData == null || (!CurrentEventData.eligibleForClick && !CurrentEventData.dragging)) { return; } if (IsPointerActiveAndAvailable()) { Pointer.OnPointerClickUp(); } var go = CurrentEventData.pointerCurrentRaycast.gameObject; // Send pointer up and click events. EventExecutor.Execute(CurrentEventData.pointerPress, CurrentEventData, ExecuteEvents.pointerUpHandler); GameObject pointerClickHandler = EventExecutor.GetEventHandler(go); if (CurrentEventData.pointerPress == pointerClickHandler && CurrentEventData.eligibleForClick) { EventExecutor.Execute(CurrentEventData.pointerPress, CurrentEventData, ExecuteEvents.pointerClickHandler); } if (CurrentEventData != null && CurrentEventData.pointerDrag != null && CurrentEventData.dragging) { EventExecutor.ExecuteHierarchy(go, CurrentEventData, ExecuteEvents.dropHandler); EventExecutor.Execute(CurrentEventData.pointerDrag, CurrentEventData, ExecuteEvents.endDragHandler); } if (CurrentEventData != null) { // Clear the click state. CurrentEventData.pointerPress = null; CurrentEventData.rawPointerPress = null; CurrentEventData.eligibleForClick = false; CurrentEventData.clickCount = 0; CurrentEventData.clickTime = 0; CurrentEventData.pointerDrag = null; CurrentEventData.dragging = false; } } private void HandleTriggerDown() { var go = CurrentEventData.pointerCurrentRaycast.gameObject; // Send pointer down event. CurrentEventData.pressPosition = CurrentEventData.position; CurrentEventData.pointerPressRaycast = CurrentEventData.pointerCurrentRaycast; CurrentEventData.pointerPress = EventExecutor.ExecuteHierarchy(go, CurrentEventData, ExecuteEvents.pointerDownHandler) ?? EventExecutor.GetEventHandler(go); // Save the pending click state. CurrentEventData.rawPointerPress = go; CurrentEventData.eligibleForClick = true; CurrentEventData.delta = Vector2.zero; CurrentEventData.dragging = false; CurrentEventData.useDragThreshold = true; CurrentEventData.clickCount = 1; CurrentEventData.clickTime = Time.unscaledTime; // Save the drag handler as well CurrentEventData.pointerDrag = EventExecutor.GetEventHandler(go); if (CurrentEventData.pointerDrag != null) { EventExecutor.Execute(CurrentEventData.pointerDrag, CurrentEventData, ExecuteEvents.initializePotentialDrag); } if (IsPointerActiveAndAvailable()) { Pointer.OnPointerClickDown(); } } private GameObject GetCurrentGameObject() { if (CurrentEventData != null) { return CurrentEventData.pointerCurrentRaycast.gameObject; } return null; } // Modified version of BaseInputModule.HandlePointerExitAndEnter that calls EventExecutor instead of // UnityEngine.EventSystems.ExecuteEvents. private void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget) { // If we have no target or pointerEnter has been deleted then // just send exit events to anything we are tracking. // Afterwards, exit. if (newEnterTarget == null || currentPointerData.pointerEnter == null) { for (var i = 0; i < currentPointerData.hovered.Count; ++i) { EventExecutor.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler); } currentPointerData.hovered.Clear(); if (newEnterTarget == null) { currentPointerData.pointerEnter = newEnterTarget; return; } } // If we have not changed hover target. if (newEnterTarget && currentPointerData.pointerEnter == newEnterTarget) { return; } GameObject commonRoot = ModuleController.FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget); // We already an entered object from last time. if (currentPointerData.pointerEnter != null) { // Send exit handler call to all elements in the chain // until we reach the new target, or null! Transform t = currentPointerData.pointerEnter.transform; while (t != null) { // If we reach the common root break out! if (commonRoot != null && commonRoot.transform == t) break; EventExecutor.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler); currentPointerData.hovered.Remove(t.gameObject); t = t.parent; } } // Now issue the enter call up to but not including the common root. currentPointerData.pointerEnter = newEnterTarget; if (newEnterTarget != null) { Transform t = newEnterTarget.transform; while (t != null && t.gameObject != commonRoot) { EventExecutor.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler); currentPointerData.hovered.Add(t.gameObject); t = t.parent; } } } private void TryExitPointer() { if (Pointer == null) { return; } GameObject currentGameObject = GetCurrentGameObject(); if (currentGameObject) { Pointer.OnPointerExit(currentGameObject); } } private bool IsPointerActiveAndAvailable() { return pointer != null && pointer.IsAvailable; } private void RaycastAll() { ModuleController.RaycastResultCache.Clear(); ModuleController.eventSystem.RaycastAll(CurrentEventData, ModuleController.RaycastResultCache); } }