Browse Source

Added interface for CharacterVirtual vs CharacterVirtual collision (#1157)

Jorrit Rouwe 1 year ago
parent
commit
f0f19df1ca

+ 7 - 0
Docs/ReleaseNotes.md

@@ -20,10 +20,12 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added HeightFieldShape::GetMinHeightValue/GetMaxHeightValue that can be used to know which range of heights are accepted by SetHeights.
 * Allowing negative stride when getting/setting height field shape heights or materials. This improves performance if your data happens to be layed out the wrong way around.
 * Added HeightFieldShapeSettings::mMaterialsCapacity which can enlarge the internal materials array capacity to avoid resizing when HeightFieldShape::SetMaterials is called with materials that weren't in use by the height field yet.
+* Added Clone function to HeightFieldShape. This allows creating a copy before modifying the shape.
 
 #### Character
 
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
+* Added ability for a CharacterVirtual to collide with another CharacterVirtual by using the new CharacterVsCharacterCollision interface.
 
 #### Vehicles
 
@@ -39,6 +41,9 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * The OVERRIDE_CXX_FLAGS cmake flag will now also work for MSVC and allow you to specify your own CMAKE_CXX_FLAGS_DEBUG/CMAKE_CXX_FLAGS_RELEASE flags
 * BodyInterface::AddForce/Torque functions now take an optional EActivation parameter that makes it optional to activate the body. This can be used e.g. to not let the body wake up if you're applying custom gravity to a body.
 * Activating bodies now resets the sleep timer when the body is already active. This prevents the body from going to sleep in the next frame and can avoid quick 1 frame naps.
+* Added Clone function to MutableCompoundShape. This allows creating a copy before modifying the shape.
+* QuadTree / FixedSizeFreeList: Reorder variable layout to reduce false sharing & thread syncs to reduce simulation time by approximately 5%.
+* Generate a CMake config file when the project is installed. Allows for other projects to import Jolt using the find_package() functionality.
 
 ### Bug fixes
 
@@ -54,6 +59,8 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Fixed -Wunused-parameter warning on GCC when building in Release mode with -Wextra.
 * Fixed tolerance in assert in GetPenetrationDepthStepEPA.
 * Due to a difference between the used instructions in NEON and SSE -Vec3::sZero() returned different binary results on ARM vs x86. When JPH_CROSS_PLATFORM_DETERMINISTIC is defined, we ensure that the calculation is the same now.
+* Forgot to free a temporary allocation on an early out in HeightFieldShape::SetMaterials.
+* Fix SSE not being enabled on x86 32-bits.
 
 ## v5.0.0
 

+ 1 - 1
Jolt/Jolt.cmake

@@ -624,7 +624,7 @@ else()
   		# On 32-bit builds we need to default to using SSE instructions, the x87 FPU instructions have higher intermediate precision
 		# which will cause problems in the collision detection code (the effect is similar to leaving FMA on, search for
 		# JPH_PRECISE_MATH_ON for the locations where this is a problem).
-  
+
 		if (USE_AVX512)
 			target_compile_options(Jolt PUBLIC -mavx512f -mavx512vl -mavx512dq -mavx2 -mbmi -mpopcnt -mlzcnt -mf16c)
 		elseif (USE_AVX2)

+ 167 - 19
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -20,6 +20,68 @@
 
 JPH_NAMESPACE_BEGIN
 
+void CharacterVsCharacterCollisionSimple::Remove(const CharacterVirtual *inCharacter)
+{
+	Array<CharacterVirtual *>::iterator i = std::find(mCharacters.begin(), mCharacters.end(), inCharacter);
+	if (i != mCharacters.end())
+		mCharacters.erase(i);
+}
+
+void CharacterVsCharacterCollisionSimple::CollideCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, const CollideShapeSettings &inCollideShapeSettings, RVec3Arg inBaseOffset, CollideShapeCollector &ioCollector) const
+{
+	// Make shape 1 relative to inBaseOffset
+	Mat44 transform1 = inCenterOfMassTransform.PostTranslated(-inBaseOffset).ToMat44();
+
+	const Shape *shape = inCharacter->GetShape();
+	CollideShapeSettings settings = inCollideShapeSettings;
+
+	// Iterate over all characters
+	for (const CharacterVirtual *c : mCharacters)
+		if (c != inCharacter
+			&& !ioCollector.ShouldEarlyOut())
+		{
+			// Collector needs to know which character we're colliding with
+			ioCollector.SetUserData(reinterpret_cast<uint64>(c));
+
+			// Make shape 2 relative to inBaseOffset
+			Mat44 transform2 = c->GetCenterOfMassTransform().PostTranslated(-inBaseOffset).ToMat44();
+
+			// We need to add the padding of character 2 so that we will detect collision with its outer shell
+			settings.mMaxSeparationDistance = inCollideShapeSettings.mMaxSeparationDistance + c->GetCharacterPadding();
+
+			// Note that this collides against the character's shape without padding, this will be corrected for in CharacterVirtual::GetContactsAtPosition
+			CollisionDispatch::sCollideShapeVsShape(shape, c->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, SubShapeIDCreator(), SubShapeIDCreator(), settings, ioCollector);
+		}
+
+	// Reset the user data
+	ioCollector.SetUserData(0);
+}
+
+void CharacterVsCharacterCollisionSimple::CastCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, Vec3Arg inDirection, const ShapeCastSettings &inShapeCastSettings, RVec3Arg inBaseOffset, CastShapeCollector &ioCollector) const
+{
+	// Convert shape cast relative to inBaseOffset
+	Mat44 transform1 = inCenterOfMassTransform.PostTranslated(-inBaseOffset).ToMat44();
+	ShapeCast shape_cast(inCharacter->GetShape(), Vec3::sReplicate(1.0f), transform1, inDirection);
+
+	// Iterate over all characters
+	for (const CharacterVirtual *c : mCharacters)
+		if (c != inCharacter
+			&& !ioCollector.ShouldEarlyOut())
+		{
+			// Collector needs to know which character we're colliding with
+			ioCollector.SetUserData(reinterpret_cast<uint64>(c));
+
+			// Make shape 2 relative to inBaseOffset
+			Mat44 transform2 = c->GetCenterOfMassTransform().PostTranslated(-inBaseOffset).ToMat44();
+
+			// Note that this collides against the character's shape without padding, this will be corrected for in CharacterVirtual::GetFirstContactForSweep
+			CollisionDispatch::sCastShapeVsShapeWorldSpace(shape_cast, inShapeCastSettings, c->GetShape(), Vec3::sReplicate(1.0f), { }, transform2, SubShapeIDCreator(), SubShapeIDCreator(), ioCollector);
+		}
+
+	// Reset the user data
+	ioCollector.SetUserData(0);
+}
+
 CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem) :
 	CharacterBase(inSettings, inSystem),
 	mBackFaceMode(inSettings->mBackFaceMode),
@@ -104,6 +166,20 @@ void CharacterVirtual::sFillContactProperties(const CharacterVirtual *inCharacte
 	outContact.mMaterial = inCollector.GetContext()->GetMaterial(inResult.mSubShapeID2);
 }
 
