Browse Source

Added 'is first step' and 'is last step' to the PhysicsStepListener (#1284)

PhysicsStepListener::OnStep now receives a single PhysicsStepListenerContext parameter, making it easier to extend parameters later without breaking the API.
Jorrit Rouwe 10 months ago
parent
commit
8153cd854c

+ 11 - 1
Jolt/Physics/PhysicsStepListener.h

@@ -8,6 +8,16 @@ JPH_NAMESPACE_BEGIN
 
 class PhysicsSystem;
 
+/// Context information for the step listener
+class JPH_EXPORT PhysicsStepListenerContext
+{
+public:
+	float					mDeltaTime;								///< Delta time of the current step
+	bool					mIsFirstStep;							///< True if this is the first step
+	bool					mIsLastStep;							///< True if this is the last step
+	PhysicsSystem *			mPhysicsSystem;							///< The physics system that is being stepped
+};
+
 /// A listener class that receives a callback before every physics simulation step
 class JPH_EXPORT PhysicsStepListener
 {
@@ -21,7 +31,7 @@ public:
 	/// The best way to do this is to have each step listener operate on a subset of the bodies and constraints
 	/// and making sure that these bodies and constraints are not touched by any other step listener.
 	/// Note that this function is not called if there aren't any active bodies or when the physics system is updated with 0 delta time.
-	virtual void			OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem) = 0;
+	virtual void			OnStep(const PhysicsStepListenerContext &inContext) = 0;
 };
 
 JPH_NAMESPACE_END

+ 7 - 2
Jolt/Physics/PhysicsSystem.cpp

@@ -626,7 +626,12 @@ void PhysicsSystem::JobStepListeners(PhysicsUpdateContext::Step *ioStep)
 	BodyManager::GrantActiveBodiesAccess grant_active(true, false);
 #endif
 
-	float step_time = ioStep->mContext->mStepDeltaTime;
+	PhysicsStepListenerContext context;
+	context.mDeltaTime = ioStep->mContext->mStepDeltaTime;
+	context.mIsFirstStep = ioStep->mIsFirst;
+	context.mIsLastStep = ioStep->mIsLast;
+	context.mPhysicsSystem = this;
+
 	uint32 batch_size = mPhysicsSettings.mStepListenersBatchSize;
 	for (;;)
 	{
@@ -637,7 +642,7 @@ void PhysicsSystem::JobStepListeners(PhysicsUpdateContext::Step *ioStep)
 
 		// Call the listeners
 		for (uint32 i = batch, i_end = min((uint32)mStepListeners.size(), batch + batch_size); i < i_end; ++i)
-			mStepListeners[i]->OnStep(step_time, *this);
+			mStepListeners[i]->OnStep(context);
 	}
 }
 

+ 11 - 11
Jolt/Physics/Vehicle/VehicleConstraint.cpp

@@ -157,13 +157,13 @@ RMat44 VehicleConstraint::GetWheelWorldTransform(uint inWheelIndex, Vec3Arg inWh
 	return mBody->GetWorldTransform() * GetWheelLocalTransform(inWheelIndex, inWheelRight, inWheelUp);
 }
 
