A game about forced loneliness, made by TACStudios
1using System;
2using UnityEngine;
3using UnityEditor.ShortcutManagement;
4using UnityEngine.UIElements;
5
6namespace UnityEditor.Rendering.LookDev
7{
8 class CameraController : Manipulator
9 {
10 [Flags]
11 enum Direction
12 {
13 None = 0,
14 Up = 1 << 0,
15 Down = 1 << 1,
16 Left = 1 << 2,
17 Right = 1 << 3,
18 Forward = 1 << 4,
19 Backward = 1 << 5,
20 All = Up | Down | Left | Right | Forward | Backward
21 }
22 Direction m_DirectionKeyPressed = Direction.None;
23
24 float m_StartZoom = 0.0f;
25 float m_ZoomSpeed = 0.0f;
26
27 float m_TotalMotion = 0.0f;
28 Vector3 m_MotionDirection = new Vector3();
29
30 float m_FlySpeedNormalized = .5f;
31 float m_FlySpeed = 1f;
32 float m_FlySpeedAccelerated = 0f;
33 const float m_FlySpeedMin = .01f;
34 const float m_FlySpeedMax = 2f;
35 //[TODO: check if necessary to add hability to deactivate acceleration]
36 const float k_FlyAcceleration = 1.1f;
37 bool m_ShiftBoostedFly = false;
38 bool m_InFlyMotion;
39
40 bool m_IsDragging;
41 static TimeHelper s_Timer = new TimeHelper();
42
43 ViewTool m_BehaviorState;
44 ViewTool behaviorState
45 {
46 get { return m_BehaviorState; }
47 set
48 {
49 if (value != m_BehaviorState && m_BehaviorState == ViewTool.FPS)
50 {
51 isDragging = false;
52 inFlyMotion = false;
53 m_DirectionKeyPressed = Direction.None;
54 }
55 m_BehaviorState = value;
56 }
57 }
58
59 protected CameraState m_CameraState;
60 DisplayWindow m_Window;
61 protected Action m_Focused;
62
63 Rect screen => target.contentRect;
64
65 bool inFlyMotion
66 {
67 get => m_InFlyMotion;
68 set
69 {
70 if (value ^ m_InFlyMotion)
71 {
72 if (value)
73 {
74 s_Timer.Begin();
75 EditorApplication.update += UpdateFPSMotion;
76 }
77 else
78 {
79 m_FlySpeedAccelerated = 0f;
80 m_MotionDirection = Vector3.zero;
81 m_ShiftBoostedFly = false;
82 EditorApplication.update -= UpdateFPSMotion;
83 }
84 m_InFlyMotion = value;
85 }
86 }
87 }
88
89 float flySpeedNormalized
90 {
91 get => m_FlySpeedNormalized;
92 set
93 {
94 m_FlySpeedNormalized = Mathf.Clamp01(value);
95 float speed = Mathf.Lerp(m_FlySpeedMin, m_FlySpeedMax, m_FlySpeedNormalized);
96 // Round to nearest decimal: 2 decimal points when between [0.01, 0.1]; 1 decimal point when between [0.1, 10]; integral between [10, 99]
97 speed = (float)(System.Math.Round((double)speed, speed < 0.1f ? 2 : speed < 10f ? 1 : 0));
98 m_FlySpeed = Mathf.Clamp(speed, m_FlySpeedMin, m_FlySpeedMax);
99 }
100 }
101 float flySpeed
102 {
103 get => m_FlySpeed;
104 set => flySpeedNormalized = Mathf.InverseLerp(m_FlySpeedMin, m_FlySpeedMax, value);
105 }
106
107 virtual protected bool isDragging
108 {
109 get => m_IsDragging;
110 set
111 {
112 //As in scene view, stop dragging as first button is release in case of multiple button down
113 if (value ^ m_IsDragging)
114 {
115 if (value)
116 {
117 target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
118 target.CaptureMouse();
119 EditorGUIUtility.SetWantsMouseJumping(1); //through screen edges
120 }
121 else
122 {
123 EditorGUIUtility.SetWantsMouseJumping(0);
124 target.ReleaseMouse();
125 target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
126 }
127 m_IsDragging = value;
128 }
129 }
130 }
131
132 public CameraController(DisplayWindow window, Action focused)
133 {
134 m_Window = window;
135 m_Focused = focused;
136 }
137
138 public void UpdateCameraState(Context context, ViewIndex index)
139 {
140 m_CameraState = context.GetViewContent(index).camera;
141 }
142
143 private void ResetCameraControl()
144 {
145 isDragging = false;
146 inFlyMotion = false;
147 behaviorState = ViewTool.None;
148 }
149
150 protected virtual void OnScrollWheel(WheelEvent evt)
151 {
152 // See UnityEditor.SceneViewMotion.HandleScrollWheel
153 switch (behaviorState)
154 {
155 case ViewTool.FPS: OnChangeFPSCameraSpeed(evt); break;
156 default: OnZoom(evt); break;
157 }
158 }
159
160 void OnMouseDrag(MouseMoveEvent evt)
161 {
162 switch (behaviorState)
163 {
164 case ViewTool.Orbit: OnMouseDragOrbit(evt); break;
165 case ViewTool.FPS: OnMouseDragFPS(evt); break;
166 case ViewTool.Pan: OnMouseDragPan(evt); break;
167 case ViewTool.Zoom: OnMouseDragZoom(evt); break;
168 default: break;
169 }
170 }
171
172 void OnKeyDown(KeyDownEvent evt)
173 {
174 OnKeyUpOrDownFPS(evt);
175 OnKeyDownReset(evt);
176 }
177
178 void OnChangeFPSCameraSpeed(WheelEvent evt)
179 {
180 float scrollWheelDelta = evt.delta.y;
181 flySpeedNormalized -= scrollWheelDelta * .01f;
182 string cameraSpeedDisplayValue = flySpeed.ToString(flySpeed < 0.1f ? "F2" : flySpeed < 10f ? "F1" : "F0");
183 if (flySpeed < 0.1f)
184 cameraSpeedDisplayValue = cameraSpeedDisplayValue.TrimStart(new Char[] { '0' });
185 GUIContent cameraSpeedContent = EditorGUIUtility.TrTempContent(
186 $"{cameraSpeedDisplayValue}x");
187 m_Window.ShowNotification(cameraSpeedContent, .5f);
188 evt.StopPropagation();
189 }
190
191 void OnZoom(WheelEvent evt)
192 {
193 const float deltaCutoff = .3f;
194 const float minZoom = .003f;
195 float scrollWheelDelta = evt.delta.y;
196 float relativeDelta = m_CameraState.viewSize * scrollWheelDelta * .015f;
197 if (relativeDelta > 0 && relativeDelta < deltaCutoff)
198 relativeDelta = deltaCutoff;
199 else if (relativeDelta <= 0 && relativeDelta > -deltaCutoff)
200 relativeDelta = -deltaCutoff;
201 m_CameraState.viewSize += relativeDelta;
202 if (m_CameraState.viewSize < minZoom)
203 m_CameraState.viewSize = minZoom;
204 evt.StopPropagation();
205 }
206
207 void OnMouseDragOrbit(MouseMoveEvent evt)
208 {
209 Quaternion rotation = m_CameraState.rotation;
210 rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
211 rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
212 m_CameraState.rotation = rotation;
213 evt.StopPropagation();
214 }
215
216 void OnMouseDragFPS(MouseMoveEvent evt)
217 {
218 Vector3 camPos = m_CameraState.pivot - m_CameraState.rotation * Vector3.forward * m_CameraState.distanceFromPivot;
219 Quaternion rotation = m_CameraState.rotation;
220 rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
221 rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
222 m_CameraState.rotation = rotation;
223 m_CameraState.pivot = camPos + rotation * Vector3.forward * m_CameraState.distanceFromPivot;
224 evt.StopPropagation();
225 }
226
227 void OnMouseDragPan(MouseMoveEvent evt)
228 {
229 //[TODO: fix WorldToScreenPoint and ScreenToWorldPoint
230 var screenPos = m_CameraState.QuickProjectPivotInScreen(screen);
231 screenPos += new Vector3(evt.mouseDelta.x, -evt.mouseDelta.y, 0);
232 //Vector3 newWorldPos = m_CameraState.ScreenToWorldPoint(screen, screenPos);
233 Vector3 newWorldPos = m_CameraState.QuickReprojectionWithFixedFOVOnPivotPlane(screen, screenPos);
234 Vector3 worldDelta = newWorldPos - m_CameraState.pivot;
235 worldDelta *= EditorGUIUtility.pixelsPerPoint;
236 if (evt.shiftKey)
237 worldDelta *= 4;
238 m_CameraState.pivot += worldDelta;
239 evt.StopPropagation();
240 }
241
242 void OnMouseDragZoom(MouseMoveEvent evt)
243 {
244 float zoomDelta = HandleUtility.niceMouseDeltaZoom * (evt.shiftKey ? 9 : 3);
245 m_TotalMotion += zoomDelta;
246 if (m_TotalMotion < 0)
247 m_CameraState.viewSize = m_StartZoom * (1 + m_TotalMotion * .001f);
248 else
249 m_CameraState.viewSize = m_CameraState.viewSize + zoomDelta * m_ZoomSpeed * .003f;
250 evt.StopPropagation();
251 }
252
253 void OnKeyDownReset(KeyDownEvent evt)
254 {
255 if (evt.keyCode == KeyCode.Escape)
256 ResetCameraControl();
257 evt.StopPropagation();
258 }
259
260 void OnKeyUpOrDownFPS<T>(KeyboardEventBase<T> evt)
261 where T : KeyboardEventBase<T>, new()
262 {
263 if (behaviorState != ViewTool.FPS)
264 return;
265
266 //Note: Keydown is called in loop but between first occurence of the
267 // loop and laters, there is a small pause. To deal with this, we
268 // need to register the UpdateMovement function to the Editor update
269 KeyCombination combination;
270 if (GetKeyCombinationByID("3D Viewport/Fly Mode Forward", out combination) && combination.Match(evt))
271 RegisterMotionChange(Direction.Forward, evt);
272 if (GetKeyCombinationByID("3D Viewport/Fly Mode Backward", out combination) && combination.Match(evt))
273 RegisterMotionChange(Direction.Backward, evt);
274 if (GetKeyCombinationByID("3D Viewport/Fly Mode Left", out combination) && combination.Match(evt))
275 RegisterMotionChange(Direction.Left, evt);
276 if (GetKeyCombinationByID("3D Viewport/Fly Mode Right", out combination) && combination.Match(evt))
277 RegisterMotionChange(Direction.Right, evt);
278 if (GetKeyCombinationByID("3D Viewport/Fly Mode Up", out combination) && combination.Match(evt))
279 RegisterMotionChange(Direction.Up, evt);
280 if (GetKeyCombinationByID("3D Viewport/Fly Mode Down", out combination) && combination.Match(evt))
281 RegisterMotionChange(Direction.Down, evt);
282 }
283
284 void RegisterMotionChange<T>(Direction direction, KeyboardEventBase<T> evt)
285 where T : KeyboardEventBase<T>, new()
286 {
287 m_ShiftBoostedFly = evt.shiftKey;
288 Direction formerDirection = m_DirectionKeyPressed;
289 bool keyUp = evt is KeyUpEvent;
290 bool keyDown = evt is KeyDownEvent;
291 if (keyDown)
292 m_DirectionKeyPressed |= direction;
293 else if (keyUp)
294 m_DirectionKeyPressed &= (Direction.All & ~direction);
295 if (formerDirection != m_DirectionKeyPressed)
296 {
297 m_MotionDirection = new Vector3(
298 ((m_DirectionKeyPressed & Direction.Right) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Left) > 0 ? 1 : 0),
299 ((m_DirectionKeyPressed & Direction.Up) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Down) > 0 ? 1 : 0),
300 ((m_DirectionKeyPressed & Direction.Forward) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Backward) > 0 ? 1 : 0));
301
302 inFlyMotion = m_DirectionKeyPressed != Direction.None;
303 }
304 evt.StopPropagation();
305 }
306
307 Vector3 GetMotionDirection()
308 {
309 var deltaTime = s_Timer.Update();
310 Vector3 result;
311 float speed = (m_ShiftBoostedFly ? 5 * flySpeed : flySpeed);
312 if (m_FlySpeedAccelerated == 0)
313 m_FlySpeedAccelerated = 9;
314 else
315 m_FlySpeedAccelerated *= Mathf.Pow(k_FlyAcceleration, deltaTime);
316 result = m_MotionDirection.normalized * m_FlySpeedAccelerated * speed * deltaTime;
317 return result;
318 }
319
320 void UpdateFPSMotion()
321 {
322 m_CameraState.pivot += m_CameraState.rotation * GetMotionDirection();
323 m_Window.Repaint(); //this prevent hich on key down as in CameraFlyModeContext.cs
324 }
325
326 bool GetKeyCombinationByID(string ID, out KeyCombination combination)
327 {
328 var sequence = ShortcutManager.instance.GetShortcutBinding(ID).keyCombinationSequence.GetEnumerator();
329 if (sequence.MoveNext()) //have a first entry
330 {
331 combination = new KeyCombination(sequence.Current);
332 return true;
333 }
334 else
335 {
336 combination = default;
337 return false;
338 }
339 }
340
341 ViewTool GetBehaviorTool<T>(MouseEventBase<T> evt, bool onMac) where T : MouseEventBase<T>, new()
342 {
343 if (evt.button == 2)
344 return ViewTool.Pan;
345 else if (evt.button == 0 && evt.ctrlKey && onMac || evt.button == 1 && evt.altKey)
346 return ViewTool.Zoom;
347 else if (evt.button == 0)
348 return ViewTool.Orbit;
349 else if (evt.button == 1 && !evt.altKey)
350 return ViewTool.FPS;
351 return ViewTool.None;
352 }
353
354 void OnMouseUp(MouseUpEvent evt)
355 {
356 bool onMac = Application.platform == RuntimePlatform.OSXEditor;
357 var state = GetBehaviorTool(evt, onMac);
358
359 if (state == behaviorState)
360 ResetCameraControl();
361 evt.StopPropagation();
362 }
363
364 void OnMouseDown(MouseDownEvent evt)
365 {
366 bool onMac = Application.platform == RuntimePlatform.OSXEditor;
367 behaviorState = GetBehaviorTool(evt, onMac);
368
369 if (behaviorState == ViewTool.Zoom)
370 {
371 m_StartZoom = m_CameraState.viewSize;
372 m_ZoomSpeed = Mathf.Max(Mathf.Abs(m_StartZoom), .3f);
373 m_TotalMotion = 0;
374 }
375
376 // see also SceneView.HandleClickAndDragToFocus()
377 if (evt.button == 1 && onMac)
378 m_Window.Focus();
379
380 target.Focus(); //required for keyboard event
381 isDragging = true;
382 evt.StopPropagation();
383
384 m_Focused?.Invoke();
385 }
386
387 protected override void RegisterCallbacksOnTarget()
388 {
389 target.focusable = true; //prerequisite for being focusable and recerive keydown events
390 target.RegisterCallback<MouseUpEvent>(OnMouseUp);
391 target.RegisterCallback<MouseDownEvent>(OnMouseDown);
392 target.RegisterCallback<WheelEvent>(OnScrollWheel);
393 target.RegisterCallback<KeyDownEvent>(OnKeyDown);
394 target.RegisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
395 }
396
397 protected override void UnregisterCallbacksFromTarget()
398 {
399 target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
400 target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
401 target.UnregisterCallback<WheelEvent>(OnScrollWheel);
402 target.UnregisterCallback<KeyDownEvent>(OnKeyDown);
403 target.UnregisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
404 }
405
406 struct KeyCombination
407 {
408 KeyCode key;
409 EventModifiers modifier;
410 public bool shiftOnLastMatch;
411
412 public KeyCombination(UnityEditor.ShortcutManagement.KeyCombination shortcutCombination)
413 {
414 key = shortcutCombination.keyCode;
415 modifier = EventModifiers.None;
416 if ((shortcutCombination.modifiers & ShortcutModifiers.Shift) != 0)
417 modifier |= EventModifiers.Shift;
418 if ((shortcutCombination.modifiers & ShortcutModifiers.Alt) != 0)
419 modifier |= EventModifiers.Alt;
420 if ((shortcutCombination.modifiers & ShortcutModifiers.Action) != 0)
421 {
422 if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
423 modifier |= EventModifiers.Command;
424 else
425 modifier |= EventModifiers.Control;
426 }
427 shiftOnLastMatch = false;
428 }
429
430 //atLeastModifier allow case were A is required but event provide shift+A
431 public bool Match(IKeyboardEvent evt, bool atLeastForModifier = true)
432 {
433 shiftOnLastMatch = evt.shiftKey;
434 if (atLeastForModifier)
435 return key == evt.keyCode && modifier == (evt.modifiers & modifier);
436 else
437 return key == evt.keyCode && modifier == evt.modifiers;
438 }
439 }
440
441 struct TimeHelper
442 {
443 long lastTime;
444
445 public void Begin() => lastTime = System.DateTime.Now.Ticks;
446
447 public float Update()
448 {
449 float deltaTime = (System.DateTime.Now.Ticks - lastTime) / 10000000.0f;
450 lastTime = System.DateTime.Now.Ticks;
451 return deltaTime;
452 }
453 }
454 }
455
456 class SwitchableCameraController : CameraController
457 {
458 CameraState m_FirstView;
459 CameraState m_SecondView;
460 ViewIndex m_CurrentViewIndex;
461
462 bool switchedDrag = false;
463 bool switchedWheel = false;
464
465 public SwitchableCameraController(DisplayWindow window, Action<ViewIndex> focused)
466 : base(window, null)
467 {
468 m_CurrentViewIndex = ViewIndex.First;
469
470 m_Focused = () => focused?.Invoke(m_CurrentViewIndex);
471 }
472
473 public void UpdateCameraState(Context context)
474 {
475 m_FirstView = context.GetViewContent(ViewIndex.First).camera;
476 m_SecondView = context.GetViewContent(ViewIndex.Second).camera;
477
478 m_CameraState = m_CurrentViewIndex == ViewIndex.First ? m_FirstView : m_SecondView;
479 }
480
481 void SwitchTo(ViewIndex index)
482 {
483 CameraState stateToSwitch;
484 switch (index)
485 {
486 case ViewIndex.First:
487 stateToSwitch = m_FirstView;
488 break;
489 case ViewIndex.Second:
490 stateToSwitch = m_SecondView;
491 break;
492 default:
493 throw new ArgumentException("Unknown ViewIndex");
494 }
495
496 if (stateToSwitch != m_CameraState)
497 m_CameraState = stateToSwitch;
498
499 m_CurrentViewIndex = index;
500 }
501
502 public void SwitchUntilNextEndOfDrag()
503 {
504 switchedDrag = true;
505 SwitchTo(ViewIndex.Second);
506 }
507
508 override protected bool isDragging
509 {
510 get => base.isDragging;
511 set
512 {
513 bool switchBack = false;
514 if (switchedDrag && base.isDragging && !value)
515 switchBack = true;
516 base.isDragging = value;
517 if (switchBack)
518 SwitchTo(ViewIndex.First);
519 }
520 }
521
522 public void SwitchUntilNextWheelEvent()
523 {
524 switchedWheel = true;
525 SwitchTo(ViewIndex.Second);
526 }
527
528 protected override void OnScrollWheel(WheelEvent evt)
529 {
530 base.OnScrollWheel(evt);
531 if (switchedWheel)
532 SwitchTo(ViewIndex.First);
533 }
534 }
535}