+void CharacterVirtual::sFillCharacterContactProperties(Contact &outContact, CharacterVirtual *inOtherCharacter, RVec3Arg inBaseOffset, const CollideShapeResult &inResult)
+{
+	outContact.mPosition = inBaseOffset + inResult.mContactPointOn2;
+	outContact.mLinearVelocity = inOtherCharacter->GetLinearVelocity();
+	outContact.mSurfaceNormal = outContact.mContactNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
+	outContact.mDistance = -inResult.mPenetrationDepth;
+	outContact.mCharacterB = inOtherCharacter;
+	outContact.mSubShapeIDB = inResult.mSubShapeID2;
+	outContact.mMotionTypeB = EMotionType::Kinematic; // Other character is kinematic, we can't directly move it
+	outContact.mIsSensorB = false;
+	outContact.mUserData = inOtherCharacter->GetUserData();
+	outContact.mMaterial = PhysicsMaterial::sDefault;
+}
+
 void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResult)
 {
 	// If we exceed our contact limit, try to clean up near-duplicate contacts
@@ -122,7 +198,7 @@ void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResu
 				for (int j = i - 1; j >= 0; --j)
 				{
 					Contact &contact_j = mContacts[j];
-					if (contact_i.mBodyB == contact_j.mBodyB // Same body
+					if (contact_i.IsSameBody(contact_j)
 						&& contact_i.mContactNormal.Dot(contact_j.mContactNormal) > mHitReductionCosMaxAngle) // Very similar contact normals
 					{
 						// Remove the contact with the biggest distance
@@ -160,22 +236,35 @@ void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResu
 		}
 	}
 
-	BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
-	if (lock.SucceededAndIsInBroadPhase())
+	if (inResult.mBodyID2.IsInvalid())
 	{
+		// Assuming this is a hit against another character
+		JPH_ASSERT(mOtherCharacter != nullptr);
+
+		// Create contact with other character
 		mContacts.emplace_back();
 		Contact &contact = mContacts.back();
-		sFillContactProperties(mCharacter, contact, lock.GetBody(), mUp, mBaseOffset, *this, inResult);
+		sFillCharacterContactProperties(contact, mOtherCharacter, mBaseOffset, inResult);
 		contact.mFraction = 0.0f;
 	}
+	else
+	{
+		// Create contact with other body
+		BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
+		if (lock.SucceededAndIsInBroadPhase())
+		{
+			mContacts.emplace_back();
+			Contact &contact = mContacts.back();
+			sFillContactProperties(mCharacter, contact, lock.GetBody(), mUp, mBaseOffset, *this, inResult);
+			contact.mFraction = 0.0f;
+		}
+	}
 }
 
 void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inResult)
 {
-	// Should not have gotten here without a lower fraction
-	JPH_ASSERT(inResult.mFraction < mContact.mFraction);
-
-	if (inResult.mFraction > 0.0f // Ignore collisions at fraction = 0
+	if (inResult.mFraction < mContact.mFraction // Since we're doing checks against the world and against characters, we may get a hit with a higher fraction than the previous hit
+		&& inResult.mFraction > 0.0f // Ignore collisions at fraction = 0
 		&& inResult.mPenetrationAxis.Dot(mDisplacement) > 0.0f) // Ignore penetrations that we're moving away from
 	{
 		// Test if this contact should be ignored
@@ -185,8 +274,17 @@ void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inRes
 
 		Contact contact;
 
-		// Lock body only while we fetch contact properties
+		if (inResult.mBodyID2.IsInvalid())
+		{
+			// Assuming this is a hit against another character
+			JPH_ASSERT(mOtherCharacter != nullptr);
+
+			// Create contact with other character
+			sFillCharacterContactProperties(contact, mOtherCharacter, mBaseOffset, inResult);
+		}
+		else
 		{
+			// Lock body only while we fetch contact properties
 			BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
 			if (!lock.SucceededAndIsInBroadPhase())
 				return;
@@ -292,6 +390,13 @@ void CharacterVirtual::CheckCollision(RVec3Arg inPosition, QuatArg inRotation, V
 	}
 	else
 		mSystem->GetNarrowPhaseQuery().CollideShape(inShape, Vec3::sReplicate(1.0f), transform, settings, inBaseOffset, ioCollector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
+
+	// Also collide with other characters
+	if (mCharacterVsCharacterCollision != nullptr)
+	{
+		ioCollector.SetContext(nullptr); // We're no longer colliding with a transformed shape, reset
+		mCharacterVsCharacterCollision->CollideCharacter(this, transform, settings, inBaseOffset, ioCollector);
+	}
 }
 
 void CharacterVirtual::GetContactsAtPosition(RVec3Arg inPosition, Vec3Arg inMovementDirection, const Shape *inShape, TempContactList &outContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter) const
@@ -313,7 +418,12 @@ void CharacterVirtual::GetContactsAtPosition(RVec3Arg inPosition, Vec3Arg inMove
 	// Reduce distance to contact by padding to ensure we stay away from the object by a little margin
 	// (this will make collision detection cheaper - especially for sweep tests as they won't hit the surface if we're properly sliding)
 	for (Contact &c : outContacts)
+	{
 		c.mDistance -= mCharacterPadding;
+
+		if (c.mCharacterB != nullptr)
+			c.mDistance -= c.mCharacterB->mCharacterPadding;
+	}
 }
 
 void CharacterVirtual::RemoveConflictingContacts(TempContactList &ioContacts, IgnoredContactList &outIgnoredContacts) const
@@ -330,7 +440,7 @@ void CharacterVirtual::RemoveConflictingContacts(TempContactList &ioContacts, Ig
 			for (size_t c2 = c1 + 1; c2 < ioContacts.size(); c2++)
 			{
 				Contact &contact2 = ioContacts[c2];
-				if (contact1.mBodyB == contact2.mBodyB // Only same body
+				if (contact1.IsSameBody(contact2)
 					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
 					&& contact1.mContactNormal.Dot(contact2.mContactNormal) < 0.0f) // Only opposing normals
 				{
@@ -360,7 +470,10 @@ bool CharacterVirtual::ValidateContact(const Contact &inContact) const
 	if (mListener == nullptr)
 		return true;
 
-	return mListener->OnContactValidate(this, inContact.mBodyB, inContact.mSubShapeIDB);
+	if (inContact.mCharacterB != nullptr)
+		return mListener->OnCharacterContactValidate(this, inContact.mCharacterB, inContact.mSubShapeIDB);
+	else
+		return mListener->OnContactValidate(this, inContact.mBodyB, inContact.mSubShapeIDB);
 }
 
 template <class T>
@@ -413,27 +526,52 @@ bool CharacterVirtual::GetFirstContactForSweep(RVec3Arg inPosition, Vec3Arg inDi
 	// Cast shape
 	Contact contact;
 	contact.mFraction = 1.0f + character_padding_fraction;
-	ContactCastCollector collector(mSystem, this, inDisplacement, mUp, inIgnoredContacts, start.GetTranslation(), contact);
+	RVec3 base_offset = start.GetTranslation();
+	ContactCastCollector collector(mSystem, this, inDisplacement, mUp, inIgnoredContacts, base_offset, contact);
 	collector.ResetEarlyOutFraction(contact.mFraction);
 	RShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
-	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, start.GetTranslation(), collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
-	if (contact.mBodyB.IsInvalid())
+	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, base_offset, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
+
+	// Also collide with other characters
+	if (mCharacterVsCharacterCollision != nullptr)
+	{
+		collector.SetContext(nullptr); // We're no longer colliding with a transformed shape, reset
+		mCharacterVsCharacterCollision->CastCharacter(this, start, inDisplacement, settings, base_offset, collector);
+	}
+
+	if (contact.mBodyB.IsInvalid() && contact.mCharacterB == nullptr)
 		return false;
 
 	// Store contact
 	outContact = contact;
 
+	TransformedShape ts;
+	float character_padding = mCharacterPadding;
+	if (outContact.mCharacterB != nullptr)
+	{
+		// Create a transformed shape for the character
+		RMat44 com = outContact.mCharacterB->GetCenterOfMassTransform();
+		ts = TransformedShape(com.GetTranslation(), com.GetQuaternion(), outContact.mCharacterB->GetShape(), BodyID(), SubShapeIDCreator());
+
+		// We need to take the other character's padding into account as well
+		character_padding += outContact.mCharacterB->mCharacterPadding;
+	}
+	else
+	{
+		// Create a transformed shape for the body
+		ts = mSystem->GetBodyInterface().GetTransformedShape(outContact.mBodyB);
+	}
+
 	// Fetch the face we're colliding with
-	TransformedShape ts = mSystem->GetBodyInterface().GetTransformedShape(outContact.mBodyB);
 	Shape::SupportingFace face;
-	ts.GetSupportingFace(outContact.mSubShapeIDB, -outContact.mContactNormal, start.GetTranslation(), face);
+	ts.GetSupportingFace(outContact.mSubShapeIDB, -outContact.mContactNormal, base_offset, face);
 
 	bool corrected = false;
 	if (face.size() >= 2)
 	{
 		// Inflate the colliding face by the character padding
 		PolygonConvexSupport polygon(face);
-		AddConvexRadius add_cvx(polygon, mCharacterPadding);
+		AddConvexRadius add_cvx(polygon, character_padding);
 
 		// Correct fraction to hit this inflated face instead of the inner shape
 		corrected = sCorrectFractionForCharacterPadding(mShape, start.GetRotation(), inDisplacement, add_cvx, outContact.mFraction);
@@ -504,7 +642,12 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 	// Send contact added event
 	CharacterContactSettings settings;
 	if (mListener != nullptr)
-		mListener->OnContactAdded(this, contact.mBodyB, contact.mSubShapeIDB, contact.mPosition, -contact.mContactNormal, settings);
+	{
+		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);
+	}
 	contact.mCanPushCharacter = settings.mCanPushCharacter;
 
 	// We don't have any further interaction with sensors beyond an OnContactAdded notification
@@ -776,7 +919,12 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, float inDeltaTime, f
 
 		// Allow application to modify calculated velocity
 		if (mListener != nullptr)
-			mListener->OnContactSolve(this, constraint->mContact->mBodyB, constraint->mContact->mSubShapeIDB, constraint->mContact->mPosition, constraint->mContact->mContactNormal, constraint->mContact->mLinearVelocity, constraint->mContact->mMaterial, velocity, new_velocity);
+		{
+			if (constraint->mContact->mCharacterB != nullptr)
+				mListener->OnCharacterContactSolve(this, constraint->mContact->mCharacterB, constraint->mContact->mSubShapeIDB, constraint->mContact->mPosition, constraint->mContact->mContactNormal, constraint->mContact->mLinearVelocity, constraint->mContact->mMaterial, velocity, new_velocity);
+			else
+				mListener->OnContactSolve(this, constraint->mContact->mBodyB, constraint->mContact->mSubShapeIDB, constraint->mContact->mPosition, constraint->mContact->mContactNormal, constraint->mContact->mLinearVelocity, constraint->mContact->mMaterial, velocity, new_velocity);
+		}
 
 #ifdef JPH_DEBUG_RENDERER
 		if (inDrawConstraints)

+ 99 - 4
Jolt/Physics/Character/CharacterVirtual.h

@@ -14,6 +14,7 @@
 JPH_NAMESPACE_BEGIN
 
 class CharacterVirtual;
+class CollideShapeSettings;
 
 /// Contains the configuration of a character
 class JPH_EXPORT CharacterVirtualSettings : public CharacterBaseSettings
@@ -47,8 +48,13 @@ public:
 class CharacterContactSettings
 {
 public:
-	bool								mCanPushCharacter = true;								///< True when the object can push the virtual character
-	bool								mCanReceiveImpulses = true;								///< True when the virtual character can apply impulses (push) the body
+	/// True when the object can push the virtual character.
+	bool								mCanPushCharacter = true;
+
+	/// True when the virtual character can apply impulses (push) the body.
+	/// Note that this only works against rigid bodies. Other CharacterVirtual objects can only be moved in their own update,
+	/// so you must ensure that in their OnCharacterContactAdded mCanPushCharacter is true.
+	bool								mCanReceiveImpulses = true;
 };
 
 /// This class receives callbacks when a virtual character hits something.
@@ -65,6 +71,9 @@ public:
 	/// Checks if a character can collide with specified body. Return true if the contact is valid.
 	virtual bool						OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) { return true; }
 
+	/// Same as OnContactValidate but when colliding with a CharacterVirtual
+	virtual bool						OnCharacterContactValidate(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2) { return true; }
+
 	/// Called whenever the character collides with a body.
 	/// @param inCharacter Character that is being solved
 	/// @param inBodyID2 Body ID of body that is being hit
@@ -74,6 +83,9 @@ public:
 	/// @param ioSettings Settings returned by the contact callback to indicate how the character should behave
 	virtual void						OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) { /* Default do nothing */ }
 
+	/// Same as OnContactAdded but when colliding with a CharacterVirtual
+	virtual void						OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) { /* Default do nothing */ }
+
 	/// Called whenever a contact is being used by the solver. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
 	/// @param inCharacter Character that is being solved
 	/// @param inBodyID2 Body ID of body that is being hit
@@ -85,6 +97,53 @@ public:
 	/// @param inCharacterVelocity World space velocity of the character prior to hitting this contact
 	/// @param ioNewCharacterVelocity Contains the calculated world space velocity of the character after hitting this contact, this velocity slides along the surface of the contact. Can be modified by the listener to provide an alternative velocity.
 	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) { /* Default do nothing */ }
+
+	/// Same as OnContactSolve but when colliding with a CharacterVirtual
+	virtual void						OnCharacterContactSolve(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity) { /* Default do nothing */ }
+};
+
+/// Interface class that allows a CharacterVirtual to check collision with other CharacterVirtual instances.
+/// Since CharacterVirtual instances are not registered anywhere, it is up to the application to test collision against relevant characters.
+/// The characters could be stored in a tree structure to make this more efficient.
+class JPH_EXPORT CharacterVsCharacterCollision : public NonCopyable
+{
+public:
+	virtual								~CharacterVsCharacterCollision() = default;
+
+	/// Collide a character against other CharacterVirtuals.
+	/// @param inCharacter The character to collide.
+	/// @param inCenterOfMassTransform Center of mass transform for this character.
+	/// @param inCollideShapeSettings Settings for the collision check.
+	/// @param inBaseOffset All hit results will be returned relative to this offset, can be zero to get results in world position, but when you're testing far from the origin you get better precision by picking a position that's closer e.g. GetPosition() since floats are most accurate near the origin
+	/// @param ioCollector Collision collector that receives the collision results.
+	virtual void						CollideCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, const CollideShapeSettings &inCollideShapeSettings, RVec3Arg inBaseOffset, CollideShapeCollector &ioCollector) const = 0;
+
+	/// Cast a character against other CharacterVirtuals.
+	/// @param inCharacter The character to cast.
+	/// @param inCenterOfMassTransform Center of mass transform for this character.
+	/// @param inDirection Direction and length to cast in.
+	/// @param inShapeCastSettings Settings for the shape cast.
+	/// @param inBaseOffset All hit results will be returned relative to this offset, can be zero to get results in world position, but when you're testing far from the origin you get better precision by picking a position that's closer e.g. GetPosition() since floats are most accurate near the origin
+	/// @param ioCollector Collision collector that receives the collision results.
+	virtual void						CastCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, Vec3Arg inDirection, const ShapeCastSettings &inShapeCastSettings, RVec3Arg inBaseOffset, CastShapeCollector &ioCollector) const = 0;
+};
+
+/// Simple collision checker that loops over all registered characters.
+/// Note that this is not thread safe, so make sure that only one CharacterVirtual is checking collision at a time.
+class JPH_EXPORT CharacterVsCharacterCollisionSimple : public CharacterVsCharacterCollision
+{
+public:
+	/// Add a character to the list of characters to check collision against.
+	void								Add(CharacterVirtual *inCharacter)						{ mCharacters.push_back(inCharacter); }
+
+	/// Remove a character from the list of characters to check collision against.
+	void								Remove(const CharacterVirtual *inCharacter);
+
+	// See: CharacterVsCharacterCollision
+	virtual void						CollideCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, const CollideShapeSettings &inCollideShapeSettings, RVec3Arg inBaseOffset, CollideShapeCollector &ioCollector) const override;
+	virtual void						CastCharacter(const CharacterVirtual *inCharacter, RMat44Arg inCenterOfMassTransform, Vec3Arg inDirection, const ShapeCastSettings &inShapeCastSettings, RVec3Arg inBaseOffset, CastShapeCollector &ioCollector) const override;
+
+	Array<CharacterVirtual *>			mCharacters;											///< The list of characters to check collision against
 };
 
 /// Runtime character object.