-void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem)
+void VehicleConstraint::OnStep(const PhysicsStepListenerContext &inContext)
 {
 	JPH_PROFILE_FUNCTION();
 
 	// Callback to higher-level systems. We do it before PreCollide, in case steering changes.
 	if (mPreStepCallback != nullptr)
-		mPreStepCallback(*this, inDeltaTime, inPhysicsSystem);
+		mPreStepCallback(*this, inContext);
 
 	if (mIsGravityOverridden)
 	{
@@ -181,11 +181,11 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 	else
 	{
 		// Calculate new world up vector by inverting gravity
-		mWorldUp = (-inPhysicsSystem.GetGravity()).NormalizedOr(mWorldUp);
+		mWorldUp = (-inContext.mPhysicsSystem->GetGravity()).NormalizedOr(mWorldUp);
 	}
 
 	// Callback on our controller
-	mController->PreCollide(inDeltaTime, inPhysicsSystem);
+	mController->PreCollide(inContext.mDeltaTime, *inContext.mPhysicsSystem);
 
 	// Calculate if this constraint is active by checking if our main vehicle body is active or any of the bodies we touch are active
 	mIsActive = mBody->IsActive();
@@ -213,7 +213,7 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 			if (!w->mContactBodyID.IsInvalid())
 			{
 				// Test if the body is still valid
-				w->mContactBody = inPhysicsSystem.GetBodyLockInterfaceNoLock().TryGetBody(w->mContactBodyID);
+				w->mContactBody = inContext.mPhysicsSystem->GetBodyLockInterfaceNoLock().TryGetBody(w->mContactBodyID);
 				if (w->mContactBody == nullptr)
 				{
 					// It's not, forget the contact
@@ -224,7 +224,7 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 				else
 				{
 					// Extrapolate the wheel contact properties
-					mVehicleCollisionTester->PredictContactProperties(inPhysicsSystem, *this, wheel_index, ws_origin, ws_direction, mBody->GetID(), w->mContactBody, w->mContactSubShapeID, w->mContactPosition, w->mContactNormal, w->mSuspensionLength);
+					mVehicleCollisionTester->PredictContactProperties(*inContext.mPhysicsSystem, *this, wheel_index, ws_origin, ws_direction, mBody->GetID(), w->mContactBody, w->mContactSubShapeID, w->mContactPosition, w->mContactNormal, w->mSuspensionLength);
 				}
 			}
 		}
@@ -237,7 +237,7 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 			w->mSuspensionLength = settings->mSuspensionMaxLength;
 
 			// Test collision to find the floor
-			if (mVehicleCollisionTester->Collide(inPhysicsSystem, *this, wheel_index, ws_origin, ws_direction, mBody->GetID(), w->mContactBody, w->mContactSubShapeID, w->mContactPosition, w->mContactNormal, w->mSuspensionLength))
+			if (mVehicleCollisionTester->Collide(*inContext.mPhysicsSystem, *this, wheel_index, ws_origin, ws_direction, mBody->GetID(), w->mContactBody, w->mContactSubShapeID, w->mContactPosition, w->mContactNormal, w->mSuspensionLength))
 			{
 				// Store ID (pointer is not valid outside of the simulation step)
 				w->mContactBodyID = w->mContactBody->GetID();
@@ -278,7 +278,7 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 
 	// Callback to higher-level systems. We do it immediately after wheel collision.
 	if (mPostCollideCallback != nullptr)
-		mPostCollideCallback(*this, inDeltaTime, inPhysicsSystem);
+		mPostCollideCallback(*this, inContext);
 
 	// Calculate anti-rollbar impulses
 	for (const VehicleAntiRollBar &r : mAntiRollBars)
@@ -290,7 +290,7 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 		{
 			// Calculate the impulse to apply based on the difference in suspension length
 			float difference = rw->mSuspensionLength - lw->mSuspensionLength;
-			float impulse = difference * r.mStiffness * inDeltaTime;
+			float impulse = difference * r.mStiffness * inContext.mDeltaTime;
 			lw->mAntiRollBarImpulse = -impulse;
 			rw->mAntiRollBarImpulse = impulse;
 		}
@@ -302,11 +302,11 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 	}
 
 	// Callback on our controller
-	mController->PostCollide(inDeltaTime, inPhysicsSystem);
+	mController->PostCollide(inContext.mDeltaTime, *inContext.mPhysicsSystem);
 
 	// Callback to higher-level systems. We do it before the sleep section, in case velocities change.
 	if (mPostStepCallback != nullptr)
-		mPostStepCallback(*this, inDeltaTime, inPhysicsSystem);
+		mPostStepCallback(*this, inContext);
 
 	// If the wheels are rotating, we don't want to go to sleep yet
 	bool allow_sleep = mController->AllowSleep();

+ 2 - 2
Jolt/Physics/Vehicle/VehicleConstraint.h

@@ -88,7 +88,7 @@ public:
 	const CombineFunction &		GetCombineFriction() const					{ return mCombineFriction; }
 
 	/// Callback function to notify of current stage in PhysicsStepListener::OnStep.
-	using StepCallback = function<void(VehicleConstraint &inVehicle, float inDeltaTime, PhysicsSystem &inPhysicsSystem)>;
+	using StepCallback = function<void(VehicleConstraint &inVehicle, const PhysicsStepListenerContext &inContext)>;
 
 	/// Callback function to notify that PhysicsStepListener::OnStep has started for this vehicle. Default is to do nothing.
 	/// Can be used to allow higher-level code to e.g. control steering. This is the last moment that the position/orientation of the vehicle can be changed.
@@ -196,7 +196,7 @@ public:
 
 private:
 	// See: PhysicsStepListener
-	virtual void				OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem) override;
+	virtual void				OnStep(const PhysicsStepListenerContext &inContext) override;
 
 	// Calculate the position where the suspension and traction forces should be applied in world space, relative to the center of mass of both bodies
 	void						CalculateSuspensionForcePoint(const Wheel &inWheel, Vec3 &outR1PlusU, Vec3 &outR2) const;

+ 4 - 4
Samples/Tests/Character/CharacterPlanetTest.cpp

@@ -170,19 +170,19 @@ void CharacterPlanetTest::RestoreInputState(StateRecorder &inStream)
 	inStream.Read(mJump);
 }
 
