Browse Source

Merge pull request #10860 from aws-lumberyard-dev/cc-timestep

Change character controller to update on physics timestep rather than on tick
greerdv 3 năm trước cách đây
mục cha
commit
cb79eb0c76

+ 50 - 14
Code/Framework/AzFramework/AzFramework/Physics/Character.h

@@ -112,24 +112,60 @@ namespace Physics
         virtual AzPhysics::CollisionGroup GetCollisionGroup() const = 0;
         virtual AZ::Crc32 GetColliderTag() const = 0;
 
-        /// Queues up a request to apply a velocity to the character.
-        /// All requests received during a tick are accumulated (so for example, the effects of animation and gravity
-        /// can be applied in two separate requests), and a movement with the accumulated velocity is performed once
-        /// per tick, prior to the physics update.
-        /// Obstacles may prevent the actual movement from exactly matching the requested movement.
-        /// @param velocity The velocity to be added to the accumulated requests.
-        virtual void AddVelocity(const AZ::Vector3& velocity) = 0;
-
-        /// Applies the queued velocity requests and zeros the accumulated requested velocity.
-        /// The expected usage is for this function to be called internally by the physics system once per tick,
+        // O3DE_DEPRECATION_NOTICE(GHI-10883)
+        // Please use AddVelocityForTick or AddVelocityForPhysicsTimestep as appropriate.
+        virtual void AddVelocity(const AZ::Vector3& velocity)
+        {
+            AZ_WarningOnce(
+                "Physics::Character",
+                false,
+                "AddVelocity is deprecated, please use AddVelocityForTick or AddVelocityForPhysicsTimestep as appropriate.");
+            AddVelocityForTick(velocity);
+        };
+
+        /// Queues up a request to apply a velocity to the character, lasting for the duration of the tick.
+        /// All requests received are accumulated (so for example, the effects of animation and gravity
+        /// can be applied in two separate requests), and the accumulated velocity is used when the character updates.
+        /// Velocities added this way will apply until the end of the tick.
+        /// Obstacles and the maximum speed setting may prevent the actual movement from exactly matching the requested movement.
+        /// @param velocity The velocity to be added to the accumulated requests, lasting for the duration of the tick.
+        virtual void AddVelocityForTick(const AZ::Vector3& velocity) = 0;
+
+        /// Queues up a request to apply a velocity to the character, lasting for the duration of the physics timestep.
+        /// All requests received are accumulated (so for example, the effects of animation and gravity
+        /// can be applied in two separate requests), and the accumulated velocity is used when the character updates.
+        /// Velocities added this way will apply until the end of the physics timestep.
+        /// Obstacles and the maximum speed setting may prevent the actual movement from exactly matching the requested movement.
+        /// @param velocity The velocity to be added to the accumulated requests, lasting for the duration of a physics timestep.
+        virtual void AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity) = 0;
+
+        /// Applies the queued velocity requests.
+        /// The expected usage is for this function to be called internally by the physics system,
         /// so that the cumulative result of multiple movement effects (e.g. animation, gravity, pseudo-impulses etc)
-        /// can be combined from separate calls to AddVelocity. Accumulating the requests avoids performing
-        /// multiple expensive character updates, and avoids any effects from the order of requests within a tick.
-        /// Users who wish to add a new movement effect should generally just be able to use AddVelocity, and
-        /// rely on the existing physics system call to ApplyRequestedVelocity.
+        /// can be combined from separate calls to AddVelocityForTick or AddVelocityForPhysicsTimestep.
+        /// Accumulating the requests avoids performing multiple expensive character updates, and avoids any effects from the order of
+        /// requests within a tick. Users who wish to add a new movement effect should generally just be able to use AddVelocityForTick or
+        /// AddVelocityForPhysicsTimestep, and rely on the existing physics system call to ApplyRequestedVelocity.
         /// @param deltaTime The duration over which to apply the accumulated requested velocity.
         virtual void ApplyRequestedVelocity(float deltaTime) = 0;
 
+        /// Sets the accumulated velocity requests which last for the duration of a tick to zero.
+        /// The expected usage is for this funciton to be called internally by the physics system, to flush per tick
+        /// velocity requests once per tick. Velocity requests with duration of the physics timestep are not affected.
+        virtual void ResetRequestedVelocityForTick() = 0;
+
+        /// Sets the accumulated velocity requests which last for the duration of a physics timestep to zero.
+        /// The expected usage is for this funciton to be called internally by the physics system, to flush per timestep
+        /// velocity requests once per timestep. Velocity requests with duration of the tick are not affected.
+        virtual void ResetRequestedVelocityForPhysicsTimestep() = 0;
+
+        /// Directly requests the character to move.
+        /// Used for making an immediate, displacement-based movement request, as opposed to a queued, velocity-based request.
+        /// Obstacles may prevent the actual movement from exactly matching the requested movement.
+        /// @ param requestedMovement The desired displacement relative to the character's current position.
+        /// @ param deltaTime The duration over which to apply the requested movement.
+        virtual void Move(const AZ::Vector3& requestedMovement, float deltaTime) = 0;
+
         virtual void AttachShape(AZStd::shared_ptr<Physics::Shape> shape) = 0;
     };
 } // namespace Physics