@@ -111,6 +170,9 @@ public:
 	/// Set the contact listener
 	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
 
+	/// Set the character vs character collision interface
+	void								SetCharacterVsCharacterCollision(CharacterVsCharacterCollision *inCharacterVsCharacterCollision) { mCharacterVsCharacterCollision = inCharacterVsCharacterCollision; }
+
 	/// Get the current contact listener
 	CharacterContactListener *			GetListener() const										{ return mListener; }
 
@@ -271,7 +333,8 @@ public:
 	/// @return Returns true if the switch succeeded.
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator);
 
-	/// @brief Get all contacts for the character at a particular location
+	/// @brief Get all contacts for the character at a particular location.
+	/// When colliding with another character virtual, this pointer will be provided through CollideShapeCollector::SetUserContext before adding a hit.
 	/// @param inPosition Position to test, note that this position will be corrected for the character padding.
 	/// @param inRotation Rotation at which to test the shape.
 	/// @param inMovementDirection A hint in which direction the character is moving, will be used to calculate a proper normal.
@@ -302,13 +365,17 @@ public:
 		void							SaveState(StateRecorder &inStream) const;
 		void							RestoreState(StateRecorder &inStream);
 
+		// Checks if two contacts refer to the same body (or virtual character)
+		inline bool						IsSameBody(const Contact &inOther) const				{ return mBodyB == inOther.mBodyB && mCharacterB == inOther.mCharacterB; }
+
 		RVec3							mPosition;												///< Position where the character makes contact
 		Vec3							mLinearVelocity;										///< Velocity of the contact point
 		Vec3							mContactNormal;											///< Contact normal, pointing towards the character
 		Vec3							mSurfaceNormal;											///< Surface normal of the contact
 		float							mDistance;												///< Distance to the contact <= 0 means that it is an actual contact, > 0 means predictive
 		float							mFraction;												///< Fraction along the path where this contact takes place
