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.
 * 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.
 * 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 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
 #### Character
 
 
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
 * 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
 #### 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
 * 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.
 * 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.
 * 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
 ### 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 -Wunused-parameter warning on GCC when building in Release mode with -Wextra.
 * Fixed tolerance in assert in GetPenetrationDepthStepEPA.
 * 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.
 * 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
 ## 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
   		# 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
 		# 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).
 		# JPH_PRECISE_MATH_ON for the locations where this is a problem).
-  
+
 		if (USE_AVX512)
 		if (USE_AVX512)
 			target_compile_options(Jolt PUBLIC -mavx512f -mavx512vl -mavx512dq -mavx2 -mbmi -mpopcnt -mlzcnt -mf16c)
 			target_compile_options(Jolt PUBLIC -mavx512f -mavx512vl -mavx512dq -mavx2 -mbmi -mpopcnt -mlzcnt -mf16c)
 		elseif (USE_AVX2)
 		elseif (USE_AVX2)

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

@@ -20,6 +20,68 @@
 
 
 JPH_NAMESPACE_BEGIN
 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) :
 CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem) :
 	CharacterBase(inSettings, inSystem),
 	CharacterBase(inSettings, inSystem),
 	mBackFaceMode(inSettings->mBackFaceMode),
 	mBackFaceMode(inSettings->mBackFaceMode),