-void CharacterPlanetTest::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem)
+void CharacterPlanetTest::OnStep(const PhysicsStepListenerContext &inContext)
 {
 	// Use the length of the global gravity vector
-	float gravity = inPhysicsSystem.GetGravity().Length();
+	float gravity = inContext.mPhysicsSystem->GetGravity().Length();
 
 	// We don't need to lock the bodies since they're already locked in the OnStep callback.
 	// Note that this means we're responsible for avoiding race conditions with other step listeners while accessing bodies.
 	// We know that this is safe because in this demo there's only one step listener.
-	const BodyLockInterface &body_interface = inPhysicsSystem.GetBodyLockInterfaceNoLock();
+	const BodyLockInterface &body_interface = inContext.mPhysicsSystem->GetBodyLockInterfaceNoLock();
 
 	// Loop over all active bodies
 	BodyIDVector body_ids;
-	inPhysicsSystem.GetActiveBodies(EBodyType::RigidBody, body_ids);
+	inContext.mPhysicsSystem->GetActiveBodies(EBodyType::RigidBody, body_ids);
 	for (const BodyID &id : body_ids)
 	{
 		BodyLockWrite lock(body_interface, id);

+ 1 - 1
Samples/Tests/Character/CharacterPlanetTest.h

@@ -38,7 +38,7 @@ public:
 	virtual void			RestoreInputState(StateRecorder &inStream) override;
 
 	// See: PhysicsStepListener
-	virtual void			OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem) override;
+	virtual void			OnStep(const PhysicsStepListenerContext &inContext) override;
 
 	// See: CharacterContactListener
 	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;

+ 22 - 4
UnitTests/Physics/PhysicsStepListenerTests.cpp

@@ -13,14 +13,16 @@ TEST_SUITE("StepListenerTest")
 	class TestStepListener : public PhysicsStepListener
 	{
 	public:
-		virtual void			OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem) override
+		virtual void			OnStep(const PhysicsStepListenerContext &inContext) override
 		{
-			CHECK(inDeltaTime == mExpectedDeltaTime);
-
-			++mCount;
+			CHECK(inContext.mDeltaTime == mExpectedDeltaTime);
+			CHECK(inContext.mIsFirstStep == ((mCount % mExpectedSteps) == 0));
+			int new_count = mCount.fetch_add(1) + 1;
+			CHECK(inContext.mIsLastStep == ((new_count % mExpectedSteps) == 0));
 		}
 
 		atomic<int>				mCount = 0;
+		int						mExpectedSteps;
 		float					mExpectedDeltaTime = 0.0f;
 	};
 
@@ -32,7 +34,10 @@ TEST_SUITE("StepListenerTest")
 		// Initialize and add listeners
 		TestStepListener listeners[10];
 		for (TestStepListener &l : listeners)
+		{
 			l.mExpectedDeltaTime = 1.0f / 60.0f / inCollisionSteps;
+			l.mExpectedSteps = inCollisionSteps;
+		}
 		for (TestStepListener &l : listeners)
 			c.GetSystem()->AddStepListener(&l);
 
@@ -61,6 +66,13 @@ TEST_SUITE("StepListenerTest")
 		// Unregister all listeners
 		for (TestStepListener &l : listeners)
 			c.GetSystem()->RemoveStepListener(&l);
+
+		// Step the simulation
+		c.SimulateSingleStep();
+
+		// Check that no further callbacks were triggered
+		for (TestStepListener &l : listeners)
+			CHECK(l.mCount == 2 * inCollisionSteps);
 	}
 
 	// Test the step listeners with a single collision step
@@ -74,4 +86,10 @@ TEST_SUITE("StepListenerTest")
 	{
 		DoTest(2);
 	}
+
+	// Test the step listeners with four collision steps
+	TEST_CASE("TestStepListener4")
+	{
+		DoTest(4);
+	}
 }