-		BodyID							mBodyB;													///< ID of body we're colliding with
+		BodyID							mBodyB;													///< ID of body we're colliding with (if not invalid)
+		CharacterVirtual *				mCharacterB = nullptr;									///< Character we're colliding with (if not null)
 		SubShapeID						mSubShapeIDB;											///< Sub shape ID of body we're colliding with
 		EMotionType						mMotionTypeB;											///< Motion type of B, used to determine the priority of the contact
 		bool							mIsSensorB;												///< If B is a sensor
@@ -325,6 +392,24 @@ public:
 	/// Access to the internal list of contacts that the character has found.
 	const ContactList &					GetActiveContacts() const								{ return mActiveContacts; }
 
+	/// Check if the character is currently in contact with or has collided with another body in the last time step
+	bool								HasCollidedWith(const BodyID &inBody) const
+	{
+		for (const CharacterVirtual::Contact &c : mActiveContacts)
+			if (c.mHadCollision && c.mBodyB == inBody)
+				return true;
+		return false;
+	}
+
+	/// Check if the character is currently in contact with or has collided with another character in the last time step
+	bool								HasCollidedWith(const CharacterVirtual *inCharacter) const
+	{
+		for (const CharacterVirtual::Contact &c : mActiveContacts)
+			if (c.mHadCollision && c.mCharacterB == inCharacter)
+				return true;
+		return false;
+	}
+
 private:
 	// Sorting predicate for making contact order deterministic
 	struct ContactOrderingPredicate
@@ -369,12 +454,15 @@ private:
 	public:
 										ContactCollector(PhysicsSystem *inSystem, const CharacterVirtual *inCharacter, uint inMaxHits, float inHitReductionCosMaxAngle, Vec3Arg inUp, RVec3Arg inBaseOffset, TempContactList &outContacts) : mBaseOffset(inBaseOffset), mUp(inUp), mSystem(inSystem), mCharacter(inCharacter), mContacts(outContacts), mMaxHits(inMaxHits), mHitReductionCosMaxAngle(inHitReductionCosMaxAngle) { }
 
+		virtual void					SetUserData(uint64 inUserData) override					{ mOtherCharacter = reinterpret_cast<CharacterVirtual *>(inUserData); }
+
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 
 		RVec3							mBaseOffset;
 		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		const CharacterVirtual *		mCharacter;
+		CharacterVirtual *				mOtherCharacter = nullptr;
 		TempContactList &				mContacts;
 		uint							mMaxHits;
 		float							mHitReductionCosMaxAngle;
@@ -387,6 +475,8 @@ private:
 	public:
 										ContactCastCollector(PhysicsSystem *inSystem, const CharacterVirtual *inCharacter, Vec3Arg inDisplacement, Vec3Arg inUp, const IgnoredContactList &inIgnoredContacts, RVec3Arg inBaseOffset, Contact &outContact) : mBaseOffset(inBaseOffset), mDisplacement(inDisplacement), mUp(inUp), mSystem(inSystem), mCharacter(inCharacter), mIgnoredContacts(inIgnoredContacts), mContact(outContact) { }
 
+		virtual void					SetUserData(uint64 inUserData) override					{ mOtherCharacter = reinterpret_cast<CharacterVirtual *>(inUserData); }
+
 		virtual void					AddHit(const ShapeCastResult &inResult) override;
 
 		RVec3							mBaseOffset;
@@ -394,6 +484,7 @@ private:
 		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		const CharacterVirtual *		mCharacter;
+		CharacterVirtual *				mOtherCharacter = nullptr;
 		const IgnoredContactList &		mIgnoredContacts;
 		Contact &						mContact;
 	};
@@ -401,6 +492,7 @@ private:
 	// Helper function to convert a Jolt collision result into a contact
 	template <class taCollector>
 	inline static void					sFillContactProperties(const CharacterVirtual *inCharacter, Contact &outContact, const Body &inBody, Vec3Arg inUp, RVec3Arg inBaseOffset, const taCollector &inCollector, const CollideShapeResult &inResult);