+ 22 - 5
Code/Framework/AzFramework/AzFramework/Physics/CharacterBus.h

@@ -70,12 +70,29 @@ namespace Physics
         /// Gets the observed velocity of the character, which may differ from the desired velocity if the character is obstructed.
         virtual AZ::Vector3 GetVelocity() const = 0;
 
-        /// Queues up a request to apply a velocity to the character.
-        /// All requests received during a tick are accumulated (so for example, the effects of animation and gravity
-        /// can be applied in two separate requests), and a movement with the accumulated velocity is performed once
-        /// per tick, prior to the physics update.
+        // O3DE_DEPRECATION_NOTICE(GHI-10883)
+        // Please use AddVelocityForTick or AddVelocityForPhysicsTimestep as appropriate.
+        virtual void AddVelocity(const AZ::Vector3& velocity)
+        {
+            AddVelocityForTick(velocity);
+        };
+
+        /// Queues up a request to apply a velocity to the character, lasting for the duration of the tick.
+        /// All requests received are accumulated (so for example, the effects of animation and gravity
+        /// can be applied in two separate requests), and the accumulated velocity is used when the character updates.
+        /// Velocities added this way will apply until the end of the tick.
         /// Obstacles may prevent the actual movement from exactly matching the requested movement.
-        virtual void AddVelocity(const AZ::Vector3& velocity) = 0;
+        /// @param velocity The velocity to be added to the accumulated requests, lasting for the duration of the tick.
+        virtual void AddVelocityForTick(const AZ::Vector3& velocity) = 0;
+
+        /// Queues up a request to apply a velocity to the character, lasting for the duration of the physics timestep.
+        /// All requests received are accumulated (so for example, the effects of animation and gravity
+        /// can be applied in two separate requests), and the accumulated velocity is used when the character updates.
+        /// Velocities added this way will apply until the end of the physics timestep.
+        /// Obstacles may prevent the actual movement from exactly matching the requested movement.
+        /// @param velocity The velocity to be added to the accumulated requests, lasting for the duration of a physics timestep.
+        virtual void AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity) = 0;
+
 
         /// Check if there is a character physics component present.
         /// Return true in the request handler implementation in order for things like the animation system to work properly.

+ 4 - 4
Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.h

@@ -444,8 +444,8 @@ namespace AzPhysics
         SceneEvents::OnSimulationBodyRemoved m_simulatedBodyRemovedEvent;
         SceneEvents::OnSimulationBodySimulationEnabled m_simulatedBodySimulationEnabledEvent;
         SceneEvents::OnSimulationBodySimulationDisabled m_simulatedBodySimulationDisabledEvent;
-        SceneEvents::OnSceneSimulationStartEvent m_sceneSimuationStartEvent;
-        SceneEvents::OnSceneSimulationFinishEvent m_sceneSimuationFinishEvent;
+        SceneEvents::OnSceneSimulationStartEvent m_sceneSimulationStartEvent;
+        SceneEvents::OnSceneSimulationFinishEvent m_sceneSimulationFinishEvent;
         SceneEvents::OnSceneActiveSimulatedBodiesEvent m_sceneActiveSimulatedBodies;
         SceneEvents::OnSceneCollisionsEvent m_sceneCollisionEvent;
         SceneEvents::OnSceneTriggersEvent m_sceneTriggerEvent;
@@ -485,12 +485,12 @@ namespace AzPhysics
 
     inline void Scene::RegisterSceneSimulationStartHandler(SceneEvents::OnSceneSimulationStartHandler& handler)
     {
-        handler.Connect(m_sceneSimuationStartEvent);
+        handler.Connect(m_sceneSimulationStartEvent);
     }
 
     inline void Scene::RegisterSceneSimulationFinishHandler(SceneEvents::OnSceneSimulationFinishHandler& handler)
     {
-        handler.Connect(m_sceneSimuationFinishEvent);
+        handler.Connect(m_sceneSimulationFinishEvent);
     }
 
     inline void Scene::RegisterSceneActiveSimulatedBodiesHandler(SceneEvents::OnSceneActiveSimulatedBodiesEvent::Handler& handler)

+ 5 - 1
Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp

@@ -90,7 +90,11 @@ namespace Physics
                     ->Event("SetMaximumSpeed", &CharacterRequests::SetMaximumSpeed, "Set Maximum Speed")
                     ->Event("GetVelocity", &CharacterRequests::GetVelocity, "Get Velocity")
                     ->Event("AddVelocity", &CharacterRequests::AddVelocity, "Add Velocity")
