forked from Unity-Technologies/InputSystem
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathUIvsGameInputHandler.cs
475 lines (381 loc) · 15.8 KB
/
UIvsGameInputHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
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.WasPressedThisDynamicUpdate() && 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.WasPressedThisDynamicUpdate() || !m_LookEngageAction.IsPressed())
DisengageCameraControl();
break;
case State.InMenu:
if (m_CancelAction.WasPressedThisDynamicUpdate())
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