+	inline static void					sFillCharacterContactProperties(Contact &outContact, CharacterVirtual *inOtherCharacter, RVec3Arg inBaseOffset, const CollideShapeResult &inResult);
 
 	// Move the shape from ioPosition and try to displace it by inVelocity * inDeltaTime, this will try to slide the shape along the world geometry
 	void								MoveShape(RVec3 &ioPosition, Vec3Arg inVelocity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator
@@ -460,6 +552,9 @@ private:
 	// Our main listener for contacts
 	CharacterContactListener *			mListener = nullptr;
 
+	// Interface to detect collision between characters
+	CharacterVsCharacterCollision *		mCharacterVsCharacterCollision = nullptr;
+
 	// Movement settings
 	EBackFaceMode						mBackFaceMode;											// When colliding with back faces, the character will not be able to move through back facing triangles. Use this if you have triangles that need to collide on both sides.
 	float								mPredictiveContactDistance;								// How far to scan outside of the shape for predictive contacts. A value of 0 will most likely cause the character to get stuck as it cannot properly calculate a sliding direction anymore. A value that's too high will cause ghost collisions.

+ 3 - 0
Jolt/Physics/Collision/CollisionCollector.h

@@ -70,6 +70,9 @@ public:
 	void					SetContext(const TransformedShape *inContext)	{ mContext = inContext; }
 	const TransformedShape *GetContext() const								{ return mContext; }
 
+	/// This function can be used to set some user data on the collision collector
+	virtual void			SetUserData(uint64 inUserData)					{ /* Does nothing by default */ }
+
 	/// This function will be called for every hit found, it's up to the application to decide how to store the hit
 	virtual void			AddHit(const ResultType &inResult) = 0;
 

+ 88 - 20
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -82,9 +82,37 @@ static const int cMeshWallSegments = 25;
 static const RVec3 cHalfCylinderPosition(5.0f, 0, 8.0f);
 static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
 static const RVec3 cSensorPosition(30, 0.9f, -5);
+static const RVec3 cCharacterPosition(-4.0f, 0, 3.0f);
+static const RVec3 cCharacterVirtualPosition(-6.0f, 0, 3.0f);
+static const Vec3 cCharacterVelocity(0, 0, 2);
+
+CharacterBaseTest::~CharacterBaseTest()
+{
+	if (mAnimatedCharacter != nullptr)
+		mAnimatedCharacter->RemoveFromPhysicsSystem();
+}
 
 void CharacterBaseTest::Initialize()
 {
+	// Create capsule shapes for all stances
+	switch (sShapeType)
+	{
+	case EType::Capsule:
+		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
+		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching)).Create().Get();
+		break;
+
+	case EType::Cylinder:
+		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)).Create().Get();
+		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)).Create().Get();
+		break;
+
+	case EType::Box:
+		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
+		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
+		break;
+	}
+
 	if (strcmp(sSceneName, "PerlinMesh") == 0)
 	{
 		// Default terrain
@@ -495,6 +523,26 @@ void CharacterBaseTest::Initialize()
 			sensor.mIsSensor = true;
 			mSensorBody = mBodyInterface->CreateAndAddBody(sensor, EActivation::Activate);
 		}
+
+		// Create Character
+		{
+			CharacterSettings settings;
+			settings.mLayer = Layers::MOVING;
+			settings.mShape = mStandingShape;
+			settings.mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
+			mAnimatedCharacter = new Character(&settings, cCharacterPosition, Quat::sIdentity(), 0, mPhysicsSystem);
+			mAnimatedCharacter->AddToPhysicsSystem();
+		}
+
+		// Create CharacterVirtual
+		{
+			CharacterVirtualSettings settings;
+			settings.mShape = mStandingShape;
+			settings.mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
+			mAnimatedCharacterVirtual = new CharacterVirtual(&settings, cCharacterVirtualPosition, Quat::sIdentity(), 0, mPhysicsSystem);
+			mAnimatedCharacterVirtual->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
+			mCharacterVsCharacterCollision.Add(mAnimatedCharacterVirtual);
+		}
 	}
 #ifdef JPH_OBJECT_STREAM
 	else
@@ -512,26 +560,6 @@ void CharacterBaseTest::Initialize()
 		scene->CreateBodies(mPhysicsSystem);
 	}
 #endif // JPH_OBJECT_STREAM
-
-
-	// Create capsule shapes for all stances
-	switch (sShapeType)
-	{
-	case EType::Capsule:
-		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
-		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching)).Create().Get();
-		break;
-
-	case EType::Cylinder:
-		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)).Create().Get();
-		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)).Create().Get();
-		break;
-
-	case EType::Box:
-		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
-		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
-		break;
-	}
 }
 
 void CharacterBaseTest::ProcessInput(const ProcessInputParams &inParams)
@@ -586,6 +614,40 @@ void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		mBodyInterface->MoveKinematic(mReversingVerticallyMovingBody, pos + Vec3(0, mReversingVerticallyMovingVelocity * 3.0f * inParams.mDeltaTime, 0), cReversingVerticallyMovingOrientation, inParams.mDeltaTime);
 	}
 
+	// Animate character
+	if (mAnimatedCharacter != nullptr)
+		mAnimatedCharacter->SetLinearVelocity(Sin(mTime) * cCharacterVelocity);
+
+	// Animate character virtual
+	if (mAnimatedCharacterVirtual != nullptr)
+	{
+	#ifdef JPH_DEBUG_RENDERER
+		mAnimatedCharacterVirtual->GetShape()->Draw(mDebugRenderer, mAnimatedCharacterVirtual->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sOrange, false, true);
+	#else
+		mDebugRenderer->DrawCapsule(mAnimatedCharacterVirtual->GetCenterOfMassTransform(), 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + mAnimatedCharacterVirtual->GetCharacterPadding(), Color::sOrange, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+	#endif // JPH_DEBUG_RENDERER
+
+		// Update velocity and apply gravity
+		Vec3 velocity;
+		if (mAnimatedCharacterVirtual->GetGroundState() == CharacterVirtual::EGroundState::OnGround)
+			velocity = Vec3::sZero();
+		else
+			velocity = mAnimatedCharacterVirtual->GetLinearVelocity() * mAnimatedCharacter->GetUp() + mPhysicsSystem->GetGravity() * inParams.mDeltaTime;
+		velocity += Sin(mTime) * cCharacterVelocity;
+		mAnimatedCharacterVirtual->SetLinearVelocity(velocity);
+
+		// Move character
+		CharacterVirtual::ExtendedUpdateSettings update_settings;
+		mAnimatedCharacterVirtual->ExtendedUpdate(inParams.mDeltaTime,
+			mPhysicsSystem->GetGravity(),
+			update_settings,
+			mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
+			mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
+			{ },
+			{ },
+			*mTempAllocator);
+	}
+
 	// Reset ramp blocks
 	mRampBlocksTimeLeft -= inParams.mDeltaTime;
 	if (mRampBlocksTimeLeft < 0.0f)
@@ -650,6 +712,9 @@ void CharacterBaseTest::SaveState(StateRecorder &inStream) const
 	inStream.Write(mTime);
 	inStream.Write(mRampBlocksTimeLeft);
 	inStream.Write(mReversingVerticallyMovingVelocity);
+
+	if (mAnimatedCharacterVirtual != nullptr)
+		mAnimatedCharacterVirtual->SaveState(inStream);
 }
 
 void CharacterBaseTest::RestoreState(StateRecorder &inStream)
@@ -657,6 +722,9 @@ void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 	inStream.Read(mTime);
 	inStream.Read(mRampBlocksTimeLeft);
 	inStream.Read(mReversingVerticallyMovingVelocity);
+
+	if (mAnimatedCharacterVirtual != nullptr)
+		mAnimatedCharacterVirtual->RestoreState(inStream);
 }
 
 void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const

+ 13 - 2
Samples/Tests/Character/CharacterBaseTest.h

@@ -5,7 +5,8 @@
 #pragma once
 
 #include <Tests/Test.h>
-#include <Jolt/Physics/Character/CharacterBase.h>
+#include <Jolt/Physics/Character/Character.h>
+#include <Jolt/Physics/Character/CharacterVirtual.h>
 
 // Base class for the character tests, initializes the test scene.
 class CharacterBaseTest : public Test