-                    ;
+                    ->Event("AddVelocityForTick", &CharacterRequests::AddVelocityForTick, "Add Velocity For Tick")
+                    ->Event(
+                        "AddVelocityForPhysicsTimestep",
+                        &CharacterRequests::AddVelocityForPhysicsTimestep,
+                        "Add Velocity For Physics Timestep");
             }
         }
 

+ 1 - 1
Gems/EMotionFX/Code/Source/Integration/System/SystemComponent.cpp

@@ -673,7 +673,7 @@ namespace EMotionFX
                 if (hasPhysicsController)
                 {
                     Physics::CharacterRequestBus::Event(
-                        entityId, &Physics::CharacterRequests::AddVelocity, positionDelta * deltaTimeInv);
+                        entityId, &Physics::CharacterRequests::AddVelocityForTick, positionDelta * deltaTimeInv);
                 }
                 else if (hasCustomMotionExtractionController)
                 {

+ 2 - 0
Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h

@@ -63,6 +63,8 @@ namespace Multiplayer
         bool IsOnGround() const override;
         float GetGravityMultiplier() const override { return {}; }
         void SetGravityMultiplier([[maybe_unused]] float gravityMultiplier) override {}
+        float GetGroundDetectionBoxHeight() const override { return {}; }
+        void SetGroundDetectionBoxHeight([[maybe_unused]] float groundDetectionBoxHeight) override {}
         AZ::Vector3 GetFallingVelocity() const override { return {}; }
         void SetFallingVelocity([[maybe_unused]] const AZ::Vector3& fallingVelocity) override {}
 

+ 1 - 2
Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp

@@ -213,8 +213,7 @@ namespace Multiplayer
         {
             return GetEntity()->GetTransform()->GetWorldTranslation();
         }
-        GetParent().m_physicsCharacter->AddVelocity(velocity);
-        GetParent().m_physicsCharacter->ApplyRequestedVelocity(deltaTime);
+        GetParent().m_physicsCharacter->Move(velocity * deltaTime, deltaTime);
         GetEntity()->GetTransform()->SetWorldTranslation(GetParent().m_physicsCharacter->GetBasePosition());
         AZLOG
         (

+ 8 - 1
Gems/PhysX/Code/Include/PhysX/CharacterGameplayBus.h

@@ -28,9 +28,16 @@ namespace PhysX
         //! Sets the gravity multipier.
         //! The gravity multiplier is combined with the gravity value for the physics world to which the character
         //! belongs when applying gravity to the character.
-        //! @param gravityMultiplier The new 
+        //! @param gravityMultiplier The new gravity multiplier value.
         virtual void SetGravityMultiplier(float gravityMultiplier) = 0;
 
+        //! Gets the vertical size of the box used when checking for ground contact.
+        virtual float GetGroundDetectionBoxHeight() const = 0;
+
+        //! Sets the vertical size of the box used when checking for ground contact.
+        //! @param groundDetectionBoxHeight The new ground detection box height value.
+        virtual void SetGroundDetectionBoxHeight(float groundDetectionBoxHeight) = 0;
+
         //! Gets the falling velocity.
         virtual AZ::Vector3 GetFallingVelocity() const = 0;
 

+ 29 - 10
Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterController.cpp

@@ -599,24 +599,34 @@ namespace PhysX
         return m_colliderTag;
     }
 
-    void CharacterController::AddVelocity(const AZ::Vector3& velocity)
+    void CharacterController::AddVelocityForTick(const AZ::Vector3& velocity)
     {
-        m_requestedVelocity += velocity;
+        m_requestedVelocityForTick += velocity;
     }
 
-    void CharacterController::ApplyRequestedVelocity(float deltaTime)
+    void CharacterController::AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity)
     {
-        const AZ::Vector3 oldPosition = GetBasePosition();
-        const AZ::Vector3 clampedVelocity = m_requestedVelocity.GetLength() > m_maximumSpeed
-            ? m_maximumSpeed * m_requestedVelocity.GetNormalized()
-            : m_requestedVelocity;
-        const AZ::Vector3 deltaPosition = clampedVelocity * deltaTime;
+        m_requestedVelocityForPhysicsTimestep += velocity;
+    }
+
+    void CharacterController::ResetRequestedVelocityForTick()
+    {
+        m_requestedVelocityForTick = AZ::Vector3::CreateZero();
+    }
 
