NetworkPlayerMovementComponent.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include <Source/Components/NetworkPlayerMovementComponent.h>
  8. #include <Source/Components/NetworkWeaponsComponent.h>
  9. #include <Source/Components/NetworkAiComponent.h>
  10. #include <Multiplayer/Components/NetworkCharacterComponent.h>
  11. #include <Source/Components/NetworkAnimationComponent.h>
  12. #include <Source/Components/NetworkMatchComponent.h>
  13. #include <Source/Components/NetworkSimplePlayerCameraComponent.h>
  14. #include <Multiplayer/Components/NetworkTransformComponent.h>
  15. #include <AzCore/Time/ITime.h>
  16. #include <AzFramework/Components/CameraBus.h>
  17. #include <AzFramework/Physics/SystemBus.h>
  18. #include <AzFramework/Physics/PhysicsScene.h>
  19. #include <AzFramework/Physics/CharacterBus.h>
  20. #include <AzFramework/Physics/Common/PhysicsTypes.h>
  21. #include <AzFramework/Physics/Components/SimulatedBodyComponentBus.h>
  22. #include <PhysX/CharacterGameplayBus.h>
  23. #include <PhysX/CharacterControllerBus.h>
  24. #include <GameplayEffectsNotificationBus.h>
  25. namespace MultiplayerSample
  26. {
  27. AZ_CVAR(float, cl_WasdStickAccel, 5.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The linear acceleration to apply to WASD inputs to simulate analog stick controls");
  28. AZ_CVAR(float, cl_AimStickScaleZ, 0.025f, nullptr, AZ::ConsoleFunctorFlags::Null, "The scaling to apply to aim and view adjustments");
  29. AZ_CVAR(float, cl_AimStickScaleX, 0.0125f, nullptr, AZ::ConsoleFunctorFlags::Null, "The scaling to apply to aim and view adjustments");
  30. /*
  31. * @cl_MaxMouseDelta should be large enough to contain the sum of mouse deltas across frames
  32. * between NetworkPlayerMovementComponentController::CreateInput() calls,
  33. * which happens at the frequency set by @cl_InputRateMs (33 times per second by default).
  34. *
  35. * The total value is then quantized using type @MouseAxis in "MultiplayerSampleTypes.h", for example:
  36. * using MouseAxis = AzNetworking::QuantizedValues<1, 2, -1, 1>;
  37. * (The four numbers within the template parameters above read as: for a single value, spend 2 bytes to quantize a value between -1 and 1.)
  38. *
  39. * Keep the ranges and the precision of QuantizedValues and float types in mind when modifying mouse input configuration values.
  40. */
  41. AZ_CVAR(float, cl_MaxMouseDelta, 128.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The sum of mouse deltas will be clamped to this maximum");
  42. #if AZ_TRAIT_CLIENT
  43. AZ_CVAR(bool, mps_botMode, false, nullptr, AZ::ConsoleFunctorFlags::Null, "If true, enable bot (AI) mode for client.");
  44. AZ_CVAR(float, mps_botMinInterval, 500.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The minimum amount of time between bot control updates");
  45. AZ_CVAR(float, mps_botMaxInterval, 9500.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The maximum amount of time between bot control updates");
  46. #endif
  47. NetworkPlayerMovementComponentController::NetworkPlayerMovementComponentController(NetworkPlayerMovementComponent& parent)
  48. : NetworkPlayerMovementComponentControllerBase(parent)
  49. #if AZ_TRAIT_SERVER
  50. , m_updateAI{ [this] { UpdateAI(); }, AZ::Name{ "MovementControllerAi" } }
  51. #endif
  52. #if AZ_TRAIT_CLIENT
  53. , m_updateLocalBot{ [this] { UpdateLocalBot(); }, AZ::Name{ "MovementControllerLocalBot" } }
  54. #endif
  55. {
  56. ;
  57. }
  58. void NetworkPlayerMovementComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
  59. {
  60. #if AZ_TRAIT_SERVER
  61. NetworkAiComponent* networkAiComponent = GetParent().GetNetworkAiComponent();
  62. m_aiEnabled = (networkAiComponent != nullptr) ? networkAiComponent->GetEnabled() : false;
  63. if (m_aiEnabled)
  64. {
  65. m_updateAI.Enqueue(AZ::TimeMs{ 0 }, true);
  66. m_networkAiComponentController = GetNetworkAiComponentController();
  67. }
  68. #endif
  69. #if AZ_TRAIT_CLIENT
  70. if (IsNetEntityRoleAutonomous())
  71. {
  72. if (mps_botMode)
  73. {
  74. m_updateLocalBot.Enqueue(AZ::TimeMs{ 0 }, true);
  75. }
  76. else
  77. {
  78. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(MoveFwdEventId);
  79. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(MoveBackEventId);
  80. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(MoveLeftEventId);
  81. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(MoveRightEventId);
  82. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(SprintEventId);
  83. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(JumpEventId);
  84. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(CrouchEventId);
  85. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(LookLeftRightEventId);
  86. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(LookUpDownEventId);
  87. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(ZoomInEventId);
  88. StartingPointInput::InputEventNotificationBus::MultiHandler::BusConnect(ZoomOutEventId);
  89. }
  90. }
  91. #endif
  92. // During activation the character controller is not created yet.
  93. // Connect to CharacterNotificationBus to listen when it's activated after creation.
  94. Physics::CharacterNotificationBus::Handler::BusConnect(GetEntityId());
  95. AzPhysics::SimulatedBody* worldBody = nullptr;
  96. AzPhysics::SimulatedBodyComponentRequestsBus::EventResult(worldBody, GetEntityId(), &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody);
  97. if (worldBody)
  98. {
  99. if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
  100. {
  101. m_gravity = sceneInterface->GetGravity(worldBody->m_sceneOwner).GetZ();
  102. }
  103. }
  104. }
  105. void NetworkPlayerMovementComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
  106. {
  107. #if AZ_TRAIT_CLIENT
  108. if (IsNetEntityRoleAutonomous() && !mps_botMode)
  109. {
  110. StartingPointInput::InputEventNotificationBus::MultiHandler::BusDisconnect();
  111. }
  112. #endif
  113. }
  114. void NetworkPlayerMovementComponentController::OnCharacterActivated([[maybe_unused]] const AZ::EntityId& entityId)
  115. {
  116. // Wait until the character is activated before requesting its parameters.
  117. Physics::CharacterRequestBus::EventResult(m_stepHeight, GetEntityId(), &Physics::CharacterRequestBus::Events::GetStepHeight);
  118. PhysX::CharacterControllerRequestBus::EventResult(m_radius, GetEntityId(), &PhysX::CharacterControllerRequestBus::Events::GetRadius);
  119. PhysX::CharacterGameplayRequestBus::EventResult(
  120. m_gravityMultiplier, GetEntityId(), &PhysX::CharacterGameplayRequestBus::Events::GetGravityMultiplier);
  121. Physics::CharacterNotificationBus::Handler::BusDisconnect();
  122. }
  123. void NetworkPlayerMovementComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime)
  124. {
  125. // Inputs for your own component always exist
  126. NetworkPlayerMovementComponentNetworkInput* playerInput = input.FindComponentInput<NetworkPlayerMovementComponentNetworkInput>();
  127. // Check current game-play state
  128. INetworkMatch* networkMatchComponent = AZ::Interface<INetworkMatch>::Get();
  129. if (networkMatchComponent && (networkMatchComponent->PlayerActionsAllowed() != AllowedPlayerActions::None))
  130. {
  131. // View Axis are clamped and brought into the -1,1 range for transport across the network.
  132. // These are set if the player actions allow for rotation and/or all movement.
  133. playerInput->m_viewYaw = MouseAxis(AZStd::clamp<float>(m_viewYaw, -cl_MaxMouseDelta, cl_MaxMouseDelta) / cl_MaxMouseDelta);
  134. playerInput->m_viewPitch = MouseAxis(AZStd::clamp<float>(m_viewPitch, -cl_MaxMouseDelta, cl_MaxMouseDelta) / cl_MaxMouseDelta);
  135. }
  136. // reset accumulated amounts
  137. m_viewYaw = 0.f;
  138. m_viewPitch = 0.f;
  139. if (networkMatchComponent && (networkMatchComponent->PlayerActionsAllowed() == AllowedPlayerActions::All))
  140. {
  141. // Check if the user requested to toggle sprint-state
  142. if (m_toggleSprint)
  143. {
  144. m_sprinting = !m_sprinting;
  145. m_toggleSprint = false;
  146. }
  147. // Movement axis
  148. // Since we're on a keyboard, this adds a touch of an acceleration curve to the keyboard inputs
  149. // This is so that tapping the keyboard moves the virtual stick less than just holding it down
  150. m_forwardWeight = std::min<float>(m_forwardDown ? m_forwardWeight + cl_WasdStickAccel * deltaTime : 0.0f, 1.0f);
  151. m_leftWeight = std::min<float>(m_leftDown ? m_leftWeight + cl_WasdStickAccel * deltaTime : 0.0f, 1.0f);
  152. m_backwardWeight = std::min<float>(m_backwardDown ? m_backwardWeight + cl_WasdStickAccel * deltaTime : 0.0f, 1.0f);
  153. m_rightWeight = std::min<float>(m_rightDown ? m_rightWeight + cl_WasdStickAccel * deltaTime : 0.0f, 1.0f);
  154. playerInput->m_forwardAxis = StickAxis(m_forwardWeight - m_backwardWeight);
  155. playerInput->m_strafeAxis = StickAxis(m_leftWeight - m_rightWeight);
  156. playerInput->m_sprint = m_sprinting && (playerInput->m_forwardAxis > 0.0f); // Only sprint if we're moving forward
  157. playerInput->m_jump = m_jumping;
  158. playerInput->m_crouch = m_crouching;
  159. }
  160. else
  161. {
  162. // Don't set m_forward/left/right/backDown to 0, instead just 0 out the net-input by hand.
  163. // This way players can start the round hot out of the gate.
  164. // Keep their finger on the 'W' (forward) key, and instantly start
  165. // running when the round starts instead of having to release the 'W' key and pressing it again.
  166. playerInput->m_forwardAxis = StickAxis(0);
  167. playerInput->m_strafeAxis = StickAxis(0);
  168. playerInput->m_sprint = false;
  169. playerInput->m_jump = false;
  170. playerInput->m_crouch = false;
  171. }
  172. // reset jumping until next press. We only track when the jump is initially pressed, not that it's being held.
  173. m_jumping = false;
  174. // Just a note for anyone who is super confused by this, ResetCount is a predictable network property, it gets set on the client
  175. // through correction packets
  176. playerInput->m_resetCount = GetNetworkTransformComponentController()->GetResetCount();
  177. }
  178. void NetworkPlayerMovementComponentController::ProcessInput(Multiplayer::NetworkInput& input, float deltaTime)
  179. {
  180. // If the input reset count doesn't match the state's reset count it can mean two things:
  181. // 1) On the server: we were reset and we are now receiving inputs from the client for an old reset count
  182. // 2) On the client: we were reset and we are replaying old inputs after being corrected
  183. // In both cases we don't want to process these inputs
  184. NetworkPlayerMovementComponentNetworkInput* playerInput = input.FindComponentInput<NetworkPlayerMovementComponentNetworkInput>();
  185. if (playerInput->m_resetCount != GetNetworkTransformComponentController()->GetResetCount())
  186. {
  187. return;
  188. }
  189. NetworkWeaponsComponentNetworkInput* weaponInput = input.FindComponentInput<NetworkWeaponsComponentNetworkInput>();
  190. if ((weaponInput != nullptr) && weaponInput->m_firing.AnySet())
  191. {
  192. // Note that weaponInput is not guaranteed to exist, so we have to check for nullptr
  193. // Don't allow sprinting when the character is trying to shoot
  194. playerInput->m_sprint = false;
  195. }
  196. const bool wasOnGround = GetWasOnGround();
  197. bool onGround = GetOnGround();
  198. // Update the "on ground" state for the character.
  199. PhysX::CharacterGameplayRequestBus::EventResult(onGround, GetEntityId(), &PhysX::CharacterGameplayRequestBus::Events::IsOnGround);
  200. SetOnGround(onGround);
  201. // Track timers for how recently it's been since the player was on the ground and how recently they pressed the jump button.
  202. // These will be compared against "slop factors" to allow for a little bit of leniency in jumping to make it feel more reactive.
  203. SetSecondsSinceOnGround(onGround ? 0.0f : (GetSecondsSinceOnGround() + deltaTime));
  204. SetSecondsSinceJumpRequest(playerInput->m_jump ? 0.0f : (GetSecondsSinceJumpRequest() + deltaTime));
  205. // Update orientation
  206. AZ::Vector3 aimAngles = GetNetworkSimplePlayerCameraComponentController()->GetAimAngles();
  207. aimAngles.SetZ(NormalizeHeading(aimAngles.GetZ() - playerInput->m_viewYaw * cl_AimStickScaleZ * cl_MaxMouseDelta));
  208. aimAngles.SetX(NormalizeHeading(aimAngles.GetX() - playerInput->m_viewPitch * cl_AimStickScaleX * cl_MaxMouseDelta));
  209. aimAngles.SetX(NormalizeHeading(AZ::GetClamp(aimAngles.GetX(), -AZ::Constants::QuarterPi * 1.5f, AZ::Constants::QuarterPi * 1.5f)));
  210. GetNetworkSimplePlayerCameraComponentController()->SetAimAngles(aimAngles);
  211. const AZ::Quaternion newOrientation = AZ::Quaternion::CreateRotationZ(aimAngles.GetZ());
  212. GetEntity()->GetTransform()->SetLocalRotationQuaternion(newOrientation);
  213. // Update velocity
  214. bool jumpTriggered = false;
  215. bool movingDownward = false;
  216. UpdateVelocity(*playerInput, deltaTime, jumpTriggered, movingDownward);
  217. // absolute velocity is based on velocity generated by the player and other sources
  218. const AZ::Vector3 absoluteVelocity = GetVelocityFromExternalSources() + GetSelfGeneratedVelocity();
  219. GetNetworkCharacterComponentController()->TryMoveWithVelocity(absoluteVelocity, deltaTime);
  220. // If a jump was triggered, reset our jump request time to our "slop threshold" so that we don't double-count the jump request
  221. // if we land too quickly.
  222. if (jumpTriggered)
  223. {
  224. SetSecondsSinceJumpRequest(GetJumpPressQueuedSeconds());
  225. }
  226. // Tell the camera whether or not we're sprinting
  227. GetNetworkSimplePlayerCameraComponentController()->SetSprintMode(playerInput->m_sprint);
  228. GetNetworkAnimationComponentController()->ModifyActiveAnimStates().SetBit(aznumeric_cast<uint32_t>(CharacterAnimState::Sprinting), playerInput->m_sprint);
  229. GetNetworkAnimationComponentController()->ModifyActiveAnimStates().SetBit(aznumeric_cast<uint32_t>(CharacterAnimState::Crouching), playerInput->m_crouch);
  230. // The Landing anim state will automatically turn off after it's triggered
  231. GetNetworkAnimationComponentController()->ModifyActiveAnimStates().SetBit(aznumeric_cast<uint32_t>(CharacterAnimState::Landing), onGround && !wasOnGround && !jumpTriggered);
  232. // Always set/clear the jump state every tick or you might get ghost jump animations.
  233. // We only set it on the tick where the jump is first triggered, not for the entire jump.
  234. GetNetworkAnimationComponentController()->ModifyActiveAnimStates().SetBit(aznumeric_cast<uint32_t>(CharacterAnimState::Jumping), jumpTriggered);
  235. // Set whether or not we're currently falling.
  236. GetNetworkAnimationComponentController()->ModifyActiveAnimStates().SetBit(aznumeric_cast<uint32_t>(CharacterAnimState::Falling), !onGround);
  237. // If we're still on the ground, then zero out our velocity from external forces
  238. // This prevents us from sliding along the ground after we land
  239. PhysX::CharacterGameplayRequestBus::EventResult(onGround, GetEntityId(), &PhysX::CharacterGameplayRequestBus::Events::IsOnGround);
  240. if (onGround)
  241. {
  242. SetVelocityFromExternalSources(AZ::Vector3::CreateZero());
  243. }
  244. // At the end, track whether or not we were on the ground for this input so we can compare states when processing the next input.
  245. SetWasOnGround(onGround);
  246. }
  247. #if AZ_TRAIT_SERVER
  248. void NetworkPlayerMovementComponentController::HandleApplyImpulse([[maybe_unused]] AzNetworking::IConnection* connection, const AZ::Vector3& impulse, const bool& external)
  249. {
  250. if (external)
  251. {
  252. const AZ::Vector3 newVelocity = GetVelocityFromExternalSources() + impulse;
  253. SetVelocityFromExternalSources(newVelocity);
  254. }
  255. else
  256. {
  257. const AZ::Vector3 newVelocity = GetSelfGeneratedVelocity() + impulse;
  258. SetSelfGeneratedVelocity(newVelocity);
  259. }
  260. }
  261. void NetworkPlayerMovementComponentController::HandleSetVelocity([[maybe_unused]] AzNetworking::IConnection* connection, const AZ::Vector3& velocity, const bool& external)
  262. {
  263. if (external)
  264. {
  265. SetVelocityFromExternalSources(velocity);
  266. }
  267. else
  268. {
  269. SetSelfGeneratedVelocity(velocity);
  270. }
  271. }
  272. #endif
  273. void NetworkPlayerMovementComponentController::UpdateVelocity(const NetworkPlayerMovementComponentNetworkInput& playerInput, float deltaTime, bool& jumpTriggered, bool& movingDownward)
  274. {
  275. AZ::Vector3 velocityFromExternalSources = GetVelocityFromExternalSources(); // non-player generated (jump pads, explosions etc.)
  276. AZ::Vector3 selfGeneratedVelocity = GetSelfGeneratedVelocity(); // player generated
  277. const float secondsSinceOnGround = GetSecondsSinceOnGround();
  278. const float secondsSinceJumpRequest = GetSecondsSinceJumpRequest();
  279. const bool onGround = GetOnGround();
  280. if (onGround)
  281. {
  282. // Reset our jumping state if we're on the ground.
  283. if (GetIsJumping())
  284. {
  285. SetIsJumping(false);
  286. }
  287. // If we're on the ground, we should never have velocities pushing us into the ground.
  288. if (selfGeneratedVelocity.GetZ() <= 0.0f)
  289. {
  290. selfGeneratedVelocity.SetZ(0.0f);
  291. }
  292. }
  293. else
  294. {
  295. // If we're not on the ground, apply gravity.
  296. // NOTE: We do this *before* trying to trigger a jump so that the jump can overwrite the velocity and not have
  297. // gravity applied on the first tick of the jump.
  298. selfGeneratedVelocity.SetZ(selfGeneratedVelocity.GetZ() + m_gravity * m_gravityMultiplier * deltaTime);
  299. }
  300. // Ideally, the way velocities would work below is that there would be a single player velocity tracking the player's
  301. // current velocity, and pressing the jump button and getting external sources would just add an impulse into the player's
  302. // velocity, which then would get ticked back down by gravity over time.
  303. // However, the animation graph is expecting the player-generated velocity to be tracked separately from any external sources,
  304. // so at least for now, we'll keep them as separate velocities.
  305. // The one place where this is a problem is in how to make gravity work, as seen below. It's not actually solved correctly
  306. // right now, but until something uses the external sources velocity, it's hard to tell whether or not it needs to be fixed
  307. // in a better way.
  308. // If we're not currently jumping, see if we should trigger a jump.
  309. if (!GetIsJumping())
  310. {
  311. // We can trigger a jump as long as we aren't currently falling
  312. if (selfGeneratedVelocity.GetZ() <= 0.0f)
  313. {
  314. // We can trigger a jump as long as we *were* on the ground in the last "slop factor" fractions of a second
  315. if (secondsSinceOnGround <= GetJumpOnGroundQueuedSeconds())
  316. {
  317. // We can trigger a jump as long as we pressed the jump button in the last "slop factor" fractions of a second
  318. if (secondsSinceJumpRequest <= GetJumpPressQueuedSeconds())
  319. {
  320. // We're jumping, so set the upwards velocity necessary to reach our desired jump height.
  321. // Note that we're only setting Z velocity because the XY velocity components can be changed by the player
  322. // even while in midair.
  323. const float initialJumpVelocity = AZ::Sqrt(2.0f * (-m_gravity * m_gravityMultiplier) * GetMaxJumpHeight());
  324. selfGeneratedVelocity.SetZ(initialJumpVelocity);
  325. jumpTriggered = true;
  326. SetIsJumping(true);
  327. GameplayEffectsNotificationBus::Broadcast(&GameplayEffectsNotificationBus::Events::OnPositionalEffect,
  328. SoundEffect::PlayerExertion, GetEntity()->GetTransform()->GetWorldTranslation());
  329. }
  330. }
  331. }
  332. }
  333. // External sources can add velocity, but get clamped to 0 so that they never add negative velocity, since
  334. // the selfGeneratedVelocity already fully accounts for gravity so that the player can fall.
  335. // If we don't clamp it, we'll get too much gravity influence on the player.
  336. // Note that because we're applying gravity to the external source as well as to the self-generated velocity,
  337. // we're double-counting gravity's influence. This will probably make the external source velocities a bit harder
  338. // to tune.
  339. velocityFromExternalSources.SetZ(
  340. AZStd::max(0.0f, velocityFromExternalSources.GetZ() + m_gravity * m_gravityMultiplier * deltaTime));
  341. // Now that we've got the vertical velocity from jumps / external sources / gravity accounted for, let's calculate
  342. // the player horizontal movement velocity.
  343. const float fwdBack = playerInput.m_forwardAxis;
  344. const float leftRight = playerInput.m_strafeAxis;
  345. // TODO break out into air/water/ground speeds
  346. float speed = 0.0f;
  347. if (playerInput.m_crouch)
  348. {
  349. speed = GetCrouchSpeed();
  350. }
  351. else if (fwdBack < 0.0f)
  352. {
  353. speed = GetReverseSpeed();
  354. }
  355. else
  356. {
  357. if (playerInput.m_sprint)
  358. {
  359. speed = GetSprintSpeed();
  360. }
  361. else
  362. {
  363. speed = GetWalkSpeed();
  364. }
  365. }
  366. // If the player isn't trying to move, set the self-generated XY velocity to 0.
  367. // If they *are* trying to move, set a velocity based on the XY movement direction requested and the Z direction based
  368. // on the slope of the ground directly ahead vs what's currently under the player.
  369. if (fwdBack != 0.0f || leftRight != 0.0f)
  370. {
  371. const float stickInputAngle = AZ::Atan2(leftRight, fwdBack);
  372. const float currentHeading = GetNetworkTransformComponentController()->GetRotation().GetEulerRadians().GetZ();
  373. const float targetHeadingAngleRadians = NormalizeHeading(currentHeading + stickInputAngle);
  374. // Using the unit vector from GetSlopeHeading that provides the direction of movement, multiply by speed
  375. // to get the moveVelocity.
  376. const AZ::Vector3 moveVelocity = GetSlopeHeading(targetHeadingAngleRadians) * speed;
  377. // Immediately switch to the newly-requested XY movement direction, even if in midair.
  378. // We've chosen not to apply acceleration / deceleration.
  379. selfGeneratedVelocity.SetX(moveVelocity.GetX());
  380. selfGeneratedVelocity.SetY(moveVelocity.GetY());
  381. // If we're jumping, use the jumping/falling Z velocity. If we're on the ground or falling not from a jump, then we'll
  382. // add in any movement velocity. This will be 0 if there's no ground near us, but it will be in the direction of the ground
  383. // if we're next to some ground that's within a step height up or down.
  384. // Note that we can't just check for "on ground" here, because in the case of moving down a ramp or small steps, we might
  385. // actually be off the ground a little bit and need to correct our movement downward to get back onto the ground.
  386. if (!GetIsJumping())
  387. {
  388. selfGeneratedVelocity.SetZ(selfGeneratedVelocity.GetZ() + moveVelocity.GetZ());
  389. // If we're not jumping and we have a downwards velocity, track that we're deliberately moving downward so that
  390. // we can distinguish this state from arbitrary falling.
  391. if (moveVelocity.GetZ() < 0.0f)
  392. {
  393. movingDownward = true;
  394. }
  395. }
  396. }
  397. else
  398. {
  399. // instant deceleration for now
  400. selfGeneratedVelocity.SetX(0.f);
  401. selfGeneratedVelocity.SetY(0.f);
  402. }
  403. SetVelocityFromExternalSources(velocityFromExternalSources);
  404. SetSelfGeneratedVelocity(selfGeneratedVelocity);
  405. }
  406. AZ::Vector3 NetworkPlayerMovementComponentController::GetSlopeHeading(float headingAngleRadians) const
  407. {
  408. // Returns a unit vector pointing in the direction that the player is moving.
  409. // Start with a direction vector in the XY plane.
  410. const AZ::Vector3 fwd = AZ::Quaternion::CreateRotationZ(headingAngleRadians).TransformVector(AZ::Vector3::CreateAxisY());
  411. // The origin is set to the bottom of the player, not the center.
  412. const AZ::Vector3 origin = GetEntity()->GetTransform()->GetWorldTranslation();
  413. constexpr float forwardEpsilon = 0.01f;
  414. constexpr float heightEpsilon = 0.01f;
  415. // Raycast straight down in front of the player by a tiny amount (forwardEpsilon) starting at the step height plus an epsilon
  416. // and ending at negative step height plus an epsilon. This will tell us if there's any surface directly in front of the player
  417. // within the step height up or down. If so, we'll use that to calculate the Z direction.
  418. const AZ::Vector3 start = origin + fwd * (m_radius + forwardEpsilon) + AZ::Vector3(0.f, 0.f, m_stepHeight + heightEpsilon);
  419. AzPhysics::RayCastRequest request;
  420. request.m_start = start;
  421. request.m_direction = AZ::Vector3::CreateAxisZ(-1.f);
  422. request.m_distance = (m_stepHeight + heightEpsilon) * 2.f;
  423. request.m_queryType = AzPhysics::SceneQuery::QueryType::Static;
  424. AzPhysics::SceneQueryHits result;
  425. if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
  426. {
  427. if (AzPhysics::SceneHandle sceneHandle = sceneInterface->GetSceneHandle(AzPhysics::DefaultPhysicsSceneName);
  428. sceneHandle != AzPhysics::InvalidSceneHandle)
  429. {
  430. result = sceneInterface->QueryScene(sceneHandle, &request);
  431. }
  432. }
  433. // If we've found a surface in front of us that's within the step height in size in either direction, then we'll create a vector
  434. // from the current bottom of the player to that new location so that our heading direction accounts for the Z slope.
  435. if (result && result.m_hits[0].IsValid())
  436. {
  437. // we use epsilon here to avoid the case where we are pushing up against an object and become slightly
  438. // elevated
  439. if (result.m_hits[0].m_position.GetZ() < (origin.GetZ() - heightEpsilon))
  440. {
  441. const AZ::Vector3 delta = result.m_hits[0].m_position - origin;
  442. return delta.GetNormalized();
  443. }
  444. }
  445. return fwd;
  446. }
  447. float NetworkPlayerMovementComponentController::NormalizeHeading(float heading) const
  448. {
  449. // Ensure heading in range [-pi, +pi]
  450. if (heading > AZ::Constants::Pi)
  451. {
  452. return static_cast<float>(heading - AZ::Constants::TwoPi);
  453. }
  454. else if (heading < -AZ::Constants::Pi)
  455. {
  456. return static_cast<float>(heading + AZ::Constants::TwoPi);
  457. }
  458. return heading;
  459. }
  460. void NetworkPlayerMovementComponentController::OnPressed(float value)
  461. {
  462. const StartingPointInput::InputEventNotificationId* inputId = StartingPointInput::InputEventNotificationBus::GetCurrentBusId();
  463. if (inputId == nullptr)
  464. {
  465. return;
  466. }
  467. else if (*inputId == MoveFwdEventId)
  468. {
  469. m_forwardDown = true;
  470. }
  471. else if (*inputId == MoveBackEventId)
  472. {
  473. m_backwardDown = true;
  474. }
  475. else if (*inputId == MoveLeftEventId)
  476. {
  477. m_leftDown = true;
  478. }
  479. else if (*inputId == MoveRightEventId)
  480. {
  481. m_rightDown = true;
  482. }
  483. else if (*inputId == SprintEventId)
  484. {
  485. m_toggleSprint = true;
  486. }
  487. else if (*inputId == JumpEventId)
  488. {
  489. m_jumping = true;
  490. }
  491. else if (*inputId == CrouchEventId)
  492. {
  493. m_crouching = true;
  494. }
  495. else if (*inputId == LookLeftRightEventId)
  496. {
  497. // Accumulate input to be processed in CreateInput().
  498. // In-between two CreateInput() calls, multiple series of presses and holds can occur, accumulate all of them,
  499. // otherwise we will drop some of the input data by only including the last press and hold combination.
  500. m_viewYaw += value;
  501. }
  502. else if (*inputId == LookUpDownEventId)
  503. {
  504. m_viewPitch += value;
  505. }
  506. }
  507. void NetworkPlayerMovementComponentController::OnReleased([[maybe_unused]]float value)
  508. {
  509. const StartingPointInput::InputEventNotificationId* inputId = StartingPointInput::InputEventNotificationBus::GetCurrentBusId();
  510. if (inputId == nullptr)
  511. {
  512. return;
  513. }
  514. else if (*inputId == MoveFwdEventId)
  515. {
  516. m_forwardDown = false;
  517. }
  518. else if (*inputId == MoveBackEventId)
  519. {
  520. m_backwardDown = false;
  521. }
  522. else if (*inputId == MoveLeftEventId)
  523. {
  524. m_leftDown = false;
  525. }
  526. else if (*inputId == MoveRightEventId)
  527. {
  528. m_rightDown = false;
  529. }
  530. else if (*inputId == JumpEventId)
  531. {
  532. m_jumping = false;
  533. }
  534. else if (*inputId == CrouchEventId)
  535. {
  536. m_crouching = false;
  537. }
  538. }
  539. void NetworkPlayerMovementComponentController::OnHeld(float value)
  540. {
  541. const StartingPointInput::InputEventNotificationId* inputId = StartingPointInput::InputEventNotificationBus::GetCurrentBusId();
  542. if (inputId == nullptr)
  543. {
  544. return;
  545. }
  546. else if (*inputId == LookLeftRightEventId)
  547. {
  548. // accumulate input to be processed in CreateInput()
  549. m_viewYaw += value;
  550. }
  551. else if (*inputId == LookUpDownEventId)
  552. {
  553. // accumulate input to be processed in CreateInput()
  554. m_viewPitch += value;
  555. }
  556. }
  557. #if AZ_TRAIT_SERVER
  558. void NetworkPlayerMovementComponentController::UpdateAI()
  559. {
  560. float deltaTime = static_cast<float>(m_updateAI.TimeInQueueMs()) / 1000.f;
  561. if (m_networkAiComponentController != nullptr)
  562. {
  563. m_networkAiComponentController->TickMovement(*this, deltaTime);
  564. }
  565. }
  566. #endif
  567. #if AZ_TRAIT_CLIENT
  568. void NetworkPlayerMovementComponentController::UpdateLocalBot()
  569. {
  570. float deltaTimeMs = static_cast<float>(m_updateLocalBot.TimeInQueueMs());
  571. m_botRemainingTime -= deltaTimeMs;
  572. if (m_botRemainingTime <= 0)
  573. {
  574. // Determine a new directive after 500 to 9500 ms
  575. m_botRemainingTime = (m_botLcg.GetRandomFloat() * (mps_botMaxInterval - mps_botMinInterval) + mps_botMinInterval);
  576. m_botTurnRate = (1.f / m_botRemainingTime);
  577. // Randomize new target yaw and pitch and compute the delta from the current yaw and pitch respectively
  578. m_botTargetYawDelta = -m_viewYaw + (m_botLcg.GetRandomFloat() * 2.f - 1.f);
  579. m_botTargetPitchDelta = -m_viewPitch + (m_botLcg.GetRandomFloat() - 0.5f);
  580. // Randomize the action and strafe direction (used only if we decide to strafe)
  581. m_botAction = static_cast<Action>(m_botLcg.GetRandom() % static_cast<int>(Action::COUNT));
  582. m_botStrafingRight = static_cast<bool>(m_botLcg.GetRandom() % 2);
  583. }
  584. // Translate desired motion into inputs
  585. // Interpolate the current view yaw and pitch values towards the desired values
  586. m_viewYaw += m_botTurnRate * deltaTimeMs * m_botTargetPitchDelta;
  587. m_viewPitch += m_botTurnRate * deltaTimeMs * m_botTargetPitchDelta;
  588. // Reset keyboard movement inputs decided on the previous frame
  589. m_forwardDown = false;
  590. m_backwardDown = false;
  591. m_leftDown = false;
  592. m_rightDown = false;
  593. m_sprinting = false;
  594. m_jumping = false;
  595. m_crouching = false;
  596. switch (m_botAction)
  597. {
  598. case Action::Default:
  599. m_forwardDown = true;
  600. break;
  601. case Action::Sprinting:
  602. m_forwardDown = true;
  603. m_sprinting = true;
  604. break;
  605. case Action::Jumping:
  606. m_forwardDown = true;
  607. m_jumping = true;
  608. break;
  609. case Action::Crouching:
  610. m_forwardDown = true;
  611. m_crouching = true;
  612. break;
  613. case Action::Strafing:
  614. if (m_botStrafingRight)
  615. {
  616. m_rightDown = true;
  617. }
  618. else
  619. {
  620. m_leftDown = true;
  621. }
  622. break;
  623. default:
  624. break;
  625. }
  626. }
  627. #endif
  628. } // namespace MultiplayerSample