@@ -13,6 +14,9 @@ class CharacterBaseTest : public Test
 public:
 	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterBaseTest)
 
+	// Destructor
+	virtual					~CharacterBaseTest() override;
+
 	// Number used to scale the terrain and camera movement to the scene
 	virtual float			GetWorldScale() const override								{ return 0.2f; }
 
@@ -66,7 +70,7 @@ protected:
 	static constexpr float	cCharacterRadiusCrouching = 0.3f;
 
 	// Character movement properties
-	inline static bool		sControlMovementDuringJump = true;					///< If false the character cannot change movement direction in mid air
+	inline static bool		sControlMovementDuringJump = true;							///< If false the character cannot change movement direction in mid air
 	inline static float		sCharacterSpeed = 6.0f;
 	inline static float		sJumpSpeed = 4.0f;
 
@@ -84,6 +88,9 @@ protected:
 	// Sensor body
 	BodyID					mSensorBody;
 
+	// List of active characters in the scene so they can collide
+	CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision;
+
 private:
 	// Shape types
 	enum class EType
@@ -117,6 +124,10 @@ private:
 	float					mReversingVerticallyMovingVelocity = 1.0f;
 	BodyID					mHorizontallyMovingBody;
 
+	// Moving characters
+	Ref<Character>			mAnimatedCharacter;
+	Ref<CharacterVirtual>	mAnimatedCharacterVirtual;
+
 	// Player input
 	Vec3					mControlInput = Vec3::sZero();
 	bool					mJump = false;

+ 0 - 1
Samples/Tests/Character/CharacterTest.h

@@ -5,7 +5,6 @@
 #pragma once
 
 #include <Tests/Character/CharacterBaseTest.h>
-#include <Jolt/Physics/Character/Character.h>
 
 // Simple test that test the Character class. Allows the user to move around with the arrow keys and jump with the J button.
 class CharacterTest : public CharacterBaseTest, public ContactListener

+ 31 - 3
Samples/Tests/Character/CharacterVirtualTest.cpp

@@ -32,7 +32,12 @@ void CharacterVirtualTest::Initialize()
 	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
 	mCharacter = new CharacterVirtual(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
-	mCharacter->SetListener(this);
+	mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
+	mCharacterVsCharacterCollision.Add(mCharacter);
+
+	// Install contact listener for all characters
+	for (CharacterVirtual *character : mCharacterVsCharacterCollision.mCharacters)
+		character->SetListener(this);
 }
 
 void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
@@ -163,6 +168,8 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 void CharacterVirtualTest::AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu)
 {
 	inUI->CreateCheckBox(inSubMenu, "Enable Character Inertia", sEnableCharacterInertia, [](UICheckBox::EState inState) { sEnableCharacterInertia = inState == UICheckBox::STATE_CHECKED; });
+	inUI->CreateCheckBox(inSubMenu, "Player Can Push Other Virtual Characters", sPlayerCanPushOtherCharacters, [](UICheckBox::EState inState) { sPlayerCanPushOtherCharacters = inState == UICheckBox::STATE_CHECKED; });
+	inUI->CreateCheckBox(inSubMenu, "Other Virtual Characters Can Push Player", sOtherCharactersCanPushPlayer, [](UICheckBox::EState inState) { sOtherCharactersCanPushPlayer = inState == UICheckBox::STATE_CHECKED; });
 }
 
 void CharacterVirtualTest::AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu)
@@ -232,13 +239,34 @@ void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, c
 		ioSettings.mCanReceiveImpulses = (index & 2) != 0;
 	}
 
-	// If we encounter an object that can push us, enable sliding
-	if (ioSettings.mCanPushCharacter && mPhysicsSystem->GetBodyInterface().GetMotionType(inBodyID2) != EMotionType::Static)
+	// If we encounter an object that can push the player, enable sliding
+	if (inCharacter == mCharacter
+		&& ioSettings.mCanPushCharacter
+		&& mPhysicsSystem->GetBodyInterface().GetMotionType(inBodyID2) != EMotionType::Static)
+		mAllowSliding = true;
+}
+
+void CharacterVirtualTest::OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
+{
+	// Characters can only be pushed in their own update
+	if (sPlayerCanPushOtherCharacters)
+		ioSettings.mCanPushCharacter = sOtherCharactersCanPushPlayer || inOtherCharacter == mCharacter;
+	else if (sOtherCharactersCanPushPlayer)
+		ioSettings.mCanPushCharacter = inCharacter == mCharacter;
+	else
+		ioSettings.mCanPushCharacter = false;
+
+	// If the player can be pushed by the other virtual character, we allow sliding
+	if (inCharacter == mCharacter && ioSettings.mCanPushCharacter)
 		mAllowSliding = true;
 }
 
 void CharacterVirtualTest::OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity)
 {
+	// Ignore callbacks for other characters than the player
+	if (inCharacter != mCharacter)
+		return;
+
 	// Don't allow the player to slide down static not-too-steep surfaces when not actively moving and when not on a moving platform
 	if (!mAllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal))
 		ioNewCharacterVelocity = Vec3::sZero();

+ 6 - 2
Samples/Tests/Character/CharacterVirtualTest.h

@@ -5,7 +5,6 @@
 #pragma once
 
 #include <Tests/Character/CharacterBaseTest.h>
-#include <Jolt/Physics/Character/CharacterVirtual.h>
 
 // Simple test that test the CharacterVirtual class. Allows the user to move around with the arrow keys and jump with the J button.
 class CharacterVirtualTest : public CharacterBaseTest, public CharacterContactListener
@@ -26,9 +25,12 @@ public:
 	/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
 	virtual void			OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) override;
 
-	// Called whenever the character collides with a body. Returns true if the contact can push the character.
+	// Called whenever the character collides with a body.
 	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
 
+	// Called whenever the character collides with a virtual character.
+	virtual void			OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
+
 	// Called whenever the character movement is solved and a constraint is hit. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
 	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;
 
@@ -61,6 +63,8 @@ private:
 	static inline bool		sEnableWalkStairs = true;
 	static inline bool		sEnableStickToFloor = true;
 	static inline bool		sEnhancedInternalEdgeRemoval = false;
+	static inline bool		sPlayerCanPushOtherCharacters = true;
+	static inline bool		sOtherCharactersCanPushPlayer = true;
 
 	// The 'player' character
 	Ref<CharacterVirtual>	mCharacter;

+ 151 - 29
UnitTests/Physics/CharacterVirtualTests.cpp

@@ -32,6 +32,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Create character
 			mCharacter = new CharacterVirtual(&mCharacterSettings, mInitialPosition, Quat::sIdentity(), 0, mContext.GetSystem());
 			mCharacter->SetListener(this);
+			mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacter);
 		}
 
 		// Step the character and the world
@@ -68,7 +69,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Update character velocity
 			mCharacter->SetLinearVelocity(new_velocity);
 
-			RVec3 start_pos = mCharacter->GetPosition();
+			RVec3 start_pos = GetPosition();
 
 			// Update the character position
 			TempAllocatorMalloc allocator;
@@ -82,7 +83,7 @@ TEST_SUITE("CharacterVirtualTests")
 				allocator);
 
 			// Calculate effective velocity in this step
-			mEffectiveVelocity = Vec3(mCharacter->GetPosition() - start_pos) / delta_time;
+			mEffectiveVelocity = Vec3(GetPosition() - start_pos) / delta_time;
 		}
 
 		// Simulate a longer period of time
@@ -93,6 +94,30 @@ TEST_SUITE("CharacterVirtualTests")
 				Step();
 		}
 
