Bladeren bron

CharacterVirtual stick to floor / stair walk did not trigger a contact added callback on the CharacterContactListener (#1205)

Jorrit Rouwe 1 jaar geleden
bovenliggende
commit
33394b357f

+ 1 - 0
Docs/ReleaseNotes.md

@@ -56,6 +56,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Suppress GCC warning: 'XXX' may be used uninitialized in this function [-Werror=maybe-uninitialized].
 * Fixed compile errors when compiling with GCC for the ARM platform.
 * When calling CharacterVirtual::SetShape, a collision with a sensor would cause the function to abort as if the character was in collision.
+* CharacterVirtual stick to floor / stair walk did not trigger a contact added callback on the CharacterContactListener.
 * Fixed bug where the the skinned position of a soft body would update in the first sub-iteration, causing a large velocity spike and jittery behavior.
 * Fixed bug where the velocity of soft body vertices would increase indefinitely when resting on the back stop of a skinned constraint.
 * Fixed bug when SkinVertices for a soft body is not called every frame, the previous position of the skin was still used causing a replay of the motion of the previous frame.

+ 16 - 7
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -481,6 +481,17 @@ bool CharacterVirtual::ValidateContact(const Contact &inContact) const
 		return mListener->OnContactValidate(this, inContact.mBodyB, inContact.mSubShapeIDB);
 }
 