+    void CharacterController::ResetRequestedVelocityForPhysicsTimestep()
+    {
+        m_requestedVelocityForPhysicsTimestep = AZ::Vector3::CreateZero();
+    }
+
+    void CharacterController::Move(const AZ::Vector3& requestedMovement, float deltaTime)
+    {
         if (m_pxController)
         {
+            const AZ::Vector3 oldPosition = GetBasePosition();
             {
                 PHYSX_SCENE_WRITE_LOCK(m_pxController->getScene());
-                m_pxController->move(PxMathConvert(deltaPosition), m_minimumMovementDistance, deltaTime, m_pxControllerFilters);
+                m_pxController->move(PxMathConvert(requestedMovement), m_minimumMovementDistance, deltaTime, m_pxControllerFilters);
                 if (m_shadowBody)
                 {
                     m_shadowBody->SetKinematicTarget(AZ::Transform::CreateTranslation(GetBasePosition()));
@@ -625,8 +635,17 @@ namespace PhysX
             const AZ::Vector3 newPosition = GetBasePosition();
             m_observedVelocity = deltaTime > 0.0f ? (newPosition - oldPosition) / deltaTime : AZ::Vector3::CreateZero();
         }
+    }
+
+    void CharacterController::ApplyRequestedVelocity(float deltaTime)
+    {
+        const AZ::Vector3 totalRequestedVelocity = m_requestedVelocityForTick + m_requestedVelocityForPhysicsTimestep;
+        const AZ::Vector3 clampedVelocity = totalRequestedVelocity.GetLength() > m_maximumSpeed
+            ? m_maximumSpeed * totalRequestedVelocity.GetNormalized()
+            : totalRequestedVelocity;
+        const AZ::Vector3 deltaPosition = clampedVelocity * deltaTime;
 
-        m_requestedVelocity = AZ::Vector3::CreateZero();
+        Move(deltaPosition, deltaTime);
     }
 
     void CharacterController::SetRotation(const AZ::Quaternion& rotation)

+ 8 - 2
Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterController.h

@@ -191,7 +191,11 @@ namespace PhysX
         void SetCollisionLayer(const AzPhysics::CollisionLayer& layer) override;
         void SetCollisionGroup(const AzPhysics::CollisionGroup& group) override;
         AZ::Crc32 GetColliderTag() const override;
-        void AddVelocity(const AZ::Vector3& velocity) override;
+        void AddVelocityForTick(const AZ::Vector3& velocity) override;
+        void AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity) override;
+        void ResetRequestedVelocityForTick() override;
+        void ResetRequestedVelocityForPhysicsTimestep() override;
+        void Move(const AZ::Vector3& requestedMovement, float deltaTime) override;
         void ApplyRequestedVelocity(float deltaTime) override;
         void SetRotation(const AZ::Quaternion& rotation) override;
         void AttachShape(AZStd::shared_ptr<Physics::Shape> shape) override;
@@ -232,7 +236,9 @@ namespace PhysX
 
         physx::PxController* m_pxController = nullptr; ///< The underlying PhysX controller.
         float m_minimumMovementDistance = 0.0f; ///< To avoid jittering, the controller will not attempt to move distances below this.
-        AZ::Vector3 m_requestedVelocity = AZ::Vector3::CreateZero(); ///< Used to accumulate velocity requests during a tick.
+        AZ::Vector3 m_requestedVelocityForTick = AZ::Vector3::CreateZero(); ///< Used to accumulate velocity requests which last for a tick.
+        AZ::Vector3 m_requestedVelocityForPhysicsTimestep =
+            AZ::Vector3::CreateZero(); ///< Used to accumulate velocity requests which last for a physics timestep.
         AZ::Vector3 m_observedVelocity = AZ::Vector3::CreateZero(); ///< Velocity observed in the simulation, may not match desired.
         PhysX::ActorData m_actorUserData; ///< Used to populate the user data on the PxActor associated with the controller.
         physx::PxFilterData m_filterData; ///< Controls filtering for collisions with other objects and scene queries.

+ 39 - 8
Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp

@@ -198,11 +198,19 @@ namespace PhysX
         return AZ::Vector3::CreateZero();
     }
 
-    void CharacterControllerComponent::AddVelocity(const AZ::Vector3& velocity)
+    void CharacterControllerComponent::AddVelocityForTick(const AZ::Vector3& velocity)
     {
         if (auto* controller = GetController())
         {
-            controller->AddVelocity(velocity);
+            controller->AddVelocityForTick(velocity);
+        }
+    }
+
+    void CharacterControllerComponent::AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity)
+    {
+        if (auto* controller = GetController())
+        {
+            controller->AddVelocityForPhysicsTimestep(velocity);
         }
     }
 
@@ -432,13 +440,22 @@ namespace PhysX
         }
     }
 
-    void CharacterControllerComponent::OnPreSimulate(float deltaTime)
+    void CharacterControllerComponent::OnPostSimulate([[maybe_unused]] float deltaTime)
     {
         if (auto* controller = GetController())
         {
-            controller->ApplyRequestedVelocity(deltaTime);
             const AZ::Vector3 newPosition = controller->GetBasePosition();
             AZ::TransformBus::Event(GetEntityId(), &AZ::TransformBus::Events::SetWorldTranslation, newPosition);
+            controller->ResetRequestedVelocityForTick();
+        }
+    }
+
+    void CharacterControllerComponent::OnSceneSimulationStart(float physicsTimestep)
+    {
+        if (auto* controller = GetController())
+        {
+            controller->ApplyRequestedVelocity(physicsTimestep);
+            controller->ResetRequestedVelocityForPhysicsTimestep();
         }
     }
 