+		// Get the number of active contacts
+		size_t					GetNumContacts() const
+		{
+			return mCharacter->GetActiveContacts().size();
+		}
+
+		// Check if the character is in contact with another body
+		bool					HasCollidedWith(const BodyID &inBody) const
+		{
+			return mCharacter->HasCollidedWith(inBody);
+		}
+
+		// Check if the character is in contact with another character
+		bool					HasCollidedWith(const CharacterVirtual *inCharacter) const
+		{
+			return mCharacter->HasCollidedWith(inCharacter);
+		}
+
+		// Get position of character
+		RVec3					GetPosition() const
+		{
+			return mCharacter->GetPosition();
+		}
+
 		// Configuration
 		RVec3					mInitialPosition = RVec3::sZero();
 		float					mHeightStanding = 1.35f;
@@ -107,6 +132,9 @@ TEST_SUITE("CharacterVirtualTests")
 		// The character
 		Ref<CharacterVirtual>	mCharacter;
 
+		// Character vs character
+		CharacterVsCharacterCollisionSimple mCharacterVsCharacter;
+
 		// Calculated effective velocity after a step
 		Vec3					mEffectiveVelocity = Vec3::sZero();
 
@@ -140,21 +168,21 @@ TEST_SUITE("CharacterVirtualTests")
 		// After some time we should be on the floor
 		character.Simulate(1.0f);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3::sZero());
+		CHECK_APPROX_EQUAL(character.GetPosition(), RVec3::sZero());
 		CHECK_APPROX_EQUAL(character.mEffectiveVelocity, Vec3::sZero());
 
 		// Jump
 		character.mJumpSpeed = 1.0f;
 		character.Step();
 		Vec3 velocity(0, 1.0f + c.GetDeltaTime() * c.GetSystem()->GetGravity().GetY(), 0);
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(velocity * c.GetDeltaTime()));
+		CHECK_APPROX_EQUAL(character.GetPosition(), RVec3(velocity * c.GetDeltaTime()));
 		CHECK_APPROX_EQUAL(character.mEffectiveVelocity, velocity);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::InAir);
 
 		// After some time we should be on the floor again
 		character.Simulate(1.0f);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3::sZero());
+		CHECK_APPROX_EQUAL(character.GetPosition(), RVec3::sZero());
 		CHECK_APPROX_EQUAL(character.mEffectiveVelocity, Vec3::sZero());
 	}
 
@@ -198,12 +226,12 @@ TEST_SUITE("CharacterVirtualTests")
 			// After 1 step we should be on the slope
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), position_after_1_step, 2.0e-6f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), position_after_1_step, 2.0e-6f);
 
 			// 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.mCharacter->GetPosition();
+			RVec3 start_pos = character.GetPosition();
 
 			// Start moving in X direction
 			character.mHorizontalSpeed = Vec3(2.0f, 0, 0);
@@ -211,7 +239,7 @@ TEST_SUITE("CharacterVirtualTests")
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
 
 			// Calculate resulting translation
-			Vec3 translation = Vec3(character.mCharacter->GetPosition() - start_pos);
+			Vec3 translation = Vec3(character.GetPosition() - start_pos);
 
 			// Calculate expected translation
 			Vec3 expected_translation;
@@ -271,7 +299,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// 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.mCharacter->GetPosition();
+			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);
@@ -279,7 +307,7 @@ TEST_SUITE("CharacterVirtualTests")
 			CHECK(character.mCharacter->GetGroundState() == (stick_to_floor? CharacterBase::EGroundState::OnGround : CharacterBase::EGroundState::InAir));
 
 			// Calculate resulting translation
-			Vec3 translation = Vec3(character.mCharacter->GetPosition() - start_pos);
+			Vec3 translation = Vec3(character.GetPosition() - start_pos);
 
 			// Calculate expected translation
 			Vec3 expected_translation;
@@ -366,7 +394,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// We should have gotten stuck at the start of the stairs (can't move up)
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(0, 0, -radius_and_padding), 1.1e-2f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), RVec3(0, 0, -radius_and_padding), 1.1e-2f);
 
 			// Enable stair walking
 			character.mUpdateSettings.mWalkStairsStepUp = Vec3(0, 0.4f, 0);
@@ -376,7 +404,7 @@ TEST_SUITE("CharacterVirtualTests")
 			int max_steps = int(1.5f * round(movement_time / time_step)); // In practice there is a bit of slowdown while stair stepping, so add a bit of slack
 
 			// Step until we reach the top of the stairs
-			RVec3 last_position = character.mCharacter->GetPosition();
+			RVec3 last_position = character.GetPosition();
 			bool reached_goal = false;
 			for (int i = 0; i < max_steps; ++i)
 			{
@@ -386,7 +414,7 @@ TEST_SUITE("CharacterVirtualTests")
 				CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 
 				// Check position progression
-				RVec3 position = character.mCharacter->GetPosition();
+				RVec3 position = character.GetPosition();
 				CHECK_APPROX_EQUAL(position.GetX(), 0); // No movement in X
 				CHECK(position.GetZ() > last_position.GetZ()); // Always moving forward
 				CHECK(position.GetZ() < cNumSteps * cStepHeight); // No movement beyond stairs
@@ -436,7 +464,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Note that the character moves according to the ground velocity and the ground velocity is updated at the end of the step
 			// so the character is always 1 time step behind the platform. This is why we use t and not t + 1 to calculate the expected position.
 			RVec3 expected_position = RMat44::sRotation(Quat::sRotation(Vec3::sAxisY(), float(t) * c.GetDeltaTime() * cAngularVelocity)) * character.mInitialPosition;
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), expected_position, 1.0e-4f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), expected_position, 1.0e-4f);
 		}
 	}
 
@@ -470,7 +498,7 @@ TEST_SUITE("CharacterVirtualTests")
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			RVec3 expected_position = box.GetPosition() + character.mInitialPosition;
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), expected_position, 1.0e-2f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), expected_position, 1.0e-2f);
 		}
 
 		// Stop box
@@ -486,7 +514,7 @@ TEST_SUITE("CharacterVirtualTests")
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			RVec3 expected_position = box.GetPosition() + character.mInitialPosition;
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), expected_position, 1.0e-2f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), expected_position, 1.0e-2f);
 		}
 	}
 
@@ -558,11 +586,11 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
-			CHECK(character.mCharacter->GetActiveContacts().size() <= character.mCharacter->GetMaxNumHits());
+			CHECK(character.GetNumContacts() <= character.mCharacter->GetMaxNumHits());
 			CHECK(character.mCharacter->GetGroundBodyID() == cylinder_id);
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 		}
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), pos_end, 1.0e-4f);
+		CHECK_APPROX_EQUAL(character.GetPosition(), pos_end, 1.0e-4f);
 
 		// Move towards negative cap and test if we hit the end
 		character.mHorizontalSpeed = Vec3(-cCylinderLength, 0, 0);
@@ -570,11 +598,11 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
-			CHECK(character.mCharacter->GetActiveContacts().size() <= character.mCharacter->GetMaxNumHits());
+			CHECK(character.GetNumContacts() <= character.mCharacter->GetMaxNumHits());
 			CHECK(character.mCharacter->GetGroundBodyID() == cylinder_id);
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 		}
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), neg_end, 1.0e-4f);
+		CHECK_APPROX_EQUAL(character.GetPosition(), neg_end, 1.0e-4f);
 
 		// Turn off contact point reduction
 		character.mCharacter->SetHitReductionCosMaxAngle(-1.0f);