+void CharacterVirtual::ContactAdded(const Contact &inContact, CharacterContactSettings &ioSettings) const
+{
+	if (mListener != nullptr)
+	{
+		if (inContact.mCharacterB != nullptr)
+			mListener->OnCharacterContactAdded(this, inContact.mCharacterB, inContact.mSubShapeIDB, inContact.mPosition, -inContact.mContactNormal, ioSettings);
+		else
+			mListener->OnContactAdded(this, inContact.mBodyB, inContact.mSubShapeIDB, inContact.mPosition, -inContact.mContactNormal, ioSettings);
+	}
+}
+
 template <class T>
 inline static bool sCorrectFractionForCharacterPadding(const Shape *inShape, Mat44Arg inStart, Vec3Arg inDisplacement, const T &inPolygon, float &ioFraction)
 {
@@ -646,13 +657,7 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 
 	// Send contact added event
 	CharacterContactSettings settings;
-	if (mListener != nullptr)
-	{
-		if (contact.mCharacterB != nullptr)
-			mListener->OnCharacterContactAdded(this, contact.mCharacterB, contact.mSubShapeIDB, contact.mPosition, -contact.mContactNormal, settings);
-		else
-			mListener->OnContactAdded(this, contact.mBodyB, contact.mSubShapeIDB, contact.mPosition, -contact.mContactNormal, settings);
-	}
+	ContactAdded(contact, settings);
 	contact.mCanPushCharacter = settings.mCanPushCharacter;
 
 	// We don't have any further interaction with sensors beyond an OnContactAdded notification
@@ -1331,6 +1336,10 @@ void CharacterVirtual::MoveToContact(RVec3Arg inPosition, const Contact &inConta
 	// Set the new position
 	SetPosition(inPosition);
 
+	// Trigger contact added callback
+	CharacterContactSettings dummy;
+	ContactAdded(inContact, dummy);
+
 	// Determine the contacts
 	TempContactList contacts(inAllocator);
 	contacts.reserve(mMaxNumHits + 1); // +1 because we can add one extra below

+ 3 - 0
Jolt/Physics/Character/CharacterVirtual.h

@@ -504,6 +504,9 @@ private:
 	// Ask the callback if inContact is a valid contact point
 	bool								ValidateContact(const Contact &inContact) const;
 
+	// Trigger the contact callback for inContact and get the contact settings
+	void								ContactAdded(const Contact &inContact, CharacterContactSettings &ioSettings) const;
+
 	// Tests the shape for collision around inPosition
 	void								GetContactsAtPosition(RVec3Arg inPosition, Vec3Arg inMovementDirection, const Shape *inShape, TempContactList &outContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter) const;
 

+ 147 - 0
UnitTests/LoggingCharacterContactListener.h

@@ -0,0 +1,147 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Character/CharacterVirtual.h>
+
+// Character contact listener that just logs the calls made to it for later validation
+class LoggingCharacterContactListener : public CharacterContactListener
+{
+public:
+	// Contact callback type
+	enum class EType
+	{
+		ValidateBody,
+		ValidateCharacter,
+		AddBody,
+		AddCharacter,
+	};
+
+	// Entry written when a contact callback happens
+	struct LogEntry
+	{
+		EType						mType;
+		const CharacterVirtual *	mCharacter;
+		BodyID						mBody2;
+		const CharacterVirtual *	mCharacter2;
+		SubShapeID					mSubShapeID2;
+	};
+
+	virtual bool					OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) override
+	{
+		mLog.push_back({ EType::ValidateBody, inCharacter, inBodyID2, nullptr, inSubShapeID2 });
+		return true;
+	}
+
+	virtual bool					OnCharacterContactValidate(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2) override
+	{
+		mLog.push_back({ EType::ValidateCharacter, inCharacter, BodyID(), inOtherCharacter, inSubShapeID2 });
+		return true;
+	}
+
+	virtual void					OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		mLog.push_back({ EType::AddBody, inCharacter, inBodyID2, nullptr, inSubShapeID2 });
+	}
+
+	virtual void					OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+	{
+		mLog.push_back({ EType::AddCharacter, inCharacter, BodyID(), inOtherCharacter, inSubShapeID2 });
+	}
+
+	void							Clear()
+	{
+		mLog.clear();
+	}
+
+	size_t							GetEntryCount() const
+	{
+		return mLog.size();
+	}
+
+	const LogEntry &				GetEntry(size_t inIdx) const
+	{
+		return mLog[inIdx];
+	}
+
+	// Find first event with a particular type and involving a particular character vs body
+	int								Find(EType inType, const CharacterVirtual *inCharacter, const BodyID &inBody2) const
+	{
+		for (size_t i = 0; i < mLog.size(); ++i)
+		{
+			const LogEntry &e = mLog[i];
+			if (e.mType == inType && e.mCharacter == inCharacter && e.mBody2 == inBody2 && e.mCharacter2 == nullptr)
+				return int(i);
+		}
+
+		return -1;
+	}
+
+	// Check if event with a particular type and involving a particular character vs body
+	bool							Contains(EType inType, const CharacterVirtual *inCharacter, const BodyID &inBody2) const
+	{
+		return Find(inType, inCharacter, inBody2) >= 0;
+	}
+
+	// Find first event with a particular type and involving a particular character vs character
+	int								Find(EType inType, const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter) const
+	{
+		for (size_t i = 0; i < mLog.size(); ++i)
+		{
+			const LogEntry &e = mLog[i];
+			if (e.mType == inType && e.mCharacter == inCharacter && e.mBody2.IsInvalid() && e.mCharacter2 == inOtherCharacter)
+				return int(i);
+		}
+
+		return -1;
+	}
+
+	// Check if event with a particular type and involving a particular character vs character
+	bool							Contains(EType inType, const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter) const
+	{
+		return Find(inType, inCharacter, inOtherCharacter) >= 0;
+	}
+
+	// Find first event with a particular type and involving a particular character vs body and sub shape ID
+	int								Find(EType inType, const CharacterVirtual *inCharacter, const BodyID &inBody2, const SubShapeID &inSubShapeID2) const
+	{
+		for (size_t i = 0; i < mLog.size(); ++i)
+		{
+			const LogEntry &e = mLog[i];
+			if (e.mType == inType && e.mCharacter == inCharacter && e.mBody2 == inBody2 && e.mCharacter2 == nullptr && e.mSubShapeID2 == inSubShapeID2)
+				return int(i);
+		}
+
+		return -1;
+	}
+
+	// Check if a particular type and involving a particular character vs body and sub shape ID exists
+	bool							Contains(EType inType, const CharacterVirtual *inCharacter, const BodyID &inBody2, const SubShapeID &inSubShapeID2) const
+	{
+		return Find(inType, inCharacter, inBody2, inSubShapeID2) >= 0;
+	}
+
+	// Find first event with a particular type and involving a particular character vs character and sub shape ID
+	int								Find(EType inType, const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2) const
+	{
+		for (size_t i = 0; i < mLog.size(); ++i)
+		{
+			const LogEntry &e = mLog[i];
+			if (e.mType == inType && e.mCharacter == inCharacter && e.mBody2.IsInvalid() && e.mCharacter2 == inOtherCharacter && e.mSubShapeID2 == inSubShapeID2)
+				return int(i);
+		}
+
+		return -1;
+	}
+
+	// Check if a particular type and involving a particular character vs character and sub shape ID exists
+	bool							Contains(EType inType, const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2) const
+	{
+		return Find(inType, inCharacter, inOtherCharacter, inSubShapeID2) >= 0;
+	}
+
+private:
+	Array<LogEntry>					mLog;
+};

+ 59 - 9
UnitTests/Physics/CharacterVirtualTests.cpp

@@ -4,6 +4,7 @@
 
 #include "UnitTestFramework.h"
 #include "PhysicsTestContext.h"
+#include "LoggingCharacterContactListener.h"
 #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Physics/Collision/Shape/MeshShape.h>