@@ -521,16 +538,29 @@ namespace PhysX
 
         if (m_characterConfig->m_applyMoveOnPhysicsTick)
         {
-            m_preSimulateHandler = AzPhysics::SystemEvents::OnPresimulateEvent::Handler(
+            m_sceneSimulationStartHandler = AzPhysics::SceneEvents::OnSceneSimulationStartHandler(
+                [this](
+                    [[maybe_unused]] AzPhysics::SceneHandle sceneHandle,
+                    float fixedDeltaTime)
+                {
+                    OnSceneSimulationStart(fixedDeltaTime);
+                }, aznumeric_cast<int32_t>(AzPhysics::SceneEvents::PhysicsStartFinishSimulationPriority::Physics));
+
+            m_postSimulateHandler = AzPhysics::SystemEvents::OnPostsimulateEvent::Handler(
                 [this](float deltaTime)
                 {
-                    OnPreSimulate(deltaTime);
+                    OnPostSimulate(deltaTime);
                 }
             );
 
             if (auto* physXSystem = GetPhysXSystem())
             {
-                physXSystem->RegisterPreSimulateEvent(m_preSimulateHandler);
+                physXSystem->RegisterPostSimulateEvent(m_postSimulateHandler);
+            }
+
+            if (sceneInterface != nullptr)
+            {
+                sceneInterface->RegisterSceneSimulationStartHandler(m_attachedSceneHandle, m_sceneSimulationStartHandler);
             }
         }
     }
@@ -554,7 +584,8 @@ namespace PhysX
     {
         m_controllerBodyHandle = AzPhysics::InvalidSimulatedBodyHandle;
         m_attachedSceneHandle = AzPhysics::InvalidSceneHandle;
-        m_preSimulateHandler.Disconnect();
+        m_sceneSimulationStartHandler.Disconnect();
+        m_postSimulateHandler.Disconnect();
         m_onSimulatedBodyRemovedHandler.Disconnect();
         CharacterControllerRequestBus::Handler::BusDisconnect();
     }

+ 6 - 3
Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h

@@ -92,7 +92,8 @@ namespace PhysX
         float GetMaximumSpeed() const override;
         void SetMaximumSpeed(float maximumSpeed) override;
         AZ::Vector3 GetVelocity() const override;
-        void AddVelocity(const AZ::Vector3& velocity) override;
+        void AddVelocityForTick(const AZ::Vector3& velocity) override;
+        void AddVelocityForPhysicsTimestep(const AZ::Vector3& velocity) override;
         bool IsPresent() const override { return IsPhysicsEnabled(); }
         Physics::Character* GetCharacter() override;
 
@@ -137,13 +138,15 @@ namespace PhysX
         // Cleans up all references and events used with the physics character controller.
         void DestroyController();
 
-        void OnPreSimulate(float deltaTime);
+        void OnPostSimulate(float deltaTime);
+        void OnSceneSimulationStart(float physicsTimestep);
 
         AZStd::unique_ptr<Physics::CharacterConfiguration> m_characterConfig;
         AZStd::shared_ptr<Physics::ShapeConfiguration> m_shapeConfig;
         AzPhysics::SimulatedBodyHandle m_controllerBodyHandle = AzPhysics::InvalidSimulatedBodyHandle;
         AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle;
-        AzPhysics::SystemEvents::OnPresimulateEvent::Handler m_preSimulateHandler;
+        AzPhysics::SystemEvents::OnPostsimulateEvent::Handler m_postSimulateHandler;
+        AzPhysics::SceneEvents::OnSceneSimulationStartHandler m_sceneSimulationStartHandler;
         AzPhysics::SceneEvents::OnSimulationBodyRemoved::Handler m_onSimulatedBodyRemovedHandler;
     };
 } // namespace PhysX

+ 124 - 19
Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterGameplayComponent.cpp

@@ -23,6 +23,7 @@ namespace PhysX
             serializeContext->Class<CharacterGameplayConfiguration>()
                 ->Version(1)
                 ->Field("GravityMultiplier", &CharacterGameplayConfiguration::m_gravityMultiplier)
+                ->Field("GroundDetectionBoxHeight", &CharacterGameplayConfiguration::m_groundDetectionBoxHeight)
                 ;
 
             if (auto editContext = serializeContext->GetEditContext())
@@ -33,6 +34,13 @@ namespace PhysX
                     ->DataElement(AZ::Edit::UIHandlers::Default, &CharacterGameplayConfiguration::m_gravityMultiplier,
                         "Gravity Multiplier", "Multiplier for global gravity value that applies only to this character entity.")
                     ->Attribute(AZ::Edit::Attributes::Step, 0.1f)
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &CharacterGameplayConfiguration::m_groundDetectionBoxHeight,
+                        "Ground Detection Box Height",
+                        "Vertical size of box centered on the character's foot position used when testing for ground contact.")
+                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                    ->Attribute(AZ::Edit::Attributes::Step, 0.001f)
                     ;
             }
         }
