test_ModularViewportCameraController.cpp 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AtomToolsFramework/Viewport/ModularViewportCameraController.h>
  9. #include <AzCore/Settings/SettingsRegistryImpl.h>
  10. #include <AzFramework/Viewport/ViewportControllerList.h>
  11. #include <AzToolsFramework/Input/QtEventToAzInputMapper.h>
  12. #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
  13. #include <AzToolsFramework/UnitTest/Mocks/MockViewportInteractionRequests.h>
  14. #include <EditorViewportWidget.h>
  15. #include <Mocks/MockWindowRequests.h>
  16. namespace UnitTest
  17. {
  18. const QSize WidgetSize = QSize(1920, 1080);
  19. using AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
  20. static constexpr float DeltaTime = 1.0f / 60.0f;
  21. class ViewportMouseCursorRequestImpl : public AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Handler
  22. {
  23. public:
  24. void Connect(const AzFramework::ViewportId viewportId, AzToolsFramework::QtEventToAzInputMapper* inputChannelMapper)
  25. {
  26. AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Handler::BusConnect(viewportId);
  27. m_inputChannelMapper = inputChannelMapper;
  28. }
  29. void Disconnect()
  30. {
  31. AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Handler::BusDisconnect();
  32. }
  33. // ViewportMouseCursorRequestBus overrides ...
  34. void BeginCursorCapture() override;
  35. void EndCursorCapture() override;
  36. void SetCursorMode(AzToolsFramework::CursorInputMode mode) override;
  37. bool IsMouseOver() const override;
  38. void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override;
  39. void ClearOverrideCursor() override;
  40. AZStd::optional<AzFramework::ScreenPoint> MousePosition() const override;
  41. AzFramework::ScreenPoint m_mousePosition = AzFramework::ScreenPoint{};
  42. private:
  43. AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr;
  44. };
  45. void ViewportMouseCursorRequestImpl::BeginCursorCapture()
  46. {
  47. m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeCaptured);
  48. }
  49. void ViewportMouseCursorRequestImpl::EndCursorCapture()
  50. {
  51. m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone);
  52. }
  53. void ViewportMouseCursorRequestImpl::SetCursorMode(AzToolsFramework::CursorInputMode mode)
  54. {
  55. m_inputChannelMapper->SetCursorMode(mode);
  56. }
  57. bool ViewportMouseCursorRequestImpl::IsMouseOver() const
  58. {
  59. return true;
  60. }
  61. void ViewportMouseCursorRequestImpl::SetOverrideCursor(
  62. [[maybe_unused]] AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride)
  63. {
  64. // noop
  65. }
  66. void ViewportMouseCursorRequestImpl::ClearOverrideCursor()
  67. {
  68. // noop
  69. }
  70. AZStd::optional<AzFramework::ScreenPoint> ViewportMouseCursorRequestImpl::MousePosition() const
  71. {
  72. return m_mousePosition;
  73. }
  74. class ModularViewportCameraControllerFixture : public LeakDetectionFixture
  75. {
  76. public:
  77. static inline constexpr AzFramework::ViewportId TestViewportId = 1234;
  78. void SetUp() override
  79. {
  80. LeakDetectionFixture::SetUp();
  81. m_rootWidget = new QWidget();
  82. // set root widget to the the active window to ensure focus in/out events are fired
  83. QApplication::setActiveWindow(m_rootWidget);
  84. m_rootWidget->setFixedSize(WidgetSize);
  85. m_rootWidget->move(0, 0); // explicitly set the widget to be in the upper left corner
  86. m_otherWidget = new QWidget(m_rootWidget);
  87. m_otherWidget->setFixedSize(WidgetSize / 2);
  88. m_otherWidget->move(WidgetSize.width(), 0); // move widget to right of root widget
  89. m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
  90. m_controllerList->RegisterViewportContext(TestViewportId);
  91. m_inputChannelMapper = AZStd::make_unique<AzToolsFramework::QtEventToAzInputMapper>(m_rootWidget, TestViewportId);
  92. m_settingsRegistry = AZStd::make_unique<AZ::SettingsRegistryImpl>();
  93. AZ::SettingsRegistry::Register(m_settingsRegistry.get());
  94. }
  95. void TearDown() override
  96. {
  97. AZ::SettingsRegistry::Unregister(m_settingsRegistry.get());
  98. m_settingsRegistry.reset();
  99. m_inputChannelMapper.reset();
  100. m_controllerList->UnregisterViewportContext(TestViewportId);
  101. m_controllerList.reset();
  102. QApplication::setActiveWindow(nullptr);
  103. delete m_rootWidget;
  104. LeakDetectionFixture::TearDown();
  105. }
  106. void PrepareCollaborators()
  107. {
  108. AzFramework::NativeWindowHandle nativeWindowHandle = nullptr;
  109. // listen for events signaled from QtEventToAzInputMapper and forward to the controller list
  110. QObject::connect(
  111. m_inputChannelMapper.get(),
  112. &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated,
  113. m_rootWidget,
  114. [this, nativeWindowHandle](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event)
  115. {
  116. m_controllerList->HandleInputChannelEvent(
  117. AzFramework::ViewportControllerInputEvent{ TestViewportId, nativeWindowHandle, *inputChannel });
  118. });
  119. m_mockWindowRequests.Connect(nativeWindowHandle);
  120. using ::testing::Return;
  121. // note: WindowRequests is used internally by ModularViewportCameraController, this ensures it returns the viewport size we want
  122. ON_CALL(m_mockWindowRequests, GetClientAreaSize())
  123. .WillByDefault(Return(AzFramework::WindowSize(WidgetSize.width(), WidgetSize.height())));
  124. ON_CALL(m_mockWindowRequests, GetRenderResolution())
  125. .WillByDefault(Return(AzFramework::WindowSize(WidgetSize.width(), WidgetSize.height())));
  126. m_mockViewportInteractionRequests.Connect(TestViewportId);
  127. // respond to begin/end cursor capture events
  128. m_viewportMouseCursorRequests.Connect(TestViewportId, m_inputChannelMapper.get());
  129. // create editor modular camera
  130. m_editorModularViewportCameraComposer = AZStd::make_unique<SandboxEditor::EditorModularViewportCameraComposer>(TestViewportId);
  131. auto controller = m_editorModularViewportCameraComposer->CreateModularViewportCameraController();
  132. // set some overrides for the test
  133. controller->SetCameraViewportContextBuilderCallback(
  134. [this](AZStd::unique_ptr<AtomToolsFramework::ModularCameraViewportContext>& cameraViewportContext)
  135. {
  136. cameraViewportContext = AZStd::make_unique<AtomToolsFramework::PlaceholderModularCameraViewportContextImpl>();
  137. m_cameraViewportContextView = cameraViewportContext.get();
  138. });
  139. // disable smoothing in the test
  140. controller->SetCameraPropsBuilderCallback(
  141. [](AzFramework::CameraProps& cameraProps)
  142. {
  143. // note: rotateSmoothness is also used for roll (not related to camera input directly)
  144. cameraProps.m_rotateSmoothnessFn = []
  145. {
  146. return 5.0f;
  147. };
  148. cameraProps.m_translateSmoothnessFn = []
  149. {
  150. return 5.0f;
  151. };
  152. cameraProps.m_rotateSmoothingEnabledFn = []
  153. {
  154. return false;
  155. };
  156. cameraProps.m_translateSmoothingEnabledFn = []
  157. {
  158. return false;
  159. };
  160. });
  161. m_controllerList->Add(controller);
  162. }
  163. void HaltCollaborators()
  164. {
  165. m_editorModularViewportCameraComposer.reset();
  166. m_mockViewportInteractionRequests.Disconnect();
  167. m_mockWindowRequests.Disconnect();
  168. m_viewportMouseCursorRequests.Disconnect();
  169. m_cameraViewportContextView = nullptr;
  170. }
  171. void RepeatDiagonalMouseMovements(const AZStd::function<float()>& deltaTimeFn)
  172. {
  173. // move to the center of the screen
  174. const auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
  175. MouseMove(m_rootWidget, start, QPoint(0, 0));
  176. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
  177. // move mouse diagonally to top right, then to bottom left and back repeatedly
  178. auto current = start;
  179. auto halfDelta = QPoint(200, -200);
  180. const int iterationsPerDiagonal = 50;
  181. for (int diagonals = 0; diagonals < 80; ++diagonals)
  182. {
  183. for (int i = 0; i < iterationsPerDiagonal; ++i)
  184. {
  185. MousePressAndMove(m_rootWidget, current, halfDelta / iterationsPerDiagonal, Qt::MouseButton::RightButton);
  186. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
  187. current += halfDelta / iterationsPerDiagonal;
  188. }
  189. if (diagonals % 2 == 0)
  190. {
  191. halfDelta.setX(halfDelta.x() * -1);
  192. halfDelta.setY(halfDelta.y() * -1);
  193. }
  194. }
  195. QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, current);
  196. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
  197. }
  198. ::testing::NiceMock<MockViewportInteractionRequests> m_mockViewportInteractionRequests;
  199. QWidget* m_rootWidget = nullptr;
  200. QWidget* m_otherWidget = nullptr;;
  201. AzFramework::ViewportControllerListPtr m_controllerList;
  202. AZStd::unique_ptr<AzToolsFramework::QtEventToAzInputMapper> m_inputChannelMapper;
  203. ::testing::NiceMock<MockWindowRequests> m_mockWindowRequests;
  204. ViewportMouseCursorRequestImpl m_viewportMouseCursorRequests;
  205. AtomToolsFramework::ModularCameraViewportContext* m_cameraViewportContextView = nullptr;
  206. AZStd::unique_ptr<AZ::SettingsRegistryInterface> m_settingsRegistry;
  207. AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
  208. };
  209. TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime)
  210. {
  211. SandboxEditor::SetCameraCaptureCursorForLook(false);
  212. // Given
  213. PrepareCollaborators();
  214. // When
  215. RepeatDiagonalMouseMovements(
  216. [t = 0.0f]() mutable
  217. {
  218. // vary between 30 and 50 fps (40 +/- 10)
  219. const float fps = 40.0f + (10.0f * AZStd::sin(t));
  220. t += AZ::DegToRad(5.0f);
  221. return 1.0f / fps;
  222. });
  223. // Then
  224. // ensure the camera rotation is the identity (no significant drift has occurred as we moved the mouse)
  225. const AZ::Transform cameraRotation = m_cameraViewportContextView->GetCameraTransform();
  226. EXPECT_THAT(cameraRotation.GetRotation(), IsClose(AZ::Quaternion::CreateIdentity()));
  227. // Clean-up
  228. HaltCollaborators();
  229. }
  230. class ModularViewportCameraControllerDeltaTimeParamFixture
  231. : public ModularViewportCameraControllerFixture
  232. , public ::testing::WithParamInterface<float> // delta time
  233. {
  234. };
  235. TEST_P(
  236. ModularViewportCameraControllerDeltaTimeParamFixture,
  237. MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithFixedDeltaTime)
  238. {
  239. SandboxEditor::SetCameraCaptureCursorForLook(false);
  240. // Given
  241. PrepareCollaborators();
  242. // When
  243. RepeatDiagonalMouseMovements(
  244. [this]
  245. {
  246. // note that earlier versions of GoogleMock required 'this' capture in order to call GetParam
  247. // GetParam is not a static member, but only works on static class data, which can cause some compilers to complain
  248. // about 'this' being captured but not used, and other situations to complain about this NOT being captured but used,
  249. // depending on which version of googlemock we actually use. To try to satisfy all versions, we will explicitly capture 'this'
  250. // AND explicily use it despite it not being syntactically necessary (ie, 'this->' being impiled by GetParam()).
  251. return this->GetParam();
  252. });
  253. // Then
  254. // ensure the camera rotation is the identity (no significant drift has occurred as we moved the mouse)
  255. const AZ::Transform cameraRotation = m_cameraViewportContextView->GetCameraTransform();
  256. EXPECT_THAT(cameraRotation.GetRotation(), IsClose(AZ::Quaternion::CreateIdentity()));
  257. // Clean-up
  258. HaltCollaborators();
  259. }
  260. INSTANTIATE_TEST_CASE_P(
  261. All, ModularViewportCameraControllerDeltaTimeParamFixture, testing::Values(1.0f / 60.0f, 1.0f / 50.0f, 1.0f / 30.0f));
  262. TEST_F(ModularViewportCameraControllerFixture, MouseMovementOrientatesCameraWhenCursorIsCaptured)
  263. {
  264. // Given
  265. PrepareCollaborators();
  266. // ensure cursor is captured
  267. SandboxEditor::SetCameraCaptureCursorForLook(true);
  268. // When
  269. // move to the center of the screen
  270. auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
  271. MouseMove(m_rootWidget, start, QPoint(0, 0));
  272. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  273. const auto mouseDelta = QPoint(5, 0);
  274. // initial movement to begin the camera behavior
  275. MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
  276. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  277. // move the cursor right
  278. for (int i = 0; i < 50; ++i)
  279. {
  280. MousePressAndMove(m_rootWidget, start + mouseDelta, mouseDelta, Qt::MouseButton::RightButton);
  281. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  282. }
  283. // move the cursor left (do an extra iteration moving left to account for the initial dead-zone)
  284. for (int i = 0; i < 51; ++i)
  285. {
  286. MousePressAndMove(m_rootWidget, start + mouseDelta, -mouseDelta, Qt::MouseButton::RightButton);
  287. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  288. }
  289. QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, start + mouseDelta);
  290. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  291. // Then
  292. // retrieve the amount of yaw rotation
  293. const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation();
  294. const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromQuaternion(cameraRotation));
  295. // camera should be back at the center (no yaw)
  296. using ::testing::FloatNear;
  297. EXPECT_THAT(eulerAngles.GetZ(), FloatNear(0.0f, 0.001f));
  298. // Clean-up
  299. HaltCollaborators();
  300. }
  301. TEST_F(ModularViewportCameraControllerFixture, CameraDoesNotContinueToRotateGivenNoInputWhenCaptured)
  302. {
  303. // Given
  304. PrepareCollaborators();
  305. SandboxEditor::SetCameraCaptureCursorForLook(true);
  306. // When
  307. // move to the center of the screen
  308. auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
  309. MouseMove(m_rootWidget, start, QPoint(0, 0));
  310. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  311. // will move a small amount initially
  312. const auto mouseDelta = QPoint(5, 0);
  313. MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
  314. // ensure further updates to not continue to rotate
  315. for (int i = 0; i < 50; ++i)
  316. {
  317. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  318. }
  319. // Then
  320. // ensure the camera rotation is no longer the identity
  321. const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation();
  322. const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromQuaternion(cameraRotation));
  323. // initial amount of rotation after first mouse move
  324. using ::testing::FloatNear;
  325. EXPECT_THAT(eulerAngles.GetZ(), FloatNear(-0.025f, 0.001f));
  326. // Clean-up
  327. HaltCollaborators();
  328. }
  329. // test to verify deltas and cursor positions are handled correctly when the widget is moved
  330. TEST_F(ModularViewportCameraControllerFixture, CameraDoesNotStutterAfterWidgetIsMoved)
  331. {
  332. // Given
  333. PrepareCollaborators();
  334. SandboxEditor::SetCameraCaptureCursorForLook(true);
  335. // When
  336. // move cursor to the center of the screen
  337. auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
  338. MouseMove(m_rootWidget, start, QPoint(0, 0));
  339. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  340. // move camera right
  341. const auto mouseDelta = QPoint(200, 0);
  342. MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
  343. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  344. QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::NoModifier, start + mouseDelta);
  345. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  346. // update the position of the widget
  347. const auto offset = QPoint(500, 500);
  348. m_rootWidget->move(offset);
  349. // move cursor back to widget center
  350. MouseMove(m_rootWidget, start, QPoint(0, 0));
  351. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  352. // move camera left
  353. MousePressAndMove(m_rootWidget, start, -mouseDelta, Qt::MouseButton::RightButton);
  354. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  355. // Then
  356. // ensure the camera rotation has returned to the identity
  357. const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation();
  358. const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromQuaternion(cameraRotation));
  359. using ::testing::FloatNear;
  360. EXPECT_THAT(eulerAngles.GetX(), FloatNear(0.0f, 0.001f));
  361. EXPECT_THAT(eulerAngles.GetZ(), FloatNear(0.0f, 0.001f));
  362. // Clean-up
  363. HaltCollaborators();
  364. }
  365. TEST_F(ModularViewportCameraControllerFixture, CameraModifersAreDetectedWhenViewportIsNotInFocus)
  366. {
  367. SandboxEditor::SetCameraCaptureCursorForLook(false);
  368. // Given
  369. PrepareCollaborators();
  370. // store initial camera translation and rotation
  371. const AZ::Vector3 cameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
  372. const AZ::Quaternion cameraRotation = m_cameraViewportContextView->GetCameraTransform().GetRotation();
  373. // create default projected ray (going into the screen)
  374. using ::testing::_;
  375. using ::testing::Return;
  376. ON_CALL(m_mockViewportInteractionRequests, ViewportScreenToWorldRay(_))
  377. .WillByDefault(Return(AzToolsFramework::ViewportInteraction::ProjectedViewportRay{ cameraTranslation, AZ::Vector3::CreateAxisY() }));
  378. // When
  379. // press alt without main viewport in focus
  380. m_otherWidget->setFocus();
  381. QTest::keyPress(m_otherWidget, Qt::Key::Key_Alt);
  382. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  383. // change focus
  384. m_rootWidget->setFocus();
  385. // move cursor to the center of the screen
  386. auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
  387. MouseMove(m_rootWidget, start, QPoint(0, 0));
  388. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  389. // update starting position of mouse cursor request (if needed later)
  390. m_viewportMouseCursorRequests.m_mousePosition = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(start);
  391. // start a mouse press and update the viewport
  392. QTest::mousePress(m_rootWidget, Qt::MouseButton::LeftButton, Qt::NoModifier, start);
  393. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  394. // move mouse right and perform a camera orbit (with left mouse button held from before)
  395. const auto mouseDelta = QPoint(200, 0);
  396. MouseMove(m_rootWidget, start, mouseDelta);
  397. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
  398. // Then
  399. // camera should have moved (we track both position and rotation)
  400. EXPECT_THAT(cameraTranslation, ::testing::Not(IsClose(m_cameraViewportContextView->GetCameraTransform().GetTranslation())));
  401. EXPECT_THAT(cameraRotation, ::testing::Not(IsClose(m_cameraViewportContextView->GetCameraTransform().GetRotation())));
  402. // Clean-up
  403. HaltCollaborators();
  404. }
  405. TEST_F(ModularViewportCameraControllerFixture, CameraSystemStopsMovingWhenViewportLosesFocus)
  406. {
  407. SandboxEditor::SetCameraCaptureCursorForLook(false);
  408. // Given
  409. PrepareCollaborators();
  410. // ensure widgets are showing to make sure focus in/out events are fired correctly
  411. m_rootWidget->setVisible(true);
  412. m_otherWidget->setVisible(true);
  413. // store initial camera translation and rotation
  414. const AZ::Vector3 cameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
  415. // change focus to main widget
  416. m_rootWidget->setFocus();
  417. // start moving the camera left
  418. QTest::keyPress(m_rootWidget, Qt::Key::Key_A);
  419. // update the viewport
  420. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(1.0f), AZ::ScriptTimePoint() });
  421. // ensure the camera moved from its initial position
  422. const AZ::Vector3 nextCameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
  423. EXPECT_THAT(nextCameraTranslation, ::testing::Not(IsClose(cameraTranslation)));
  424. // move focus to the other widget
  425. m_otherWidget->setFocus();
  426. // update the viewport
  427. m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(1.0f), AZ::ScriptTimePoint() });
  428. // ensure the camera did not move from its last position
  429. const AZ::Vector3 lastCameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
  430. EXPECT_THAT(lastCameraTranslation, IsClose(nextCameraTranslation));
  431. // Clean-up
  432. HaltCollaborators();
  433. }
  434. } // namespace UnitTest