@@ -138,8 +139,31 @@ TEST_SUITE("CharacterVirtualTests")
 		// Calculated effective velocity after a step
 		Vec3					mEffectiveVelocity = Vec3::sZero();
 
+		// Log of contact events
+		LoggingCharacterContactListener mContactLog;
+
 	private:
 		// CharacterContactListener callback
+		virtual bool			OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) override
+		{
+			return mContactLog.OnContactValidate(inCharacter, inBodyID2, inSubShapeID2);
+		}
+
+		virtual bool			OnCharacterContactValidate(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2) override
+		{
+			return mContactLog.OnCharacterContactValidate(inCharacter, inOtherCharacter, inSubShapeID2);
+		}
+
+		virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+		{
+			mContactLog.OnContactAdded(inCharacter, inBodyID2, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
+		}
+
+		virtual void			OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override
+		{
+			mContactLog.OnCharacterContactAdded(inCharacter, inOtherCharacter, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
+		}
+
 		virtual void			OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity) override
 		{
 			// Don't allow sliding if the character doesn't want to move
@@ -283,7 +307,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Create sloped floor
 			PhysicsTestContext c;
 			Quat slope_rotation = Quat::sRotation(Vec3::sAxisZ(), cSlopeAngle);
-			c.CreateBox(RVec3::sZero(), slope_rotation, EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100.0f, cFloorHalfHeight, 100.0f));
+			Body &floor = c.CreateBox(RVec3::sZero(), slope_rotation, EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100.0f, cFloorHalfHeight, 100.0f));
 
 			// Create character so that it is touching the slope
 			Character character(c);
@@ -295,16 +319,45 @@ TEST_SUITE("CharacterVirtualTests")
 			// After 1 step we should be on the slope
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
+			CHECK(character.mContactLog.GetEntryCount() == 2);
+			CHECK(character.mContactLog.Contains(LoggingCharacterContactListener::EType::ValidateBody, character.mCharacter, floor.GetID()));
+			CHECK(character.mContactLog.Contains(LoggingCharacterContactListener::EType::AddBody, character.mCharacter, floor.GetID()));
+			character.mContactLog.Clear();
 
 			// Cancel any velocity to make the calculation below easier (otherwise we have to take gravity for 1 time step into account)
 			character.mCharacter->SetLinearVelocity(Vec3::sZero());
 
 			RVec3 start_pos = character.GetPosition();
 
-			// Start moving down the slope at a speed high enough so that gravity will not keep us on the floor
-			character.mHorizontalSpeed = Vec3(-10.0f, 0, 0);
-			character.Simulate(cMovementTime);
-			CHECK(character.mCharacter->GetGroundState() == (stick_to_floor? CharacterBase::EGroundState::OnGround : CharacterBase::EGroundState::InAir));
+			float time_step = c.GetDeltaTime();
+			int num_steps = (int)round(cMovementTime / time_step);
+
+			for (int i = 0; i < num_steps; ++i)
+			{
+				// Start moving down the slope at a speed high enough so that gravity will not keep us on the floor
+				character.mHorizontalSpeed = Vec3(-10.0f, 0, 0);
+				character.Step();
+
+				if (stick_to_floor)
+				{
+					// Should stick to floor
+					CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
+
+					// Should have received callbacks
+					CHECK(character.mContactLog.GetEntryCount() == 2);
+					CHECK(character.mContactLog.Contains(LoggingCharacterContactListener::EType::ValidateBody, character.mCharacter, floor.GetID()));
+					CHECK(character.mContactLog.Contains(LoggingCharacterContactListener::EType::AddBody, character.mCharacter, floor.GetID()));
+					character.mContactLog.Clear();
+				}
+				else
+				{
+					// Should be off ground
+					CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::InAir);
+
+					// No callbacks
+					CHECK(character.mContactLog.GetEntryCount() == 0);
+				}
+			}
 
 			// Calculate resulting translation
 			Vec3 translation = Vec3(character.GetPosition() - start_pos);
@@ -319,13 +372,10 @@ TEST_SUITE("CharacterVirtualTests")
 			}
 			else
 			{
-				Vec3 gravity = c.GetSystem()->GetGravity();
-				float time_step = c.GetDeltaTime();
-
 				// If too steep, we're just falling. Integrate using an Euler integrator.
 				Vec3 velocity = character.mHorizontalSpeed;
 				expected_translation = Vec3::sZero();
-				int num_steps = (int)round(cMovementTime / time_step);
+				Vec3 gravity = c.GetSystem()->GetGravity();
 				for (int i = 0; i < num_steps; ++i)
 				{
 					velocity += gravity * time_step;