@@ -585,9 +613,9 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
-			CHECK(character.mCharacter->GetActiveContacts().size() == character.mCharacter->GetMaxNumHits());
+			CHECK(character.GetNumContacts() == character.mCharacter->GetMaxNumHits());
 		}
-		RVec3 cur_pos = character.mCharacter->GetPosition();
+		RVec3 cur_pos = character.GetPosition();
 		CHECK((pos_end - cur_pos).Length() > 0.01_r);
 
 		// Move towards negative cap and test that we got stuck
@@ -596,9 +624,9 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
-			CHECK(character.mCharacter->GetActiveContacts().size() == character.mCharacter->GetMaxNumHits());
+			CHECK(character.GetNumContacts() == character.mCharacter->GetMaxNumHits());
 		}
-		CHECK(cur_pos.IsClose(character.mCharacter->GetPosition(), 1.0e-6f));
+		CHECK(cur_pos.IsClose(character.GetPosition(), 1.0e-6f));
 
 		// Now teleport the character next to the half cylinder
 		character.mCharacter->SetPosition(RVec3(0, 0, 1));
@@ -609,11 +637,11 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 			character.Step();
 			CHECK(!character.mCharacter->GetMaxHitsExceeded());
-			CHECK(character.mCharacter->GetActiveContacts().size() == 1); // We should only hit the floor
+			CHECK(character.GetNumContacts() == 1); // We should only hit the floor
 			CHECK(character.mCharacter->GetGroundBodyID() == floor.GetID());
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 		}
-		CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(cCylinderLength, 0, 1), 1.0e-4f);
+		CHECK_APPROX_EQUAL(character.GetPosition(), RVec3(cCylinderLength, 0, 1), 1.0e-4f);
 	}
 
 	TEST_CASE("TestStairWalkAlongWall")
@@ -641,7 +669,7 @@ TEST_SUITE("CharacterVirtualTests")
 
 			// We should have moved along the wall at the desired speed
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(5.0f, 0, 0), 1.0e-2f);
+			CHECK_APPROX_EQUAL(character.GetPosition(), RVec3(5.0f, 0, 0), 1.0e-2f);
 		}
 	}
 
@@ -660,7 +688,7 @@ TEST_SUITE("CharacterVirtualTests")
 			Character character(c);
 			character.mCharacterSettings.mPenetrationRecoverySpeed = penetration_recovery;
 			character.Create();
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3::sZero());
+			CHECK_APPROX_EQUAL(character.GetPosition(), RVec3::sZero());
 
 			// Total radius of character
 			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
@@ -673,8 +701,102 @@ TEST_SUITE("CharacterVirtualTests")
 
 				// Step character and check that it matches expected recovery
 				character.Step();
-				CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(x, 0, 0));
+				CHECK_APPROX_EQUAL(character.GetPosition(), RVec3(x, 0, 0));
 			}
 		}
 	}
+
+	TEST_CASE("TestCharacterVsCharacter")
+	{
+		PhysicsTestContext c;
+		BodyID floor_id = c.CreateFloor().GetID();
+
+		// Create characters with different radii and padding
+		Character character1(c);
+		character1.mInitialPosition = RVec3::sZero();
+		character1.mRadiusStanding = 0.2f;
+		character1.mCharacterSettings.mCharacterPadding = 0.04f;
+		character1.Create();
+
+		Character character2(c);
+		character2.mInitialPosition = RVec3(1, 0, 0);
+		character2.mRadiusStanding = 0.3f;
+		character2.mCharacterSettings.mCharacterPadding = 0.03f;
+		character2.Create();
+
+		// Make both collide
+		character1.mCharacterVsCharacter.Add(character2.mCharacter);
+		character2.mCharacterVsCharacter.Add(character1.mCharacter);
+
+		// Add a box behind character 2, we should never hit this
+		Vec3 box_extent(0.1f, 1.0f, 1.0f);
+		c.CreateBox(RVec3(1.5f, 0, 0), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, box_extent, EActivation::DontActivate);
+
+		// Move character 1 towards character 2 so that in 1 step it will hit both character 2 and the box
+		character1.mHorizontalSpeed = Vec3(600.0f, 0, 0);
+		character1.Step();
+
+		// Character 1 should have stopped at character 2
+		float character1_radius = character1.mRadiusStanding + character1.mCharacterSettings.mCharacterPadding;
+		float character2_radius = character2.mRadiusStanding + character2.mCharacterSettings.mCharacterPadding;
+		float separation = character1_radius + character2_radius;
+		RVec3 expected_colliding_with_character = character2.mInitialPosition - Vec3(separation, 0, 0);
+		CHECK_APPROX_EQUAL(character1.GetPosition(), expected_colliding_with_character, 1.0e-3f);
+		CHECK(character1.GetNumContacts() == 2);
+		CHECK(character1.HasCollidedWith(floor_id));
+		CHECK(character1.HasCollidedWith(character2.mCharacter));
+
+		// Move character 1 back to its initial position
+		character1.mCharacter->SetPosition(character1.mInitialPosition);
+		character1.mCharacter->SetLinearVelocity(Vec3::sZero());
+
+		// Now move slowly so that we will detect the collision during the normal collide shape step
+		character1.mHorizontalSpeed = Vec3(1.0f, 0, 0);
+		character1.Step();
+		CHECK(character1.GetNumContacts() == 1);
+		CHECK(character1.HasCollidedWith(floor_id));
+		character1.Simulate(1.0f);
+
+		// Character 1 should have stopped at character 2
+		CHECK_APPROX_EQUAL(character1.GetPosition(), expected_colliding_with_character, 1.0e-3f);
+		CHECK(character1.GetNumContacts() == 2);
+		CHECK(character1.HasCollidedWith(floor_id));
+		CHECK(character1.HasCollidedWith(character2.mCharacter));
+
+		// Move character 1 back to its initial position
+		character1.mCharacter->SetPosition(character1.mInitialPosition);
+		character1.mCharacter->SetLinearVelocity(Vec3::sZero());
+
+		// Add a box in between the characters
+		RVec3 box_position(0.5f, 0, 0);
+		BodyID box_id = c.CreateBox(box_position, Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, box_extent, EActivation::DontActivate).GetID();
+
+		// Move character 1 so that it will step through both the box and the character in 1 time step
+		character1.mHorizontalSpeed = Vec3(600.0f, 0, 0);
+		character1.Step();
+
+		// Expect that it ends up at the box
+		RVec3 expected_colliding_with_box = box_position - Vec3(character1_radius + box_extent.GetX(), 0, 0);
+		CHECK_APPROX_EQUAL(character1.GetPosition(), expected_colliding_with_box, 1.0e-3f);
+		CHECK(character1.GetNumContacts() == 2);
+		CHECK(character1.HasCollidedWith(floor_id));
+		CHECK(character1.HasCollidedWith(box_id));
+
+		// Move character 1 back to its initial position
+		character1.mCharacter->SetPosition(character1.mInitialPosition);
+		character1.mCharacter->SetLinearVelocity(Vec3::sZero());
+
+		// Now move slowly so that we will detect the collision during the normal collide shape step
+		character1.mHorizontalSpeed = Vec3(1.0f, 0, 0);
+		character1.Step();
+		CHECK(character1.GetNumContacts() == 1);
+		CHECK(character1.HasCollidedWith(floor_id));
+		character1.Simulate(1.0f);
+
+		// Expect that it ends up at the box
+		CHECK_APPROX_EQUAL(character1.GetPosition(), expected_colliding_with_box, 1.0e-3f);
+		CHECK(character1.GetNumContacts() == 2);
+		CHECK(character1.HasCollidedWith(floor_id));
+		CHECK(character1.HasCollidedWith(box_id));
+	}
 }