123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEngine.EventSystems;
- using UnityEngine.InputSystem;
- using UnityEngine.UI;
-
- #if UNITY_EDITOR
- using UnityEditor;
- using UnityEditorInternal;
- #endif
-
- public class UIvsGameInputHandler : MonoBehaviour
- {
- public Text statusBarText;
- public GameObject inGameUI;
- public GameObject mainMenuUI;
- public GameObject menuButton;
- public GameObject firstButtonInMainMenu;
- public GameObject firstNavigationSelection;
- [Space]
- public PlayerInput playerInput;
- public GameObject projectile;
-
- [Space]
- [Tooltip("Multiplier for Pointer.delta values when adding to rotation.")]
- public float m_MouseLookSensitivity = 0.1f;
- [Tooltip("Rotation per second with fully actuated Gamepad/joystick stick.")]
- public float m_GamepadLookSpeed = 10f;
-
- private bool m_OpenMenuActionTriggered;
- private bool m_ResetCameraActionTriggered;
- private bool m_FireActionTriggered;
- internal bool m_UIEngaged;
-
- private Vector2 m_Rotation;
- private InputAction m_LookEngageAction;
- private InputAction m_LookAction;
- private InputAction m_CancelAction;
- private InputAction m_UIEngageAction;
- private GameObject m_LastNavigationSelection;
-
- private Mouse m_Mouse;
- private Vector2? m_MousePositionToWarpToAfterCursorUnlock;
-
- internal enum State
- {
- InGame,
- InGameControllingCamera,
- InMenu,
- }
-
- internal State m_State;
-
- internal enum ControlStyle
- {
- None,
- KeyboardMouse,
- Touch,
- GamepadJoystick,
- }
-
- internal ControlStyle m_ControlStyle;
-
- public void OnEnable()
- {
- // By default, hide menu and show game UI.
- inGameUI.SetActive(true);
- mainMenuUI.SetActive(false);
- menuButton.SetActive(false);
-
- // Look up InputActions on the player so we don't have to do this over and over.
- m_LookEngageAction = playerInput.actions["LookEngage"];
- m_LookAction = playerInput.actions["Look"];
- m_CancelAction = playerInput.actions["UI/Cancel"];
- m_UIEngageAction = playerInput.actions["UIEngage"];
-
- m_State = State.InGame;
- }
-
- // This is called when PlayerInput updates the controls bound to its InputActions.
- public void OnControlsChanged()
- {
- // We could determine the types of controls we have from the names of the control schemes or their
- // contents. However, a way that is both easier and more robust is to simply look at the kind of
- // devices we have assigned to us. We do not support mixed models this way but this does correspond
- // to the limitations of the current control code.
-
- if (playerInput.GetDevice<Touchscreen>() != null) // Note that Touchscreen is also a Pointer so check this first.
- m_ControlStyle = ControlStyle.Touch;
- else if (playerInput.GetDevice<Pointer>() != null)
- m_ControlStyle = ControlStyle.KeyboardMouse;
- else if (playerInput.GetDevice<Gamepad>() != null || playerInput.GetDevice<Joystick>() != null)
- m_ControlStyle = ControlStyle.GamepadJoystick;
- else
- Debug.LogError("Control scheme not recognized: " + playerInput.currentControlScheme);
-
- m_Mouse = default;
- m_MousePositionToWarpToAfterCursorUnlock = default;
-
- // Enable button for main menu depending on whether we use touch or not.
- // With kb&mouse and gamepad, not necessary but with touch, we have no "Cancel" control.
- menuButton.SetActive(m_ControlStyle == ControlStyle.Touch);
-
- // If we're using navigation-style input, start with UI control disengaged.
- if (m_ControlStyle == ControlStyle.GamepadJoystick)
- SetUIEngaged(false);
-
- RepaintInspector();
- }
-
- public void Update()
- {
- switch (m_State)
- {
- case State.InGame:
- {
- if (m_OpenMenuActionTriggered)
- {
- m_State = State.InMenu;
-
- // Bring up main menu.
- inGameUI.SetActive(false);
- mainMenuUI.SetActive(true);
-
- // Disable gameplay inputs.
- playerInput.DeactivateInput();
-
- // Select topmost button.
- EventSystem.current.SetSelectedGameObject(firstButtonInMainMenu);
- }
-
- var pointerIsOverUI = IsPointerOverUI();
- if (pointerIsOverUI)
- break;
-
- if (m_ResetCameraActionTriggered)
- transform.rotation = default;
-
- // When using a pointer-based control scheme, we engage camera look explicitly.
- if (m_ControlStyle != ControlStyle.GamepadJoystick && m_LookEngageAction.WasPressedThisFrame() && IsPointerInsideScreen())
- EngageCameraControl();
-
- // With gamepad/joystick, we can freely rotate the camera at any time.
- if (m_ControlStyle == ControlStyle.GamepadJoystick)
- ProcessCameraLook();
-
- if (m_FireActionTriggered)
- Fire();
-
- break;
- }
-
- case State.InGameControllingCamera:
-
- if (m_ResetCameraActionTriggered && !IsPointerOverUI())
- transform.rotation = default;
-
- if (m_FireActionTriggered && !IsPointerOverUI())
- Fire();
-
- // Rotate camera.
- ProcessCameraLook();
-
- // Keep track of distance we travel with the mouse while in mouse lock so
- // that when we unlock, we can jump to a position that feels "right".
- if (m_Mouse != null)
- m_MousePositionToWarpToAfterCursorUnlock = m_MousePositionToWarpToAfterCursorUnlock.Value + m_Mouse.delta.ReadValue();
-
- if (m_CancelAction.WasPressedThisFrame() || !m_LookEngageAction.IsPressed())
- DisengageCameraControl();
-
- break;
-
- case State.InMenu:
-
- if (m_CancelAction.WasPressedThisFrame())
- OnContinueClicked();
-
- break;
- }
-
- m_ResetCameraActionTriggered = default;
- m_OpenMenuActionTriggered = default;
- m_FireActionTriggered = default;
- }
-
- private void ProcessCameraLook()
- {
- var rotate = m_LookAction.ReadValue<Vector2>();
- if (!(rotate.sqrMagnitude > 0.01))
- return;
-
- // For gamepad and joystick, we rotate continuously based on stick actuation.
- float rotateScaleFactor;
- if (m_ControlStyle == ControlStyle.GamepadJoystick)
- rotateScaleFactor = m_GamepadLookSpeed * Time.deltaTime;
- else
- rotateScaleFactor = m_MouseLookSensitivity;
-
- m_Rotation.y += rotate.x * rotateScaleFactor;
- m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * rotateScaleFactor, -89, 89);
- transform.localEulerAngles = m_Rotation;
- }
-
- private void EngageCameraControl()
- {
- // With a mouse, it's annoying to always end up with the pointer centered in the middle of
- // the screen after we come out of a cursor lock. So, what we do is we simply remember where
- // the cursor was when we locked and then warp the mouse back to that position after the cursor
- // lock is released.
- m_Mouse = playerInput.GetDevice<Mouse>();
- m_MousePositionToWarpToAfterCursorUnlock = m_Mouse?.position.ReadValue();
-
- Cursor.lockState = CursorLockMode.Locked;
-
- m_State = State.InGameControllingCamera;
-
- RepaintInspector();
- }
-
- private void DisengageCameraControl()
- {
- Cursor.lockState = CursorLockMode.None;
-
- if (m_MousePositionToWarpToAfterCursorUnlock != null)
- m_Mouse?.WarpCursorPosition(m_MousePositionToWarpToAfterCursorUnlock.Value);
-
- m_State = State.InGame;
-
- RepaintInspector();
- }
-
- public void OnTopLeftClicked()
- {
- statusBarText.text = "'Top Left' button clicked";
- }
-
- public void OnBottomLeftClicked()
- {
- statusBarText.text = "'Bottom Left' button clicked";
- }
-
- public void OnTopRightClicked()
- {
- statusBarText.text = "'Top Right' button clicked";
- }
-
- public void OnBottomRightClicked()
- {
- statusBarText.text = "'Bottom Right' button clicked";
- }
-
- public void OnMenuClicked()
- {
- m_OpenMenuActionTriggered = true;
- }
-
- public void OnContinueClicked()
- {
- mainMenuUI.SetActive(false);
- inGameUI.SetActive(true);
-
- // Reenable gameplay inputs.
- playerInput.ActivateInput();
-
- m_State = State.InGame;
-
- RepaintInspector();
- }
-
- public void OnExitClicked()
- {
- #if UNITY_EDITOR
- EditorApplication.ExitPlaymode();
- #else
- Application.Quit();
- #endif
- }
-
- public void OnMenu(InputAction.CallbackContext context)
- {
- if (context.performed)
- m_OpenMenuActionTriggered = true;
- }
-
- public void OnResetCamera(InputAction.CallbackContext context)
- {
- if (context.performed)
- m_ResetCameraActionTriggered = true;
- }
-
- public void OnUIEngage(InputAction.CallbackContext context)
- {
- if (!context.performed)
- return;
-
- // From here, we could also do things such as showing UI that we only
- // have up while the UI is engaged. For example, the same approach as
- // here could be used to display a radial selection dials for items.
-
- SetUIEngaged(!m_UIEngaged);
- }
-
- private void SetUIEngaged(bool value)
- {
- if (value)
- {
- playerInput.actions.FindActionMap("UI").Enable();
- SetPlayerActionsEnabled(false);
-
- // Select the GO that was selected last time.
- if (m_LastNavigationSelection == null)
- m_LastNavigationSelection = firstNavigationSelection;
- EventSystem.current.SetSelectedGameObject(m_LastNavigationSelection);
- }
- else
- {
- m_LastNavigationSelection = EventSystem.current.currentSelectedGameObject; // If this happens to be null, we will automatically pick up firstNavigationSelection again.
- EventSystem.current.SetSelectedGameObject(null);
-
- playerInput.actions.FindActionMap("UI").Disable();
- SetPlayerActionsEnabled(true);
- }
-
- m_UIEngaged = value;
-
- RepaintInspector();
- }
-
- // Enable/disable every in-game action other than the UI toggle.
- private void SetPlayerActionsEnabled(bool value)
- {
- var actions = playerInput.actions.FindActionMap("Player");
- foreach (var action in actions)
- {
- if (action == m_UIEngageAction)
- continue;
-
- if (value)
- action.Enable();
- else
- action.Disable();
- }
- }
-
- // There's two different approaches taken here. The first OnFire() just does the same as the action
- // callbacks above and just sets some state to leave action responses to Update().
- // The second OnFire() puts the response logic directly inside the callback.
-
- #if false
-
- public void OnFire(InputAction.CallbackContext context)
- {
- if (context.performed)
- m_FireActionTriggered = true;
- }
-
- #else
-
- public void OnFire(InputAction.CallbackContext context)
- {
- // For this action, let's try something different. Let's say we want to trigger a response
- // right away every time the "fire" action triggers. Theoretically, this would allow us
- // to correctly respond even if there is multiple activations in a single frame. In practice,
- // this will realistically only happen with low framerates (and even then it can be questionable
- // whether we want to respond this way).
-
- if (!context.performed)
- return;
-
- var device = playerInput.GetDevice<Pointer>();
- if (device != null && IsRaycastHittingUIObject(device.position.ReadValue()))
- return;
-
- Fire();
- }
-
- // Can't use IsPointerOverGameObject() from within InputAction callbacks as the UI won't update
- // until after input processing is complete. So, need to explicitly raycast here.
- // NOTE: This is not something we'd want to do from a high-frequency action. If, for example, this
- // is called from an action bound to `<Mouse>/position`, there will be an immense amount of
- // raycasts performed per frame.
- private bool IsRaycastHittingUIObject(Vector2 position)
- {
- if (m_PointerData == null)
- m_PointerData = new PointerEventData(EventSystem.current);
- m_PointerData.position = position;
- EventSystem.current.RaycastAll(m_PointerData, m_RaycastResults);
- return m_RaycastResults.Count > 0;
- }
-
- private PointerEventData m_PointerData;
- private List<RaycastResult> m_RaycastResults = new List<RaycastResult>();
-
- #endif
-
- private bool IsPointerOverUI()
- {
- // If we're not controlling the UI with a pointer, we can early out of this.
- if (m_ControlStyle == ControlStyle.GamepadJoystick)
- return false;
-
- // Otherwise, check if the primary pointer is currently over a UI object.
- return EventSystem.current.IsPointerOverGameObject();
- }
-
- ////REVIEW: check this together with the focus PR; ideally, the code here should not be necessary
- private bool IsPointerInsideScreen()
- {
- var pointer = playerInput.GetDevice<Pointer>();
- if (pointer == null)
- return true;
-
- return Screen.safeArea.Contains(pointer.position.ReadValue());
- }
-
- private void Fire()
- {
- var transform = this.transform;
- var newProjectile = Instantiate(projectile);
- newProjectile.transform.position = transform.position + transform.forward * 0.6f;
- newProjectile.transform.rotation = transform.rotation;
- const int kSize = 1;
- newProjectile.transform.localScale *= kSize;
- newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(kSize, 3);
- newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
- newProjectile.GetComponent<MeshRenderer>().material.color =
- new Color(Random.value, Random.value, Random.value, 1.0f);
- }
-
- private void RepaintInspector()
- {
- // We have a custom inspector below that prints some debugging information for internal state.
- // When we change state, this will not result in an automatic repaint of the inspector as Unity
- // doesn't know about the change.
- //
- // We thus manually force a refresh. There's more elegant ways to do this but the easiest by
- // far is to just globally force a repaint of the entire editor window.
-
- #if UNITY_EDITOR
- InternalEditorUtility.RepaintAllViews();
- #endif
- }
- }
-
- #if UNITY_EDITOR
- [CustomEditor(typeof(UIvsGameInputHandler))]
- internal class UIvsGameInputHandlerEditor : Editor
- {
- public override void OnInspectorGUI()
- {
- base.OnInspectorGUI();
-
- using (new EditorGUI.DisabledScope(true))
- {
- EditorGUILayout.Space();
- EditorGUILayout.LabelField("Debug");
- EditorGUILayout.Space();
-
- using (new EditorGUI.IndentLevelScope())
- {
- var state = ((UIvsGameInputHandler)target).m_State;
- EditorGUILayout.LabelField("State", state.ToString());
- var style = ((UIvsGameInputHandler)target).m_ControlStyle;
- EditorGUILayout.LabelField("Controls", style.ToString());
- if (style == UIvsGameInputHandler.ControlStyle.GamepadJoystick)
- {
- var uiEngaged = ((UIvsGameInputHandler)target).m_UIEngaged;
- EditorGUILayout.LabelField("UI Engaged?", uiEngaged ? "Yes" : "No");
- }
- }
- }
- }
- }
- #endif
|