@@ -104,6 +166,20 @@ void CharacterVirtual::sFillContactProperties(const CharacterVirtual *inCharacte
 	outContact.mMaterial = inCollector.GetContext()->GetMaterial(inResult.mSubShapeID2);
 	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)
 void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResult)
 {
 {
 	// If we exceed our contact limit, try to clean up near-duplicate contacts
 	// 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)
 				for (int j = i - 1; j >= 0; --j)
 				{
 				{
 					Contact &contact_j = mContacts[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
 						&& contact_i.mContactNormal.Dot(contact_j.mContactNormal) > mHitReductionCosMaxAngle) // Very similar contact normals
 					{
 					{
 						// Remove the contact with the biggest distance
 						// 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();
 		mContacts.emplace_back();
 		Contact &contact = mContacts.back();
 		Contact &contact = mContacts.back();
-		sFillContactProperties(mCharacter, contact, lock.GetBody(), mUp, mBaseOffset, *this, inResult);
+		sFillCharacterContactProperties(contact, mOtherCharacter, mBaseOffset, inResult);
 		contact.mFraction = 0.0f;
 		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)
 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
 		&& inResult.mPenetrationAxis.Dot(mDisplacement) > 0.0f) // Ignore penetrations that we're moving away from
 	{
 	{
 		// Test if this contact should be ignored
 		// Test if this contact should be ignored
@@ -185,8 +274,17 @@ void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inRes
 
 
 		Contact contact;
 		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);
 			BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
 			if (!lock.SucceededAndIsInBroadPhase())
 			if (!lock.SucceededAndIsInBroadPhase())
 				return;
 				return;
@@ -292,6 +390,13 @@ void CharacterVirtual::CheckCollision(RVec3Arg inPosition, QuatArg inRotation, V
 	}
 	}
 	else
 	else
 		mSystem->GetNarrowPhaseQuery().CollideShape(inShape, Vec3::sReplicate(1.0f), transform, settings, inBaseOffset, ioCollector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
 		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
 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
 	// 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)
 	// (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)
 	for (Contact &c : outContacts)
+	{
 		c.mDistance -= mCharacterPadding;
 		c.mDistance -= mCharacterPadding;
+
+		if (c.mCharacterB != nullptr)
+			c.mDistance -= c.mCharacterB->mCharacterPadding;
+	}
 }
 }
 
 
 void CharacterVirtual::RemoveConflictingContacts(TempContactList &ioContacts, IgnoredContactList &outIgnoredContacts) const
 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++)
 			for (size_t c2 = c1 + 1; c2 < ioContacts.size(); c2++)
 			{
 			{
 				Contact &contact2 = ioContacts[c2];
 				Contact &contact2 = ioContacts[c2];
-				if (contact1.mBodyB == contact2.mBodyB // Only same body
+				if (contact1.IsSameBody(contact2)
 					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
 					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
 					&& contact1.mContactNormal.Dot(contact2.mContactNormal) < 0.0f) // Only opposing normals
 					&& contact1.mContactNormal.Dot(contact2.mContactNormal) < 0.0f) // Only opposing normals
 				{
 				{
@@ -360,7 +470,10 @@ bool CharacterVirtual::ValidateContact(const Contact &inContact) const
 	if (mListener == nullptr)
 	if (mListener == nullptr)
 		return true;
 		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>
 template <class T>
@@ -413,27 +526,52 @@ bool CharacterVirtual::GetFirstContactForSweep(RVec3Arg inPosition, Vec3Arg inDi
 	// Cast shape
 	// Cast shape
 	Contact contact;
 	Contact contact;
 	contact.mFraction = 1.0f + character_padding_fraction;
 	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);
 	collector.ResetEarlyOutFraction(contact.mFraction);
 	RShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
 	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;
 		return false;
 
 
 	// Store contact
 	// Store contact
 	outContact = 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
 	// Fetch the face we're colliding with
-	TransformedShape ts = mSystem->GetBodyInterface().GetTransformedShape(outContact.mBodyB);
 	Shape::SupportingFace face;
 	Shape::SupportingFace face;
-	ts.GetSupportingFace(outContact.mSubShapeIDB, -outContact.mContactNormal, start.GetTranslation(), face);
+	ts.GetSupportingFace(outContact.mSubShapeIDB, -outContact.mContactNormal, base_offset, face);
 
 
 	bool corrected = false;
 	bool corrected = false;
 	if (face.size() >= 2)
 	if (face.size() >= 2)
 	{
 	{
 		// Inflate the colliding face by the character padding
 		// Inflate the colliding face by the character padding
 		PolygonConvexSupport polygon(face);
 		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
 		// Correct fraction to hit this inflated face instead of the inner shape
 		corrected = sCorrectFractionForCharacterPadding(mShape, start.GetRotation(), inDisplacement, add_cvx, outContact.mFraction);
 		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
 	// Send contact added event
 	CharacterContactSettings settings;
 	CharacterContactSettings settings;
 	if (mListener != nullptr)
 	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;
 	contact.mCanPushCharacter = settings.mCanPushCharacter;
 
 
 	// We don't have any further interaction with sensors beyond an OnContactAdded notification
 	// 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
 		// Allow application to modify calculated velocity
 		if (mListener != nullptr)
 		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
 #ifdef JPH_DEBUG_RENDERER
 		if (inDrawConstraints)
 		if (inDrawConstraints)

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

@@ -14,6 +14,7 @@
 JPH_NAMESPACE_BEGIN
 JPH_NAMESPACE_BEGIN
 
 
 class CharacterVirtual;
 class CharacterVirtual;
+class CollideShapeSettings;
 
 
 /// Contains the configuration of a character
 /// Contains the configuration of a character
 class JPH_EXPORT CharacterVirtualSettings : public CharacterBaseSettings
 class JPH_EXPORT CharacterVirtualSettings : public CharacterBaseSettings
@@ -47,8 +48,13 @@ public:
 class CharacterContactSettings
 class CharacterContactSettings
 {
 {
 public:
 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.
 /// 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.
 	/// 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; }
 	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.
 	/// Called whenever the character collides with a body.
 	/// @param inCharacter Character that is being solved
 	/// @param inCharacter Character that is being solved
 	/// @param inBodyID2 Body ID of body that is being hit
 	/// @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
 	/// @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 */ }
 	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).
 	/// 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 inCharacter Character that is being solved
 	/// @param inBodyID2 Body ID of body that is being hit
 	/// @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 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.
 	/// @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 */ }
 	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.
 /// Runtime character object.
@@ -111,6 +170,9 @@ public:
 	/// Set the contact listener
 	/// Set the contact listener
 	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
 	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
 
 
+	/// Set the character vs character collision interface
+	void								SetCharacterVsCharacterCollision(CharacterVsCharacterCollision *inCharacterVsCharacterCollision) { mCharacterVsCharacterCollision = inCharacterVsCharacterCollision; }
+
 	/// Get the current contact listener
 	/// Get the current contact listener
 	CharacterContactListener *			GetListener() const										{ return mListener; }
 	CharacterContactListener *			GetListener() const										{ return mListener; }
 
 
@@ -271,7 +333,8 @@ public:
 	/// @return Returns true if the switch succeeded.
 	/// @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);
 	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 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 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.
 	/// @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							SaveState(StateRecorder &inStream) const;
 		void							RestoreState(StateRecorder &inStream);
 		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
 		RVec3							mPosition;												///< Position where the character makes contact
 		Vec3							mLinearVelocity;										///< Velocity of the contact point
 		Vec3							mLinearVelocity;										///< Velocity of the contact point
 		Vec3							mContactNormal;											///< Contact normal, pointing towards the character
 		Vec3							mContactNormal;											///< Contact normal, pointing towards the character
 		Vec3							mSurfaceNormal;											///< Surface normal of the contact
 		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							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
 		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
 		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
 		EMotionType						mMotionTypeB;											///< Motion type of B, used to determine the priority of the contact
 		bool							mIsSensorB;												///< If B is a sensor
 		bool							mIsSensorB;												///< If B is a sensor
@@ -325,6 +392,24 @@ public:
 	/// Access to the internal list of contacts that the character has found.
 	/// Access to the internal list of contacts that the character has found.
 	const ContactList &					GetActiveContacts() const								{ return mActiveContacts; }
 	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:
 private:
 	// Sorting predicate for making contact order deterministic
 	// Sorting predicate for making contact order deterministic
 	struct ContactOrderingPredicate
 	struct ContactOrderingPredicate
@@ -369,12 +454,15 @@ private:
 	public:
 	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) { }
 										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;
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 
 
 		RVec3							mBaseOffset;
 		RVec3							mBaseOffset;
 		Vec3							mUp;
 		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		PhysicsSystem *					mSystem;
 		const CharacterVirtual *		mCharacter;
 		const CharacterVirtual *		mCharacter;
+		CharacterVirtual *				mOtherCharacter = nullptr;
 		TempContactList &				mContacts;
 		TempContactList &				mContacts;
 		uint							mMaxHits;
 		uint							mMaxHits;
 		float							mHitReductionCosMaxAngle;
 		float							mHitReductionCosMaxAngle;
@@ -387,6 +475,8 @@ private:
 	public:
 	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) { }
 										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;
 		virtual void					AddHit(const ShapeCastResult &inResult) override;
 
 
 		RVec3							mBaseOffset;
 		RVec3							mBaseOffset;
@@ -394,6 +484,7 @@ private:
 		Vec3							mUp;
 		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		PhysicsSystem *					mSystem;
 		const CharacterVirtual *		mCharacter;
 		const CharacterVirtual *		mCharacter;
+		CharacterVirtual *				mOtherCharacter = nullptr;
 		const IgnoredContactList &		mIgnoredContacts;
 		const IgnoredContactList &		mIgnoredContacts;
 		Contact &						mContact;
 		Contact &						mContact;
 	};
 	};
@@ -401,6 +492,7 @@ private:
 	// Helper function to convert a Jolt collision result into a contact
 	// Helper function to convert a Jolt collision result into a contact
 	template <class taCollector>
 	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					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
 	// 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
 	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
 	// Our main listener for contacts
 	CharacterContactListener *			mListener = nullptr;
 	CharacterContactListener *			mListener = nullptr;
 
 
+	// Interface to detect collision between characters
+	CharacterVsCharacterCollision *		mCharacterVsCharacterCollision = nullptr;
+
 	// Movement settings
 	// 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.
 	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.
 	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; }
 	void					SetContext(const TransformedShape *inContext)	{ mContext = inContext; }
 	const TransformedShape *GetContext() const								{ return mContext; }
 	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
 	/// 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;
 	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 cHalfCylinderPosition(5.0f, 0, 8.0f);
 static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
 static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
 static const RVec3 cSensorPosition(30, 0.9f, -5);
 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()
 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)
 	if (strcmp(sSceneName, "PerlinMesh") == 0)
 	{
 	{
 		// Default terrain
 		// Default terrain
@@ -495,6 +523,26 @@ void CharacterBaseTest::Initialize()
 			sensor.mIsSensor = true;
 			sensor.mIsSensor = true;
 			mSensorBody = mBodyInterface->CreateAndAddBody(sensor, EActivation::Activate);
 			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
 #ifdef JPH_OBJECT_STREAM
 	else
 	else
@@ -512,26 +560,6 @@ void CharacterBaseTest::Initialize()
 		scene->CreateBodies(mPhysicsSystem);
 		scene->CreateBodies(mPhysicsSystem);
 	}
 	}
 #endif // JPH_OBJECT_STREAM
 #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)
 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);
 		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
 	// Reset ramp blocks
 	mRampBlocksTimeLeft -= inParams.mDeltaTime;
 	mRampBlocksTimeLeft -= inParams.mDeltaTime;
 	if (mRampBlocksTimeLeft < 0.0f)
 	if (mRampBlocksTimeLeft < 0.0f)
@@ -650,6 +712,9 @@ void CharacterBaseTest::SaveState(StateRecorder &inStream) const
 	inStream.Write(mTime);
 	inStream.Write(mTime);
 	inStream.Write(mRampBlocksTimeLeft);
 	inStream.Write(mRampBlocksTimeLeft);
 	inStream.Write(mReversingVerticallyMovingVelocity);
 	inStream.Write(mReversingVerticallyMovingVelocity);
+
+	if (mAnimatedCharacterVirtual != nullptr)
+		mAnimatedCharacterVirtual->SaveState(inStream);
 }
 }
 
 
 void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 void CharacterBaseTest::RestoreState(StateRecorder &inStream)
