No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UIvsGameInputHandler.cs 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEngine.EventSystems;
  4. using UnityEngine.InputSystem;
  5. using UnityEngine.UI;
  6. #if UNITY_EDITOR
  7. using UnityEditor;
  8. using UnityEditorInternal;
  9. #endif
  10. public class UIvsGameInputHandler : MonoBehaviour
  11. {
  12. public Text statusBarText;
  13. public GameObject inGameUI;
  14. public GameObject mainMenuUI;
  15. public GameObject menuButton;
  16. public GameObject firstButtonInMainMenu;
  17. public GameObject firstNavigationSelection;
  18. [Space]
  19. public PlayerInput playerInput;
  20. public GameObject projectile;
  21. [Space]
  22. [Tooltip("Multiplier for Pointer.delta values when adding to rotation.")]
  23. public float m_MouseLookSensitivity = 0.1f;
  24. [Tooltip("Rotation per second with fully actuated Gamepad/joystick stick.")]
  25. public float m_GamepadLookSpeed = 10f;
  26. private bool m_OpenMenuActionTriggered;
  27. private bool m_ResetCameraActionTriggered;
  28. private bool m_FireActionTriggered;
  29. internal bool m_UIEngaged;
  30. private Vector2 m_Rotation;
  31. private InputAction m_LookEngageAction;
  32. private InputAction m_LookAction;
  33. private InputAction m_CancelAction;
  34. private InputAction m_UIEngageAction;
  35. private GameObject m_LastNavigationSelection;
  36. private Mouse m_Mouse;
  37. private Vector2? m_MousePositionToWarpToAfterCursorUnlock;
  38. internal enum State
  39. {
  40. InGame,
  41. InGameControllingCamera,
  42. InMenu,
  43. }
  44. internal State m_State;
  45. internal enum ControlStyle
  46. {
  47. None,
  48. KeyboardMouse,
  49. Touch,
  50. GamepadJoystick,
  51. }
  52. internal ControlStyle m_ControlStyle;
  53. public void OnEnable()
  54. {
  55. // By default, hide menu and show game UI.
  56. inGameUI.SetActive(true);
  57. mainMenuUI.SetActive(false);
  58. menuButton.SetActive(false);
  59. // Look up InputActions on the player so we don't have to do this over and over.
  60. m_LookEngageAction = playerInput.actions["LookEngage"];
  61. m_LookAction = playerInput.actions["Look"];
  62. m_CancelAction = playerInput.actions["UI/Cancel"];
  63. m_UIEngageAction = playerInput.actions["UIEngage"];
  64. m_State = State.InGame;
  65. }
  66. // This is called when PlayerInput updates the controls bound to its InputActions.
  67. public void OnControlsChanged()
  68. {
  69. // We could determine the types of controls we have from the names of the control schemes or their
  70. // contents. However, a way that is both easier and more robust is to simply look at the kind of
  71. // devices we have assigned to us. We do not support mixed models this way but this does correspond
  72. // to the limitations of the current control code.
  73. if (playerInput.GetDevice<Touchscreen>() != null) // Note that Touchscreen is also a Pointer so check this first.
  74. m_ControlStyle = ControlStyle.Touch;
  75. else if (playerInput.GetDevice<Pointer>() != null)
  76. m_ControlStyle = ControlStyle.KeyboardMouse;
  77. else if (playerInput.GetDevice<Gamepad>() != null || playerInput.GetDevice<Joystick>() != null)
  78. m_ControlStyle = ControlStyle.GamepadJoystick;
  79. else
  80. Debug.LogError("Control scheme not recognized: " + playerInput.currentControlScheme);
  81. m_Mouse = default;
  82. m_MousePositionToWarpToAfterCursorUnlock = default;
  83. // Enable button for main menu depending on whether we use touch or not.
  84. // With kb&mouse and gamepad, not necessary but with touch, we have no "Cancel" control.
  85. menuButton.SetActive(m_ControlStyle == ControlStyle.Touch);
  86. // If we're using navigation-style input, start with UI control disengaged.
  87. if (m_ControlStyle == ControlStyle.GamepadJoystick)
  88. SetUIEngaged(false);
  89. RepaintInspector();
  90. }
  91. public void Update()
  92. {
  93. switch (m_State)
  94. {
  95. case State.InGame:
  96. {
  97. if (m_OpenMenuActionTriggered)
  98. {
  99. m_State = State.InMenu;
  100. // Bring up main menu.
  101. inGameUI.SetActive(false);
  102. mainMenuUI.SetActive(true);
  103. // Disable gameplay inputs.
  104. playerInput.DeactivateInput();
  105. // Select topmost button.
  106. EventSystem.current.SetSelectedGameObject(firstButtonInMainMenu);
  107. }
  108. var pointerIsOverUI = IsPointerOverUI();
  109. if (pointerIsOverUI)
  110. break;
  111. if (m_ResetCameraActionTriggered)
  112. transform.rotation = default;
  113. // When using a pointer-based control scheme, we engage camera look explicitly.
  114. if (m_ControlStyle != ControlStyle.GamepadJoystick && m_LookEngageAction.WasPressedThisFrame() && IsPointerInsideScreen())
  115. EngageCameraControl();
  116. // With gamepad/joystick, we can freely rotate the camera at any time.
  117. if (m_ControlStyle == ControlStyle.GamepadJoystick)
  118. ProcessCameraLook();
  119. if (m_FireActionTriggered)
  120. Fire();
  121. break;
  122. }
  123. case State.InGameControllingCamera:
  124. if (m_ResetCameraActionTriggered && !IsPointerOverUI())
  125. transform.rotation = default;
  126. if (m_FireActionTriggered && !IsPointerOverUI())
  127. Fire();
  128. // Rotate camera.
  129. ProcessCameraLook();
  130. // Keep track of distance we travel with the mouse while in mouse lock so
  131. // that when we unlock, we can jump to a position that feels "right".
  132. if (m_Mouse != null)
  133. m_MousePositionToWarpToAfterCursorUnlock = m_MousePositionToWarpToAfterCursorUnlock.Value + m_Mouse.delta.ReadValue();
  134. if (m_CancelAction.WasPressedThisFrame() || !m_LookEngageAction.IsPressed())
  135. DisengageCameraControl();
  136. break;
  137. case State.InMenu:
  138. if (m_CancelAction.WasPressedThisFrame())
  139. OnContinueClicked();
  140. break;
  141. }
  142. m_ResetCameraActionTriggered = default;
  143. m_OpenMenuActionTriggered = default;
  144. m_FireActionTriggered = default;
  145. }
  146. private void ProcessCameraLook()
  147. {
  148. var rotate = m_LookAction.ReadValue<Vector2>();
  149. if (!(rotate.sqrMagnitude > 0.01))
  150. return;
  151. // For gamepad and joystick, we rotate continuously based on stick actuation.
  152. float rotateScaleFactor;
  153. if (m_ControlStyle == ControlStyle.GamepadJoystick)
  154. rotateScaleFactor = m_GamepadLookSpeed * Time.deltaTime;
  155. else
  156. rotateScaleFactor = m_MouseLookSensitivity;
  157. m_Rotation.y += rotate.x * rotateScaleFactor;
  158. m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * rotateScaleFactor, -89, 89);
  159. transform.localEulerAngles = m_Rotation;
  160. }
  161. private void EngageCameraControl()
  162. {
  163. // With a mouse, it's annoying to always end up with the pointer centered in the middle of
  164. // the screen after we come out of a cursor lock. So, what we do is we simply remember where
  165. // the cursor was when we locked and then warp the mouse back to that position after the cursor
  166. // lock is released.
  167. m_Mouse = playerInput.GetDevice<Mouse>();
  168. m_MousePositionToWarpToAfterCursorUnlock = m_Mouse?.position.ReadValue();
  169. Cursor.lockState = CursorLockMode.Locked;
  170. m_State = State.InGameControllingCamera;
  171. RepaintInspector();
  172. }
  173. private void DisengageCameraControl()
  174. {
  175. Cursor.lockState = CursorLockMode.None;
  176. if (m_MousePositionToWarpToAfterCursorUnlock != null)
  177. m_Mouse?.WarpCursorPosition(m_MousePositionToWarpToAfterCursorUnlock.Value);
  178. m_State = State.InGame;
  179. RepaintInspector();
  180. }
  181. public void OnTopLeftClicked()
  182. {
  183. statusBarText.text = "'Top Left' button clicked";
  184. }
  185. public void OnBottomLeftClicked()
  186. {
  187. statusBarText.text = "'Bottom Left' button clicked";
  188. }
  189. public void OnTopRightClicked()
  190. {
  191. statusBarText.text = "'Top Right' button clicked";
  192. }
  193. public void OnBottomRightClicked()
  194. {
  195. statusBarText.text = "'Bottom Right' button clicked";
  196. }
  197. public void OnMenuClicked()
  198. {
  199. m_OpenMenuActionTriggered = true;
  200. }
  201. public void OnContinueClicked()
  202. {
  203. mainMenuUI.SetActive(false);
  204. inGameUI.SetActive(true);
  205. // Reenable gameplay inputs.
  206. playerInput.ActivateInput();
  207. m_State = State.InGame;
  208. RepaintInspector();
  209. }
  210. public void OnExitClicked()
  211. {
  212. #if UNITY_EDITOR
  213. EditorApplication.ExitPlaymode();
  214. #else
  215. Application.Quit();
  216. #endif
  217. }
  218. public void OnMenu(InputAction.CallbackContext context)
  219. {
  220. if (context.performed)
  221. m_OpenMenuActionTriggered = true;
  222. }
  223. public void OnResetCamera(InputAction.CallbackContext context)
  224. {
  225. if (context.performed)
  226. m_ResetCameraActionTriggered = true;
  227. }
  228. public void OnUIEngage(InputAction.CallbackContext context)
  229. {
  230. if (!context.performed)
  231. return;
  232. // From here, we could also do things such as showing UI that we only
  233. // have up while the UI is engaged. For example, the same approach as
  234. // here could be used to display a radial selection dials for items.
  235. SetUIEngaged(!m_UIEngaged);
  236. }
  237. private void SetUIEngaged(bool value)
  238. {
  239. if (value)
  240. {
  241. playerInput.actions.FindActionMap("UI").Enable();
  242. SetPlayerActionsEnabled(false);
  243. // Select the GO that was selected last time.
  244. if (m_LastNavigationSelection == null)
  245. m_LastNavigationSelection = firstNavigationSelection;
  246. EventSystem.current.SetSelectedGameObject(m_LastNavigationSelection);
  247. }
  248. else
  249. {
  250. m_LastNavigationSelection = EventSystem.current.currentSelectedGameObject; // If this happens to be null, we will automatically pick up firstNavigationSelection again.
  251. EventSystem.current.SetSelectedGameObject(null);
  252. playerInput.actions.FindActionMap("UI").Disable();
  253. SetPlayerActionsEnabled(true);
  254. }
  255. m_UIEngaged = value;
  256. RepaintInspector();
  257. }
  258. // Enable/disable every in-game action other than the UI toggle.
  259. private void SetPlayerActionsEnabled(bool value)
  260. {
  261. var actions = playerInput.actions.FindActionMap("Player");
  262. foreach (var action in actions)
  263. {
  264. if (action == m_UIEngageAction)
  265. continue;
  266. if (value)
  267. action.Enable();
  268. else
  269. action.Disable();
  270. }
  271. }
  272. // There's two different approaches taken here. The first OnFire() just does the same as the action
  273. // callbacks above and just sets some state to leave action responses to Update().
  274. // The second OnFire() puts the response logic directly inside the callback.
  275. #if false
  276. public void OnFire(InputAction.CallbackContext context)
  277. {
  278. if (context.performed)
  279. m_FireActionTriggered = true;
  280. }
  281. #else
  282. public void OnFire(InputAction.CallbackContext context)
  283. {
  284. // For this action, let's try something different. Let's say we want to trigger a response
  285. // right away every time the "fire" action triggers. Theoretically, this would allow us
  286. // to correctly respond even if there is multiple activations in a single frame. In practice,
  287. // this will realistically only happen with low framerates (and even then it can be questionable
  288. // whether we want to respond this way).
  289. if (!context.performed)
  290. return;
  291. var device = playerInput.GetDevice<Pointer>();
  292. if (device != null && IsRaycastHittingUIObject(device.position.ReadValue()))
  293. return;
  294. Fire();
  295. }
  296. // Can't use IsPointerOverGameObject() from within InputAction callbacks as the UI won't update
  297. // until after input processing is complete. So, need to explicitly raycast here.
  298. // NOTE: This is not something we'd want to do from a high-frequency action. If, for example, this
  299. // is called from an action bound to `<Mouse>/position`, there will be an immense amount of
  300. // raycasts performed per frame.
  301. private bool IsRaycastHittingUIObject(Vector2 position)
  302. {
  303. if (m_PointerData == null)
  304. m_PointerData = new PointerEventData(EventSystem.current);
  305. m_PointerData.position = position;
  306. EventSystem.current.RaycastAll(m_PointerData, m_RaycastResults);
  307. return m_RaycastResults.Count > 0;
  308. }
  309. private PointerEventData m_PointerData;
  310. private List<RaycastResult> m_RaycastResults = new List<RaycastResult>();
  311. #endif
  312. private bool IsPointerOverUI()
  313. {
  314. // If we're not controlling the UI with a pointer, we can early out of this.
  315. if (m_ControlStyle == ControlStyle.GamepadJoystick)
  316. return false;
  317. // Otherwise, check if the primary pointer is currently over a UI object.
  318. return EventSystem.current.IsPointerOverGameObject();
  319. }
  320. ////REVIEW: check this together with the focus PR; ideally, the code here should not be necessary
  321. private bool IsPointerInsideScreen()
  322. {
  323. var pointer = playerInput.GetDevice<Pointer>();
  324. if (pointer == null)
  325. return true;
  326. return Screen.safeArea.Contains(pointer.position.ReadValue());
  327. }
  328. private void Fire()
  329. {
  330. var transform = this.transform;
  331. var newProjectile = Instantiate(projectile);
  332. newProjectile.transform.position = transform.position + transform.forward * 0.6f;
  333. newProjectile.transform.rotation = transform.rotation;
  334. const int kSize = 1;
  335. newProjectile.transform.localScale *= kSize;
  336. newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(kSize, 3);
  337. newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
  338. newProjectile.GetComponent<MeshRenderer>().material.color =
  339. new Color(Random.value, Random.value, Random.value, 1.0f);
  340. }
  341. private void RepaintInspector()
  342. {
  343. // We have a custom inspector below that prints some debugging information for internal state.
  344. // When we change state, this will not result in an automatic repaint of the inspector as Unity
  345. // doesn't know about the change.
  346. //
  347. // We thus manually force a refresh. There's more elegant ways to do this but the easiest by
  348. // far is to just globally force a repaint of the entire editor window.
  349. #if UNITY_EDITOR
  350. InternalEditorUtility.RepaintAllViews();
  351. #endif
  352. }
  353. }
  354. #if UNITY_EDITOR
  355. [CustomEditor(typeof(UIvsGameInputHandler))]
  356. internal class UIvsGameInputHandlerEditor : Editor
  357. {
  358. public override void OnInspectorGUI()
  359. {
  360. base.OnInspectorGUI();
  361. using (new EditorGUI.DisabledScope(true))
  362. {
  363. EditorGUILayout.Space();
  364. EditorGUILayout.LabelField("Debug");
  365. EditorGUILayout.Space();
  366. using (new EditorGUI.IndentLevelScope())
  367. {
  368. var state = ((UIvsGameInputHandler)target).m_State;
  369. EditorGUILayout.LabelField("State", state.ToString());
  370. var style = ((UIvsGameInputHandler)target).m_ControlStyle;
  371. EditorGUILayout.LabelField("Controls", style.ToString());
  372. if (style == UIvsGameInputHandler.ControlStyle.GamepadJoystick)
  373. {
  374. var uiEngaged = ((UIvsGameInputHandler)target).m_UIEngaged;
  375. EditorGUILayout.LabelField("UI Engaged?", uiEngaged ? "Yes" : "No");
  376. }
  377. }
  378. }
  379. }
  380. }
  381. #endif