├── .gitignore ├── LICENSE ├── VRInputManager.cs ├── README.md └── VRControllerInputModule.cs /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /Assets/AssetStoreTools* 7 | 8 | # Autogenerated VS/MD solution and project files 9 | ExportedObj/ 10 | *.csproj 11 | *.unityproj 12 | *.sln 13 | *.suo 14 | *.tmp 15 | *.user 16 | *.userprefs 17 | *.pidb 18 | *.booproj 19 | *.svd 20 | 21 | 22 | # Unity3D generated meta files 23 | *.pidb.meta 24 | 25 | # Unity3D Generated File On Crash Reports 26 | sysinfo.txt 27 | 28 | # Builds 29 | *.apk 30 | *.unitypackage 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 2017 Sergey-Shamov. Based on original Unity source code by 4 | Copyright (c) 2014, Unity Technologies 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /VRInputManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public static class VRInputManager 6 | { 7 | private static bool m_isKeyPressed = false; // true if controller button is pressed 8 | private static Transform m_controllerTransform; // controller transform 9 | private static bool m_controllerActive = true; // TOOD: actual value // true if controller is active (movement should be tracked) 10 | 11 | public static void SetIsControllerButtonPressed(bool isPressed) 12 | { 13 | m_isKeyPressed = isPressed; 14 | } 15 | 16 | public static bool GetIsControllerButtonPressed() 17 | { 18 | return m_isKeyPressed; 19 | } 20 | 21 | public static void SetControllerTransform(Transform transform) 22 | { 23 | m_controllerTransform = transform; 24 | } 25 | 26 | public static Transform GetControllerTransform() 27 | { 28 | return m_controllerTransform; 29 | } 30 | 31 | public static void SetControllerActive(bool active) 32 | { 33 | m_controllerActive = active; 34 | } 35 | 36 | public static bool GetControllerActive() 37 | { 38 | return m_controllerActive; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity-VR-InputModule 2 | A very basic input module for laser pointer style controller-UI interaction. 3 | 4 | Contents: 5 | VRControllerInputModule - an input module for Unity event system that enables point and click functionality for VR controllers. 6 | VRInputManager - helper class for wrapping controller parameters (button state, etc.) 7 | 8 | # How to 9 | 1. Attach a camera to your controller; 10 | 2. Set the camera's culling mask to none; 11 | 3. Assign VRControllerInputModule to the EventSystem; 12 | 4. Assign the camera to VRControllerInputMode's Ui Camera; 13 | 5. Assign the camera as Event Camera to all of the canvases; 14 | 6. (For click to work) Call SetIsControllerButtonPressed on your controller's button press. 15 | 16 | # Upsides 17 | Very simple and short module. You can click and drag all you want using standard Unity UI. Hardware-independent. 18 | 19 | # Downsides 20 | 1) Panels are hit by raycast, but canvases are not. On a panel, you can drag a slider while pointing at the panel, but on a canvas you can only drag it while pointing at it. 21 | 2) A button will remain hovered if it was pressed and the controller moved away and released outside of the button. 22 | 23 | 24 | I'm going to fix that later, but no promises:) 25 | 26 | For more details please see my post: http://codrspace.com/Sergey-Shamov/laser-pointer-vr-ui/ 27 | -------------------------------------------------------------------------------- /VRControllerInputModule.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.EventSystems; 3 | 4 | public class VRControllerInputModule : BaseInputModule 5 | { 6 | [Tooltip("A camera mounted on the controller")] 7 | public Camera uiCamera; 8 | 9 | [Tooltip("The threshold is a square length at which the cursor will begin drag. Lenght is measured in world coordinates.")] 10 | public float dragThreshold = 0.1f; 11 | 12 | // debug 13 | public UnityEngine.UI.Text uiDebugText; 14 | private bool m_useDebugText = false; 15 | private string[] m_debugStrings = new string[5]; 16 | // debug 17 | 18 | private bool m_isButtonPressed = false; // true if controller's button is currently pressed, false otherwise 19 | private bool m_isButtonPressedChanged = false; // true if controller's button was pressed or released during the last frame 20 | private float m_pressedDistance; // Distance the cursor travelled while pressed. 21 | 22 | private Vector2 m_cameraCenter; 23 | private Vector3 m_lastRaycastHitPoint; 24 | private PointerEventData m_pointerEventData; 25 | private bool m_isActive = false; 26 | 27 | protected override void Start() 28 | { 29 | base.Start(); 30 | if (null != uiCamera) 31 | { 32 | m_isActive = true; 33 | m_cameraCenter = new Vector2(uiCamera.pixelWidth / 2, uiCamera.pixelHeight / 2); 34 | 35 | m_useDebugText = null != uiDebugText; 36 | WriteDebug("Camera center: " + m_cameraCenter.ToString()); 37 | } 38 | } 39 | 40 | public override void Process() 41 | { 42 | if (m_isActive) 43 | { 44 | bool usedEvent = SendUpdateEventToSelectedObject(); 45 | 46 | MyUpdateControllerData(); 47 | ProcessControllerEvent(); 48 | } 49 | } 50 | 51 | private void MyUpdateControllerData() 52 | { 53 | m_isButtonPressedChanged = false; 54 | if (m_isButtonPressed != VRInputManager.GetIsControllerButtonPressed()) 55 | { 56 | m_isButtonPressedChanged = true; 57 | m_isButtonPressed = VRInputManager.GetIsControllerButtonPressed(); 58 | } 59 | } 60 | 61 | private void ProcessControllerEvent() 62 | { 63 | PointerEventData eventData = GetPointerEventData(); 64 | 65 | ProcessPress(eventData); 66 | ProcessMove(eventData); 67 | ProcessDrag(eventData); 68 | } 69 | 70 | private PointerEventData GetPointerEventData() 71 | { 72 | // Currently the module is made for a single controller with one button. 73 | // That means that we only have a single pointer. 74 | if (null == m_pointerEventData) 75 | m_pointerEventData = new PointerEventData(eventSystem); 76 | 77 | if (VRInputManager.GetControllerActive()) 78 | { 79 | m_pointerEventData.position = m_cameraCenter; 80 | 81 | m_pointerEventData.scrollDelta = Vector2.zero; 82 | m_pointerEventData.button = PointerEventData.InputButton.Left; 83 | eventSystem.RaycastAll(m_pointerEventData, m_RaycastResultCache); 84 | var raycast = FindFirstRaycast(m_RaycastResultCache); 85 | 86 | // Delta is used to define if the cursor was moved. 87 | // We will also use it for drag threshold calculation, for which we'll store world distance 88 | // between the last and the current raycasts (will actually use sqrmagnitude for its speed). 89 | Ray ray = new Ray(uiCamera.transform.position, uiCamera.transform.forward); 90 | Vector3 hitPoint = ray.GetPoint(raycast.distance); 91 | m_pointerEventData.delta = new Vector2((hitPoint - m_lastRaycastHitPoint).sqrMagnitude, 0); 92 | m_lastRaycastHitPoint = hitPoint; 93 | 94 | m_pointerEventData.pointerCurrentRaycast = raycast; 95 | 96 | // Debug 97 | if (m_RaycastResultCache.Count > 0) 98 | WriteDebug("Raycast hit " + raycast.gameObject.name); 99 | 100 | m_RaycastResultCache.Clear(); 101 | } 102 | 103 | return m_pointerEventData; 104 | } 105 | 106 | // Copied from PointerInputModule 107 | private void ProcessDrag(PointerEventData eventData) 108 | { 109 | WriteDebug(eventData.delta.sqrMagnitude.ToString()); 110 | 111 | // If pointer is not moving or if a button is not pressed (or pressed control did not return drag handler), do nothing 112 | if (!eventData.IsPointerMoving() || eventData.pointerDrag == null) 113 | return; 114 | 115 | // We are eligible for drag. If drag did not start yet, add drag distance 116 | if (!eventData.dragging) 117 | { 118 | m_pressedDistance += eventData.delta.x; 119 | 120 | if (ShouldStartDrag(eventData)) 121 | { 122 | ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.beginDragHandler); 123 | eventData.dragging = true; 124 | } 125 | } 126 | 127 | // Drag notification 128 | if (eventData.dragging) 129 | { 130 | // Before doing drag we should cancel any pointer down state 131 | // And clear selection! 132 | if (eventData.pointerPress != eventData.pointerDrag) 133 | { 134 | ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler); 135 | 136 | eventData.eligibleForClick = false; 137 | eventData.pointerPress = null; 138 | eventData.rawPointerPress = null; 139 | } 140 | ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler); 141 | } 142 | } 143 | 144 | private bool ShouldStartDrag(PointerEventData eventData) 145 | { 146 | return !m_isButtonPressedChanged && (m_pressedDistance > dragThreshold); 147 | } 148 | 149 | // Copied from PointerInputModule 150 | private void ProcessMove(PointerEventData eventData) 151 | { 152 | var targetGO = eventData.pointerCurrentRaycast.gameObject; 153 | HandlePointerExitAndEnter(eventData, targetGO); 154 | } 155 | 156 | // modified StandaloneInputModule 157 | private void ProcessPress(PointerEventData eventData) 158 | { 159 | var currentOverGo = eventData.pointerCurrentRaycast.gameObject; 160 | 161 | // PointerDown notification 162 | if (MyIsButtonPressedThisFrame()) 163 | { 164 | eventData.eligibleForClick = true; 165 | eventData.delta = Vector2.zero; 166 | eventData.useDragThreshold = true; 167 | eventData.pressPosition = eventData.position; 168 | eventData.pointerPressRaycast = eventData.pointerCurrentRaycast; 169 | 170 | DeselectIfSelectionChanged(currentOverGo, eventData); 171 | 172 | var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.pointerDownHandler); 173 | 174 | // didnt find a press handler... search for a click handler 175 | if (newPressed == null) 176 | newPressed = ExecuteEvents.GetEventHandler(currentOverGo); 177 | 178 | eventData.pointerPress = newPressed; // TODO:remove? 179 | m_pressedDistance = 0; 180 | eventData.rawPointerPress = currentOverGo; 181 | 182 | eventData.clickTime = Time.unscaledTime; 183 | 184 | // Save the drag handler as well 185 | eventData.pointerDrag = ExecuteEvents.GetEventHandler(currentOverGo); 186 | 187 | if (eventData.pointerDrag != null) 188 | ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.initializePotentialDrag); 189 | } 190 | 191 | // PointerUp notification 192 | if (MyIsButtonReleasedThisFrame()) 193 | { 194 | ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler); 195 | 196 | // see if we button up on the same element that we clicked on... 197 | var pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo); 198 | 199 | // PointerClick and Drop events 200 | if (eventData.pointerPress == pointerUpHandler && eventData.eligibleForClick) 201 | { 202 | ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler); 203 | } 204 | else if (eventData.pointerDrag != null && eventData.dragging) 205 | { 206 | ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler); 207 | } 208 | 209 | eventData.eligibleForClick = false; 210 | eventData.pointerPress = null; 211 | m_pressedDistance = 0; // just in case 212 | eventData.rawPointerPress = null; 213 | 214 | if (eventData.pointerDrag != null && eventData.dragging) 215 | ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.endDragHandler); 216 | 217 | eventData.dragging = false; 218 | eventData.pointerDrag = null; 219 | 220 | // redo pointer enter / exit to refresh state 221 | // so that if we hovered over something that ignored it before 222 | // due to having pressed on something else 223 | // it now gets it. 224 | if (currentOverGo != eventData.pointerEnter) 225 | { 226 | HandlePointerExitAndEnter(eventData, null); 227 | HandlePointerExitAndEnter(eventData, currentOverGo); 228 | } 229 | } 230 | } 231 | 232 | // Copied from PointerInputModule 233 | private void DeselectIfSelectionChanged(GameObject currentOverGo, BaseEventData pointerEvent) 234 | { 235 | // Selection tracking 236 | var selectHandlerGO = ExecuteEvents.GetEventHandler(currentOverGo); 237 | // if we have clicked something new, deselect the old thing 238 | // leave 'selection handling' up to the press event though. 239 | if (selectHandlerGO != eventSystem.currentSelectedGameObject) 240 | eventSystem.SetSelectedGameObject(null, pointerEvent); 241 | } 242 | 243 | // Copied from StandaloneInputModule 244 | private bool SendUpdateEventToSelectedObject() 245 | { 246 | if (eventSystem.currentSelectedGameObject == null) 247 | return false; 248 | 249 | var data = GetBaseEventData(); 250 | ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler); 251 | return data.used; 252 | } 253 | 254 | private bool MyIsButtonReleasedThisFrame() 255 | { 256 | return (m_isButtonPressedChanged && !m_isButtonPressed); 257 | } 258 | 259 | private bool MyIsButtonPressedThisFrame() 260 | { 261 | return (m_isButtonPressedChanged && m_isButtonPressed); 262 | } 263 | 264 | // Debug 265 | private void WriteDebug(string text) 266 | { 267 | if (!m_useDebugText) 268 | return; 269 | 270 | m_debugStrings[4] = m_debugStrings[3]; 271 | m_debugStrings[3] = m_debugStrings[2]; 272 | m_debugStrings[2] = m_debugStrings[1]; 273 | m_debugStrings[1] = m_debugStrings[0]; 274 | m_debugStrings[0] = text; 275 | uiDebugText.text = string.Format("{0}\n{1}\n{2}\n{3}\n{4}", m_debugStrings[0], m_debugStrings[1], m_debugStrings[2], m_debugStrings[3], m_debugStrings[4]); 276 | } 277 | } --------------------------------------------------------------------------------