@@ -657,6 +722,9 @@ void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 	inStream.Read(mTime);
 	inStream.Read(mTime);
 	inStream.Read(mRampBlocksTimeLeft);
 	inStream.Read(mRampBlocksTimeLeft);
 	inStream.Read(mReversingVerticallyMovingVelocity);
 	inStream.Read(mReversingVerticallyMovingVelocity);
+
+	if (mAnimatedCharacterVirtual != nullptr)
+		mAnimatedCharacterVirtual->RestoreState(inStream);
 }
 }
 
 
 void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const
 void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const

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

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

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

@@ -5,7 +5,6 @@
 #pragma once
 #pragma once
 
 
 #include <Tests/Character/CharacterBaseTest.h>
 #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.
 // 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
 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->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
 	settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
 	mCharacter = new CharacterVirtual(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
 	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)
 void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
@@ -163,6 +168,8 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 void CharacterVirtualTest::AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu)
 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, "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)
 void CharacterVirtualTest::AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu)
@@ -232,13 +239,34 @@ void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, c
 		ioSettings.mCanReceiveImpulses = (index & 2) != 0;
 		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;
 		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)
 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
 	// 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))
 	if (!mAllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal))
 		ioNewCharacterVelocity = Vec3::sZero();
 		ioNewCharacterVelocity = Vec3::sZero();

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

