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