Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /Samples~ /UIvsGameInput /UIvsGameInputHandler.cs
| using System.Collections.Generic; | |
| using UnityEngine; | |
| using UnityEngine.EventSystems; | |
| using UnityEngine.InputSystem; | |
| using UnityEngine.UI; | |
| using UnityEditor; | |
| using UnityEditorInternal; | |
| public class UIvsGameInputHandler : MonoBehaviour | |
| { | |
| public Text statusBarText; | |
| public GameObject inGameUI; | |
| public GameObject mainMenuUI; | |
| public GameObject menuButton; | |
| public GameObject firstButtonInMainMenu; | |
| public GameObject firstNavigationSelection; | |
| [] | |
| public PlayerInput playerInput; | |
| public GameObject projectile; | |
| [] | |
| [] | |
| public float m_MouseLookSensitivity = 0.1f; | |
| [] | |
| 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() | |
| { | |
| EditorApplication.ExitPlaymode(); | |
| Application.Quit(); | |
| } | |
| 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. | |
| public void OnFire(InputAction.CallbackContext context) | |
| { | |
| if (context.performed) | |
| m_FireActionTriggered = true; | |
| } | |
| 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>(); | |
| 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. | |
| InternalEditorUtility.RepaintAllViews(); | |
| } | |
| } | |
| [] | |
| 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"); | |
| } | |
| } | |
| } | |
| } | |
| } | |