@@ -60,6 +68,7 @@ namespace PhysX
 
     CharacterGameplayComponent::CharacterGameplayComponent(const CharacterGameplayConfiguration& config)
         : m_gravityMultiplier(config.m_gravityMultiplier)
+        , m_groundDetectionBoxHeight(config.m_groundDetectionBoxHeight)
     {
     }
 
@@ -72,6 +81,7 @@ namespace PhysX
             serializeContext->Class<CharacterGameplayComponent, AZ::Component>()
                 ->Version(1)
                 ->Field("GravityMultiplier", &CharacterGameplayComponent::m_gravityMultiplier)
+                ->Field("GroundDetectionBoxHeight", &CharacterGameplayComponent::m_groundDetectionBoxHeight)
                 ;
         }
 
@@ -83,6 +93,14 @@ namespace PhysX
                 ->Event("IsOnGround", &CharacterGameplayRequests::IsOnGround, "Is On Ground")
                 ->Event("GetGravityMultiplier", &CharacterGameplayRequests::GetGravityMultiplier, "Get Gravity Multiplier")
                 ->Event("SetGravityMultiplier", &CharacterGameplayRequests::SetGravityMultiplier, "Set Gravity Multiplier")
+                ->Event(
+                    "GetGroundDetectionBoxHeight",
+                    &CharacterGameplayRequests::GetGroundDetectionBoxHeight,
+                    "Get Ground Detection Box Height")
+                ->Event(
+                    "SetGroundDetectionBoxHeight",
+                    &CharacterGameplayRequests::SetGroundDetectionBoxHeight,
+                    "Set Ground Detection Box Height")
                 ->Event("GetFallingVelocity", &CharacterGameplayRequests::GetFallingVelocity, "Get Falling Velocity")
                 ->Event("SetFallingVelocity", &CharacterGameplayRequests::SetFallingVelocity, "Set Falling Velocity")
                 ;
@@ -91,25 +109,83 @@ namespace PhysX
 
     // CharacterGameplayRequestBus
     bool CharacterGameplayComponent::IsOnGround() const
+    {
+        if (m_cachedGroundState == CharacterGroundState::NotYetDetermined)
+        {
+            DetermineCachedGroundState();
+        }
+
+        return m_cachedGroundState == CharacterGroundState::Touching;
+    }
+
+    void CharacterGameplayComponent::DetermineCachedGroundState() const
     {
         Physics::Character* character = nullptr;
         Physics::CharacterRequestBus::EventResult(character, GetEntityId(), &Physics::CharacterRequests::GetCharacter);
         if (!character)
         {
-            return true;
+            m_cachedGroundState = CharacterGroundState::Touching;
+            return;
         }
 
         auto pxController = static_cast<physx::PxController*>(character->GetNativePointer());
         if (!pxController)
         {
-            return true;
+            m_cachedGroundState = CharacterGroundState::Touching;
+            return;
         }
 
+        // first check if we can use the character controller state, which should be cheaper than doing a scene query
+
+        // if the controller is slightly above an object or has not been asked to move downwards, the PxController may
+        // not report a touched actor or downward collision, so this can give false negatives, but should not give
+        // false positives, so it's useful as an early out
         physx::PxControllerState state;
         pxController->getState(state);
-        return
-            state.touchedActor != nullptr ||
-            (state.collisionFlags & physx::PxControllerCollisionFlag::eCOLLISION_DOWN) != 0;
+
+        if (state.touchedActor != nullptr || (state.collisionFlags & physx::PxControllerCollisionFlag::eCOLLISION_DOWN) != 0)
+        {
+            m_cachedGroundState = CharacterGroundState::Touching;
+            return;
+        }
+
+        // if we get to this point it's still unclear whether the character is touching the ground so
+        // use an overlap query to see if there's any geometry immediately below the character's foot position
+        if (auto* scene = character->GetScene())
+        {
+            // use a box shape for the overlap, even if the character geometry is a capsule, to avoid difficulties with
+            // the curved base of the capsule
+            AZ::Vector3 footBoxDimensions(0.0f, 0.0f, m_groundDetectionBoxHeight);
+            if (pxController->getType() == physx::PxControllerShapeType::eCAPSULE)
+            {
+                const float radius = static_cast<physx::PxCapsuleController*>(pxController)->getRadius();
+                footBoxDimensions = AZ::Vector3(2.0f * radius, 2.0f * radius, m_groundDetectionBoxHeight);
+            }
+            else if (pxController->getType() == physx::PxControllerShapeType::eBOX)
+            {
+                const auto* boxController = static_cast<physx::PxBoxController*>(pxController);
+                footBoxDimensions = AZ::Vector3(
+                    2.0f * boxController->getHalfSideExtent(), 2.0f * boxController->getHalfForwardExtent(), m_groundDetectionBoxHeight);
+            }
+
+            AZ::Transform footBoxTransform = AZ::Transform::CreateFromQuaternionAndTranslation(
+                AZ::Quaternion::CreateShortestArc(AZ::Vector3::CreateAxisZ(), character->GetUpDirection()), character->GetBasePosition());
+            AzPhysics::OverlapRequest overlapRequest =
+                AzPhysics::OverlapRequestHelpers::CreateBoxOverlapRequest(footBoxDimensions, footBoxTransform);
+            overlapRequest.m_collisionGroup = character->GetCollisionGroup();
+            overlapRequest.m_maxResults = 2;
+            AZ::EntityId entityId = GetEntityId();
+            AzPhysics::SceneQueryHits sceneQueryHits = scene->QueryScene(&overlapRequest);
+            m_cachedGroundState = AZStd::any_of(
+                                      sceneQueryHits.m_hits.begin(),
+                                      sceneQueryHits.m_hits.end(),
+                                      [&entityId](const AzPhysics::SceneQueryHit& hit)
+                                      {
+                                          return hit.m_entityId != entityId;
+                                      })
+                ? CharacterGroundState::Touching
+                : CharacterGroundState::NotTouching;
+        }
     }
 
     float CharacterGameplayComponent::GetGravityMultiplier() const