@@ -5,7 +5,6 @@
 #pragma once
 #pragma once
 
 
 #include <Tests/Character/CharacterBaseTest.h>
 #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.
 // 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
 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.
 	/// 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;
 	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;
 	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).
 	// 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;
 	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		sEnableWalkStairs = true;
 	static inline bool		sEnableStickToFloor = true;
 	static inline bool		sEnableStickToFloor = true;
 	static inline bool		sEnhancedInternalEdgeRemoval = false;
 	static inline bool		sEnhancedInternalEdgeRemoval = false;
+	static inline bool		sPlayerCanPushOtherCharacters = true;
+	static inline bool		sOtherCharactersCanPushPlayer = true;
 
 
 	// The 'player' character
 	// The 'player' character
 	Ref<CharacterVirtual>	mCharacter;
 	Ref<CharacterVirtual>	mCharacter;

+ 151 - 29
UnitTests/Physics/CharacterVirtualTests.cpp

@@ -32,6 +32,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Create character
 			// Create character
 			mCharacter = new CharacterVirtual(&mCharacterSettings, mInitialPosition, Quat::sIdentity(), 0, mContext.GetSystem());
 			mCharacter = new CharacterVirtual(&mCharacterSettings, mInitialPosition, Quat::sIdentity(), 0, mContext.GetSystem());
 			mCharacter->SetListener(this);
 			mCharacter->SetListener(this);
+			mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacter);
 		}
 		}
 
 
 		// Step the character and the world
 		// Step the character and the world