@@ -122,6 +198,16 @@ namespace PhysX
         m_gravityMultiplier = gravityMultiplier;
     }
 
+    float CharacterGameplayComponent::GetGroundDetectionBoxHeight() const
+    {
+        return m_groundDetectionBoxHeight;
+    }
+
+    void CharacterGameplayComponent::SetGroundDetectionBoxHeight(float groundDetectionBoxHeight)
+    {
+        m_groundDetectionBoxHeight = AZ::GetMax(0.0f, groundDetectionBoxHeight);
+    }
+
     AZ::Vector3 CharacterGameplayComponent::GetFallingVelocity() const
     {
         return m_fallingVelocity;
@@ -142,12 +228,21 @@ namespace PhysX
                 OnGravityChanged(newGravity);
             });
 
-        m_preSimulateHandler = AzPhysics::SystemEvents::OnPresimulateEvent::Handler(
-            [this](float deltaTime)
+        m_sceneSimulationStartHandler = AzPhysics::SceneEvents::OnSceneSimulationStartHandler(
+            [this](
+                [[maybe_unused]] AzPhysics::SceneHandle sceneHandle,
+                float fixedDeltaTime)
             {
-                OnPreSimulate(deltaTime);
-            }
-        );
+                OnSceneSimulationStart(fixedDeltaTime);
+            }, aznumeric_cast<int32_t>(AzPhysics::SceneEvents::PhysicsStartFinishSimulationPriority::Animation));
+
+        m_sceneSimulationFinishHandler = AzPhysics::SceneEvents::OnSceneSimulationStartHandler(
+            [this](
+                [[maybe_unused]] AzPhysics::SceneHandle sceneHandle,
+                [[maybe_unused]] float fixedDeltaTime)
+            {
+                OnSceneSimulationFinish();
+            }, aznumeric_cast<int32_t>(AzPhysics::SceneEvents::PhysicsStartFinishSimulationPriority::Default));
     }
 
     void CharacterGameplayComponent::Activate()
@@ -160,14 +255,18 @@ namespace PhysX
             {
                 m_gravity = sceneInterface->GetGravity(worldBody->m_sceneOwner);
                 sceneInterface->RegisterSceneGravityChangedEvent(worldBody->m_sceneOwner, m_onGravityChangedHandler);
+                AzPhysics::SceneHandle attachedSceneHandle = AzPhysics::InvalidSceneHandle;
+                Physics::DefaultWorldBus::BroadcastResult(attachedSceneHandle, &Physics::DefaultWorldRequests::GetDefaultSceneHandle);
+                if (attachedSceneHandle == AzPhysics::InvalidSceneHandle)
+                {
+                    AZ_Error("PhysX Character Controller Component", false, "Failed to retrieve default scene.");
+                    return;
+                }
+                sceneInterface->RegisterSceneSimulationStartHandler(attachedSceneHandle, m_sceneSimulationStartHandler);
+                sceneInterface->RegisterSceneSimulationFinishHandler(attachedSceneHandle, m_sceneSimulationFinishHandler);
             }
         }
 
-        if (auto* physXSystem = GetPhysXSystem())
-        {
-            physXSystem->RegisterPreSimulateEvent(m_preSimulateHandler);
-        }
-
         CharacterGameplayRequestBus::Handler::BusConnect(GetEntityId());
     }
 
@@ -175,13 +274,19 @@ namespace PhysX
     {
         CharacterGameplayRequestBus::Handler::BusDisconnect();
         m_onGravityChangedHandler.Disconnect();
-        m_preSimulateHandler.Disconnect();
+        m_sceneSimulationStartHandler.Disconnect();
+        m_sceneSimulationFinishHandler.Disconnect();
     }
 
     // Physics::SystemEvent