@@ -68,7 +69,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// Update character velocity
 			// Update character velocity
 			mCharacter->SetLinearVelocity(new_velocity);
 			mCharacter->SetLinearVelocity(new_velocity);
 
 
-			RVec3 start_pos = mCharacter->GetPosition();
+			RVec3 start_pos = GetPosition();
 
 
 			// Update the character position
 			// Update the character position
 			TempAllocatorMalloc allocator;
 			TempAllocatorMalloc allocator;
@@ -82,7 +83,7 @@ TEST_SUITE("CharacterVirtualTests")
 				allocator);
 				allocator);
 
 
 			// Calculate effective velocity in this step
 			// 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
 		// Simulate a longer period of time
@@ -93,6 +94,30 @@ TEST_SUITE("CharacterVirtualTests")
 				Step();
 				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
 		// Configuration
 		RVec3					mInitialPosition = RVec3::sZero();
 		RVec3					mInitialPosition = RVec3::sZero();
 		float					mHeightStanding = 1.35f;
 		float					mHeightStanding = 1.35f;
@@ -107,6 +132,9 @@ TEST_SUITE("CharacterVirtualTests")
 		// The character
 		// The character
 		Ref<CharacterVirtual>	mCharacter;
 		Ref<CharacterVirtual>	mCharacter;
 
 
+		// Character vs character
+		CharacterVsCharacterCollisionSimple mCharacterVsCharacter;
+
 		// Calculated effective velocity after a step
 		// Calculated effective velocity after a step
 		Vec3					mEffectiveVelocity = Vec3::sZero();
 		Vec3					mEffectiveVelocity = Vec3::sZero();
 
 
@@ -140,21 +168,21 @@ TEST_SUITE("CharacterVirtualTests")
 		// After some time we should be on the floor
 		// After some time we should be on the floor
 		character.Simulate(1.0f);
 		character.Simulate(1.0f);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 		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());
 		CHECK_APPROX_EQUAL(character.mEffectiveVelocity, Vec3::sZero());
 
 
 		// Jump
 		// Jump
 		character.mJumpSpeed = 1.0f;
 		character.mJumpSpeed = 1.0f;
 		character.Step();
 		character.Step();
 		Vec3 velocity(0, 1.0f + c.GetDeltaTime() * c.GetSystem()->GetGravity().GetY(), 0);
 		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_APPROX_EQUAL(character.mEffectiveVelocity, velocity);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::InAir);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::InAir);
 
 
 		// After some time we should be on the floor again
 		// After some time we should be on the floor again
 		character.Simulate(1.0f);
 		character.Simulate(1.0f);
 		CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 		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());
 		CHECK_APPROX_EQUAL(character.mEffectiveVelocity, Vec3::sZero());
 	}
 	}
 
 
@@ -198,12 +226,12 @@ TEST_SUITE("CharacterVirtualTests")
 			// After 1 step we should be on the slope
 			// After 1 step we should be on the slope
 			character.Step();
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
 			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)
 			// 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());
 			character.mCharacter->SetLinearVelocity(Vec3::sZero());
 
 
-			RVec3 start_pos = character.mCharacter->GetPosition();
+			RVec3 start_pos = character.GetPosition();
 
 
 			// Start moving in X direction
 			// Start moving in X direction
 			character.mHorizontalSpeed = Vec3(2.0f, 0, 0);
 			character.mHorizontalSpeed = Vec3(2.0f, 0, 0);
@@ -211,7 +239,7 @@ TEST_SUITE("CharacterVirtualTests")
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
 
 
 			// Calculate resulting translation
 			// Calculate resulting translation
-			Vec3 translation = Vec3(character.mCharacter->GetPosition() - start_pos);
+			Vec3 translation = Vec3(character.GetPosition() - start_pos);
 
 
 			// Calculate expected translation
 			// Calculate expected translation
 			Vec3 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)
 			// 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());
 			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
 			// 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.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));
 			CHECK(character.mCharacter->GetGroundState() == (stick_to_floor? CharacterBase::EGroundState::OnGround : CharacterBase::EGroundState::InAir));
 
 
 			// Calculate resulting translation
 			// Calculate resulting translation