-    void CharacterGameplayComponent::OnPreSimulate(float deltaTime)
+    void CharacterGameplayComponent::OnSceneSimulationStart(float physicsTimestep)
+    {
+        ApplyGravity(physicsTimestep);
+    }
+
+    void CharacterGameplayComponent::OnSceneSimulationFinish()
     {
-        ApplyGravity(deltaTime);
+        m_cachedGroundState = CharacterGroundState::NotYetDetermined;
     }
 
     void CharacterGameplayComponent::OnGravityChanged(const AZ::Vector3& gravity)
@@ -205,6 +310,6 @@ namespace PhysX
         }
 
         m_fallingVelocity += m_gravityMultiplier * m_gravity * deltaTime;
-        Physics::CharacterRequestBus::Event(GetEntityId(), &Physics::CharacterRequests::AddVelocity, m_fallingVelocity);
+        Physics::CharacterRequestBus::Event(GetEntityId(), &Physics::CharacterRequests::AddVelocityForPhysicsTimestep, m_fallingVelocity);
     }
 } // namespace PhysX

+ 18 - 2
Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterGameplayComponent.h

@@ -14,6 +14,14 @@
 
 namespace PhysX
 {
+    //! Used for caching whether the character controller is touching the ground.
+    enum class CharacterGroundState
+    {
+        NotYetDetermined,
+        NotTouching,
+        Touching,
+    };
+
     //! Configuration for storing character gameplay settings.
     class CharacterGameplayConfiguration
     {
@@ -23,6 +31,7 @@ namespace PhysX
         static void Reflect(AZ::ReflectContext* context);
 
         float m_gravityMultiplier = 1.0f; //!< Multiplier to be combined with world gravity setting when applying gravity to character.
+        float m_groundDetectionBoxHeight = 0.02f; //!< Vertical size of box to use when testing for ground contact.
     };
 
     //! Character Gameplay Component.
@@ -54,6 +63,8 @@ namespace PhysX
         bool IsOnGround() const override;
         float GetGravityMultiplier() const override;
         void SetGravityMultiplier(float gravityMultiplier) override;
+        float GetGroundDetectionBoxHeight() const override;
+        void SetGroundDetectionBoxHeight(float groundDetectionBoxHeight) override;
         AZ::Vector3 GetFallingVelocity() const override;
         void SetFallingVelocity(const AZ::Vector3& fallingVelocity) override;
 
@@ -64,15 +75,20 @@ namespace PhysX
         void Deactivate() override;
 
     private:
-        void OnPreSimulate(float deltaTime);
+        void OnSceneSimulationStart(float physicsTimestep);
+        void OnSceneSimulationFinish();
         void OnGravityChanged(const AZ::Vector3& gravity);
         void ApplyGravity(float deltaTime);
+        void DetermineCachedGroundState() const;
 
         float m_gravityMultiplier = 1.0f;
         AZ::Vector3 m_gravity = AZ::Vector3::CreateZero();
         AZ::Vector3 m_fallingVelocity = AZ::Vector3::CreateZero();
+        float m_groundDetectionBoxHeight = 0.02f; //!< Vertical size of box to use when testing for ground contact.
 
-        AzPhysics::SystemEvents::OnPresimulateEvent::Handler m_preSimulateHandler;
+        AzPhysics::SceneEvents::OnSceneSimulationStartHandler m_sceneSimulationStartHandler;
+        AzPhysics::SceneEvents::OnSceneSimulationFinishHandler m_sceneSimulationFinishHandler;
         AzPhysics::SceneEvents::OnSceneGravityChangedEvent::Handler m_onGravityChangedHandler;
+        mutable CharacterGroundState m_cachedGroundState = CharacterGroundState::NotYetDetermined;
     };
 } // namespace PhysX

+ 2 - 2
Gems/PhysX/Code/Source/Scene/PhysXScene.cpp

@@ -542,7 +542,7 @@ namespace PhysX
 
         {
             AZ_PROFILE_SCOPE(Physics, "OnSceneSimulationStartEvent::Signaled");
-            m_sceneSimuationStartEvent.Signal(m_sceneHandle, deltatime);
+            m_sceneSimulationStartEvent.Signal(m_sceneHandle, deltatime);
         }
 
         m_currentDeltaTime = deltatime;
@@ -607,7 +607,7 @@ namespace PhysX
 
         {
             AZ_PROFILE_SCOPE(Physics, "OnSceneSimulationFinishedEvent::Signaled");
-            m_sceneSimuationFinishEvent.Signal(m_sceneHandle, m_currentDeltaTime);
+            m_sceneSimulationFinishEvent.Signal(m_sceneHandle, m_currentDeltaTime);
         }
 
         UpdateAzProfilerDataPoints();