-			Vec3 translation = Vec3(character.mCharacter->GetPosition() - start_pos);
+			Vec3 translation = Vec3(character.GetPosition() - start_pos);
 
 
 			// Calculate expected translation
 			// Calculate expected translation
 			Vec3 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)
 			// We should have gotten stuck at the start of the stairs (can't move up)
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
 			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
 			// Enable stair walking
 			character.mUpdateSettings.mWalkStairsStepUp = Vec3(0, 0.4f, 0);
 			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
 			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
 			// Step until we reach the top of the stairs
-			RVec3 last_position = character.mCharacter->GetPosition();
+			RVec3 last_position = character.GetPosition();
 			bool reached_goal = false;
 			bool reached_goal = false;
 			for (int i = 0; i < max_steps; ++i)
 			for (int i = 0; i < max_steps; ++i)
 			{
 			{
@@ -386,7 +414,7 @@ TEST_SUITE("CharacterVirtualTests")
 				CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 				CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 
 
 				// Check position progression
 				// Check position progression
-				RVec3 position = character.mCharacter->GetPosition();
+				RVec3 position = character.GetPosition();
 				CHECK_APPROX_EQUAL(position.GetX(), 0); // No movement in X
 				CHECK_APPROX_EQUAL(position.GetX(), 0); // No movement in X
 				CHECK(position.GetZ() > last_position.GetZ()); // Always moving forward
 				CHECK(position.GetZ() > last_position.GetZ()); // Always moving forward
 				CHECK(position.GetZ() < cNumSteps * cStepHeight); // No movement beyond stairs
 				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
 			// 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.
 			// 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;
 			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();
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			RVec3 expected_position = box.GetPosition() + character.mInitialPosition;
 			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
 		// Stop box
@@ -486,7 +514,7 @@ TEST_SUITE("CharacterVirtualTests")
 			character.Step();
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			RVec3 expected_position = box.GetPosition() + character.mInitialPosition;
 			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();
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
 			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->GetGroundBodyID() == cylinder_id);
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 			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
 		// Move towards negative cap and test if we hit the end
 		character.mHorizontalSpeed = Vec3(-cCylinderLength, 0, 0);
 		character.mHorizontalSpeed = Vec3(-cCylinderLength, 0, 0);
@@ -570,11 +598,11 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 		{
 			character.Step();
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
 			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->GetGroundBodyID() == cylinder_id);
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 			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
 		// Turn off contact point reduction
 		character.mCharacter->SetHitReductionCosMaxAngle(-1.0f);
 		character.mCharacter->SetHitReductionCosMaxAngle(-1.0f);
@@ -585,9 +613,9 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 		{
 			character.Step();
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
 			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);
 		CHECK((pos_end - cur_pos).Length() > 0.01_r);
 
 
 		// Move towards negative cap and test that we got stuck
 		// Move towards negative cap and test that we got stuck
@@ -596,9 +624,9 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 		{
 			character.Step();
 			character.Step();
 			CHECK(character.mCharacter->GetMaxHitsExceeded());
 			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
 		// Now teleport the character next to the half cylinder
 		character.mCharacter->SetPosition(RVec3(0, 0, 1));
 		character.mCharacter->SetPosition(RVec3(0, 0, 1));
@@ -609,11 +637,11 @@ TEST_SUITE("CharacterVirtualTests")
 		{
 		{
 			character.Step();
 			character.Step();
 			CHECK(!character.mCharacter->GetMaxHitsExceeded());
 			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->GetGroundBodyID() == floor.GetID());
 			CHECK(character.mCharacter->GetGroundNormal().Dot(Vec3::sAxisY()) > 0.999f);
 			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")
 	TEST_CASE("TestStairWalkAlongWall")
@@ -641,7 +669,7 @@ TEST_SUITE("CharacterVirtualTests")
 
 
 			// We should have moved along the wall at the desired speed
 			// We should have moved along the wall at the desired speed
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			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 character(c);
 			character.mCharacterSettings.mPenetrationRecoverySpeed = penetration_recovery;
 			character.mCharacterSettings.mPenetrationRecoverySpeed = penetration_recovery;
 			character.Create();
 			character.Create();
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3::sZero());
+			CHECK_APPROX_EQUAL(character.GetPosition(), RVec3::sZero());
 
 
 			// Total radius of character
 			// Total radius of character
 			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
 			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
 				// Step character and check that it matches expected recovery
 				character.Step();
 				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));
+	}
 }
 }