Răsfoiți Sursa

Added constraint priority to control the order of evaluation of constraints (#638)

This functionality can be used to make some constraints more important than others (because they have higher priority and are evaluated later) but it can also be used when creating a deterministic simulation between client and server where the client doesn't simulate all objects so is missing some of the constraints. By setting a unique constraint priority on every constraint it will still be possible to create a deterministic simulation for a subset of the constraints. See discussion at #611.
Jorrit Rouwe 2 ani în urmă
părinte
comite
e341bb3e95

+ 5 - 1
Jolt/Physics/Constraints/Constraint.cpp

@@ -19,16 +19,18 @@ JPH_IMPLEMENT_SERIALIZABLE_VIRTUAL(ConstraintSettings)
 
 	JPH_ADD_ATTRIBUTE(ConstraintSettings, mEnabled)
 	JPH_ADD_ATTRIBUTE(ConstraintSettings, mDrawConstraintSize)
+	JPH_ADD_ATTRIBUTE(ConstraintSettings, mConstraintPriority)
 	JPH_ADD_ATTRIBUTE(ConstraintSettings, mNumVelocityStepsOverride)
 	JPH_ADD_ATTRIBUTE(ConstraintSettings, mNumPositionStepsOverride)
 	JPH_ADD_ATTRIBUTE(ConstraintSettings, mUserData)
 }
 
 void ConstraintSettings::SaveBinaryState(StreamOut &inStream) const
-{ 
+{
 	inStream.Write(GetRTTI()->GetHash());
 	inStream.Write(mEnabled);
 	inStream.Write(mDrawConstraintSize);
+	inStream.Write(mConstraintPriority);
 	inStream.Write(mNumVelocityStepsOverride);
 	inStream.Write(mNumPositionStepsOverride);
 }
@@ -38,6 +40,7 @@ void ConstraintSettings::RestoreBinaryState(StreamIn &inStream)
 	// Type hash read by sRestoreFromBinaryState
 	inStream.Read(mEnabled);
 	inStream.Read(mDrawConstraintSize);
+	inStream.Read(mConstraintPriority);
 	inStream.Read(mNumVelocityStepsOverride);
 	inStream.Read(mNumPositionStepsOverride);
 }
@@ -89,6 +92,7 @@ void Constraint::RestoreState(StateRecorder &inStream)
 void Constraint::ToConstraintSettings(ConstraintSettings &outSettings) const
 {
 	outSettings.mEnabled = mEnabled;
+	outSettings.mConstraintPriority = mConstraintPriority;
 	outSettings.mNumVelocityStepsOverride = mNumVelocityStepsOverride;
 	outSettings.mNumPositionStepsOverride = mNumPositionStepsOverride;
 	outSettings.mUserData = mUserData;

+ 13 - 0
Jolt/Physics/Constraints/Constraint.h

@@ -77,6 +77,10 @@ public:
 	/// If this constraint is enabled initially. Use Constraint::SetEnabled to toggle after creation.
 	bool						mEnabled = true;
 
+	/// Priority of the constraint when solving. Higher numbers have are more likely to be solved correctly.
+	/// Note that if you want a deterministic simulation and you cannot guarantee the order in which constraints are added/removed, you can make the priority for all constraints unique to get a deterministic ordering.
+	uint32						mConstraintPriority = 0;
+
 	/// Override for the number of solver velocity iterations to run, the total amount of iterations is the max of PhysicsSettings::mNumVelocitySteps and this for all constraints in the island.
 	int							mNumVelocityStepsOverride = 0;
 
@@ -105,6 +109,7 @@ public:
 #ifdef JPH_DEBUG_RENDERER
 		mDrawConstraintSize(inSettings.mDrawConstraintSize),
 #endif // JPH_DEBUG_RENDERER
+		mConstraintPriority(inSettings.mConstraintPriority),
 		mNumVelocityStepsOverride(inSettings.mNumVelocityStepsOverride),
 		mNumPositionStepsOverride(inSettings.mNumPositionStepsOverride),
 		mEnabled(inSettings.mEnabled),
@@ -121,6 +126,11 @@ public:
 	/// Get the sub type of a constraint
 	virtual EConstraintSubType	GetSubType() const = 0;
 
+	/// Priority of the constraint when solving. Higher numbers have are more likely to be solved correctly.
+	/// Note that if you want a deterministic simulation and you cannot guarantee the order in which constraints are added/removed, you can make the priority for all constraints unique to get a deterministic ordering.
+	uint32						GetConstraintPriority() const				{ return mConstraintPriority; }
+	void						SetConstraintPriority(uint32 inPriority)	{ mConstraintPriority = inPriority; }
+
 	/// Override for the number of solver velocity iterations to run, the total amount of iterations is the max of PhysicsSettings::mNumVelocitySteps and this for all constraints in the island.
 	void						SetNumVelocityStepsOverride(int inN)		{ mNumVelocityStepsOverride = inN; }
 	int							GetNumVelocityStepsOverride() const			{ return mNumVelocityStepsOverride; }
@@ -201,6 +211,9 @@ private:
 	/// Index in the mConstraints list of the ConstraintManager for easy finding
 	uint32						mConstraintIndex = cInvalidConstraintIndex;
 
+	/// Priority of the constraint when solving. Higher numbers have are more likely to be solved correctly.
+	uint32						mConstraintPriority = 0;
+
 	/// Override for the number of solver velocity iterations to run, the total amount of iterations is the max of PhysicsSettings::mNumVelocitySteps and this for all constraints in the island.
 	int							mNumVelocityStepsOverride = 0;
 

+ 13 - 5
Jolt/Physics/Constraints/ConstraintManager.cpp

@@ -13,8 +13,8 @@
 
 JPH_NAMESPACE_BEGIN
 
-void ConstraintManager::Add(Constraint **inConstraints, int inNumber)						
-{ 
+void ConstraintManager::Add(Constraint **inConstraints, int inNumber)
+{
 	UniqueLock lock(mConstraintsMutex JPH_IF_ENABLE_ASSERTS(, mLockContext, EPhysicsLockTypes::ConstraintsList));
 
 	mConstraints.reserve(mConstraints.size() + inNumber);
@@ -103,7 +103,15 @@ void ConstraintManager::sSortConstraints(Constraint **inActiveConstraints, uint3
 {
 	JPH_PROFILE_FUNCTION();
 
-	QuickSort(inConstraintIdxBegin, inConstraintIdxEnd, [inActiveConstraints](uint32 inLHS, uint32 inRHS) { return inActiveConstraints[inLHS]->mConstraintIndex < inActiveConstraints[inRHS]->mConstraintIndex; });
+	QuickSort(inConstraintIdxBegin, inConstraintIdxEnd, [inActiveConstraints](uint32 inLHS, uint32 inRHS) {
+		const Constraint *lhs = inActiveConstraints[inLHS];
+		const Constraint *rhs = inActiveConstraints[inRHS];
+
+		if (lhs->GetConstraintPriority() != rhs->GetConstraintPriority())
+			return lhs->GetConstraintPriority() < rhs->GetConstraintPriority();
+
+		return lhs->mConstraintIndex < rhs->mConstraintIndex;
+	});
 }
 
 void ConstraintManager::sSetupVelocityConstraints(Constraint **inActiveConstraints, uint32 inNumActiveConstraints, float inDeltaTime)
@@ -190,7 +198,7 @@ void ConstraintManager::DrawConstraints(DebugRenderer *inRenderer) const
 
 	UniqueLock lock(mConstraintsMutex JPH_IF_ENABLE_ASSERTS(, mLockContext, EPhysicsLockTypes::ConstraintsList));
 
-	for (const Ref<Constraint> &c : mConstraints)			
+	for (const Ref<Constraint> &c : mConstraints)
 		c->DrawConstraint(inRenderer);
 }
 
@@ -216,7 +224,7 @@ void ConstraintManager::DrawConstraintReferenceFrame(DebugRenderer *inRenderer)
 #endif // JPH_DEBUG_RENDERER
 
 void ConstraintManager::SaveState(StateRecorder &inStream) const
-{	
+{
 	UniqueLock lock(mConstraintsMutex JPH_IF_ENABLE_ASSERTS(, mLockContext, EPhysicsLockTypes::ConstraintsList));
 
 	// Write state of constraints

+ 3 - 1
Samples/Samples.cmake

@@ -23,6 +23,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Character/CharacterSpaceShipTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.h
+	${SAMPLES_ROOT}/Tests/Constraints/ConstraintPriorityTest.cpp
+	${SAMPLES_ROOT}/Tests/Constraints/ConstraintPriorityTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintSingularityTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintSingularityTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintVsCOMChangeTest.cpp
@@ -235,7 +237,7 @@ set(SAMPLES_SRC_FILES
 )
 
 # Group source files
-source_group(TREE ${SAMPLES_ROOT} FILES ${SAMPLES_SRC_FILES})	
+source_group(TREE ${SAMPLES_ROOT} FILES ${SAMPLES_SRC_FILES})
 
 # Create Samples executable
 add_executable(Samples  ${SAMPLES_SRC_FILES})

+ 25 - 23
Samples/SamplesApp.cpp

@@ -150,6 +150,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SliderConstraintTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PoweredSliderConstraintTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SpringTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConstraintSingularityTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConstraintPriorityTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PoweredSwingTwistConstraintTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SwingTwistConstraintFrictionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PathConstraintTest)
@@ -179,6 +180,7 @@ static TestNameAndRTTI sConstraintTests[] =
 	{ "Spring",								JPH_RTTI(SpringTest) },
 	{ "Constraint Singularity",				JPH_RTTI(ConstraintSingularityTest) },
 	{ "Constraint vs Center Of Mass Change",JPH_RTTI(ConstraintVsCOMChangeTest) },
+	{ "Constraint Priority",				JPH_RTTI(ConstraintPriorityTest) },
 };
 
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, BoxShapeTest)
@@ -328,8 +330,8 @@ static TestNameAndRTTI sTools[] =
 	{ "Load Snapshot",						JPH_RTTI(LoadSnapshotTest) },
 };
 
-static TestCategory sAllCategories[] = 
-{ 
+static TestCategory sAllCategories[] =
+{
 	{ "General", sGeneralTests, size(sGeneralTests) },
 	{ "Shapes", sShapeTests, size(sShapeTests) },
 	{ "Scaled Shapes", sScaledShapeTests, size(sScaledShapeTests) },
@@ -340,7 +342,7 @@ static TestCategory sAllCategories[] =
 	{ "Vehicle", sVehicleTests, size(sVehicleTests) },
 	{ "Broad Phase", sBroadPhaseTests, size(sBroadPhaseTests) },
 	{ "Convex Collision", sConvexCollisionTests, size(sConvexCollisionTests) },
-	{ "Tools", sTools, size(sTools) } 
+	{ "Tools", sTools, size(sTools) }
 };
 
 //-----------------------------------------------------------------------------
@@ -372,11 +374,11 @@ SamplesApp::SamplesApp()
 
 		// Create UI
 		UIElement *main_menu = mDebugUI->CreateMenu();
-		mDebugUI->CreateTextButton(main_menu, "Select Test", [this]() { 
+		mDebugUI->CreateTextButton(main_menu, "Select Test", [this]() {
 			UIElement *tests = mDebugUI->CreateMenu();
 			for (TestCategory &c : sAllCategories)
 			{
-				mDebugUI->CreateTextButton(tests, c.mName, [=]() { 
+				mDebugUI->CreateTextButton(tests, c.mName, [=]() {
 					UIElement *category = mDebugUI->CreateMenu();
 					for (uint j = 0; j < c.mNumTests; ++j)
 						mDebugUI->CreateTextButton(category, c.mTests[j].mName, [=]() { StartTest(c.mTests[j].mRTTI); });
@@ -396,7 +398,7 @@ SamplesApp::SamplesApp()
 		mNextTestButton->SetDisabled(true);
 		mDebugUI->CreateTextButton(main_menu, "Take Snapshot", [this]() { TakeSnapshot(); });
 		mDebugUI->CreateTextButton(main_menu, "Take And Reload Snapshot", [this]() { TakeAndReloadSnapshot(); });
-		mDebugUI->CreateTextButton(main_menu, "Physics Settings", [this]() { 
+		mDebugUI->CreateTextButton(main_menu, "Physics Settings", [this]() {
 			UIElement *phys_settings = mDebugUI->CreateMenu();
 			mDebugUI->CreateSlider(phys_settings, "Max Concurrent Jobs", float(mMaxConcurrentJobs), 1, float(thread::hardware_concurrency()), 1, [this](float inValue) { mMaxConcurrentJobs = (int)inValue; });
 			mDebugUI->CreateSlider(phys_settings, "Gravity (m/s^2)", -mPhysicsSystem->GetGravity().GetY(), 0.0f, 20.0f, 1.0f, [this](float inValue) { mPhysicsSystem->SetGravity(Vec3(0, -inValue, 0)); });
@@ -427,7 +429,7 @@ SamplesApp::SamplesApp()
 			mDebugUI->ShowMenu(phys_settings);
 		});
 	#ifdef JPH_DEBUG_RENDERER
-		mDebugUI->CreateTextButton(main_menu, "Drawing Options", [this]() { 
+		mDebugUI->CreateTextButton(main_menu, "Drawing Options", [this]() {
 			UIElement *drawing_options = mDebugUI->CreateMenu();
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Shapes (H)", mBodyDrawSettings.mDrawShape, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawShape = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Shapes Wireframe (Alt+W)", mBodyDrawSettings.mDrawShapeWireframe, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawShapeWireframe = inState == UICheckBox::STATE_CHECKED; });
@@ -464,7 +466,7 @@ SamplesApp::SamplesApp()
 			mDebugUI->ShowMenu(drawing_options);
 		});
 	#endif // JPH_DEBUG_RENDERER
-		mDebugUI->CreateTextButton(main_menu, "Mouse Probe", [this]() { 
+		mDebugUI->CreateTextButton(main_menu, "Mouse Probe", [this]() {
 			UIElement *probe_options = mDebugUI->CreateMenu();
 			mDebugUI->CreateComboBox(probe_options, "Mode", { "Pick", "Ray", "RayCollector", "CollidePoint", "CollideShape", "CastShape", "TransfShape", "GetTriangles", "BP Ray", "BP Box", "BP Sphere", "BP Point", "BP OBox", "BP Cast Box" }, (int)mProbeMode, [this](int inItem) { mProbeMode = (EProbeMode)inItem; });
 			mDebugUI->CreateComboBox(probe_options, "Shape", { "Sphere", "Box", "ConvexHull", "Capsule", "TaperedCapsule", "Cylinder", "Triangle", "StaticCompound", "StaticCompound2", "MutableCompound", "Mesh" }, (int)mProbeShape, [=](int inItem) { mProbeShape = (EProbeShape)inItem; });
@@ -483,7 +485,7 @@ SamplesApp::SamplesApp()
 			mDebugUI->CreateSlider(probe_options, "Max Hits", float(mMaxHits), 0, 10, 1, [this](float inValue) { mMaxHits = (int)inValue; });
 			mDebugUI->ShowMenu(probe_options);
 		});
-		mDebugUI->CreateTextButton(main_menu, "Shoot Object", [this]() { 
+		mDebugUI->CreateTextButton(main_menu, "Shoot Object", [this]() {
 			UIElement *shoot_options = mDebugUI->CreateMenu();
 			mDebugUI->CreateTextButton(shoot_options, "Shoot Object (B)", [=]() { ShootObject(); });
 			mDebugUI->CreateSlider(shoot_options, "Initial Velocity", mShootObjectVelocity, 0.0f, 500.0f, 10.0f, [this](float inValue) { mShootObjectVelocity = inValue; });
@@ -544,7 +546,7 @@ SamplesApp::SamplesApp()
 						test = t.mRTTI;
 						break;
 					}
-				}		
+				}
 
 			// Construct test
 			StartTest(test);
@@ -617,7 +619,7 @@ void SamplesApp::StartTest(const RTTI *inRTTI)
 		mPhysicsSystem->SetContactListener(mTest->GetContactListener());
 	}
 	mTest->Initialize();
-				
+
 	// Optimize the broadphase to make the first update fast
 	mPhysicsSystem->OptimizeBroadPhase();
 
@@ -1053,7 +1055,7 @@ bool SamplesApp::CastProbe(float inProbeLength, float &outFraction, RVec3 &outPo
 				outPosition = ray.GetPointOnRay(first_hit.mFraction);
 				outFraction = first_hit.mFraction;
 				outID = first_hit.mBodyID;
-	
+
 				// Draw results
 				RVec3 prev_position = start;
 				bool c = false;
@@ -1271,7 +1273,7 @@ bool SamplesApp::CastProbe(float inProbeLength, float &outFraction, RVec3 &outPo
 					hits.resize(mMaxHits);
 			}
 
-			had_hit = !hits.empty();		
+			had_hit = !hits.empty();
 			if (had_hit)
 			{
 				// Fill in results
@@ -1660,7 +1662,7 @@ void SamplesApp::UpdateDebug()
 	const float cDragRayLength = 40.0f;
 
 	BodyInterface &bi = mPhysicsSystem->GetBodyInterface();
-			
+
 	// Handle keyboard input for which simulation needs to be running
 	for (int key = mKeyboard->GetFirstKey(); key != 0; key = mKeyboard->GetNextKey())
 		switch (key)
@@ -1752,7 +1754,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 
 	// Get the status string
 	mStatusString = mTest->GetStatusString();
-		
+
 	// Select the next test if automatic testing times out
 	if (!CheckNextTest())
 		return false;
@@ -1804,7 +1806,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 		case DIK_3:
 			ContactConstraintManager::sDrawContactPointReduction = !ContactConstraintManager::sDrawContactPointReduction;
 			break;
-				
+
 		case DIK_C:
 			mDrawConstraints = !mDrawConstraints;
 			break;
@@ -1914,7 +1916,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 			// Restore state to what it was during that time
 			StateRecorderImpl &recorder = mPlaybackFrames[mCurrentPlaybackFrame];
 			RestoreState(recorder);
-				
+
 			// Physics world is drawn using debug lines, when not paused
 			// Draw state prior to step so that debug lines are created from the same state
 			// (the constraints are solved on the current state and then the world is stepped)
@@ -1945,7 +1947,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 		if (mCurrentPlaybackFrame == 0)
 			mPlaybackMode = EPlaybackMode::Stop;
 	}
-	else 
+	else
 	{
 		// Normal update
 		JPH_ASSERT(mCurrentPlaybackFrame == -1);
@@ -2061,7 +2063,7 @@ void SamplesApp::DrawPhysics()
 
 						// Start iterating all triangles of the shape
 						Shape::GetTrianglesContext context;
-						transformed_shape.mShape->GetTrianglesStart(context, AABox::sBiggest(), Vec3::sZero(), Quat::sIdentity(), Vec3::sReplicate(1.0f));						
+						transformed_shape.mShape->GetTrianglesStart(context, AABox::sBiggest(), Vec3::sZero(), Quat::sIdentity(), Vec3::sReplicate(1.0f));
 						for (;;)
 						{
 							// Get the next batch of vertices
@@ -2262,13 +2264,13 @@ void SamplesApp::GetInitialCamera(CameraState &ioState) const
 }
 
 RMat44 SamplesApp::GetCameraPivot(float inCameraHeading, float inCameraPitch) const
-{ 
-	return mTest->GetCameraPivot(inCameraHeading, inCameraPitch); 
+{
+	return mTest->GetCameraPivot(inCameraHeading, inCameraPitch);
 }
 
 float SamplesApp::GetWorldScale() const
-{ 
-	return mTest != nullptr? mTest->GetWorldScale() : 1.0f; 
+{
+	return mTest != nullptr? mTest->GetWorldScale() : 1.0f;
 }
 
 ENTRY_POINT(SamplesApp, RegisterCustomMemoryHook)

+ 56 - 0
Samples/Tests/Constraints/ConstraintPriorityTest.cpp

@@ -0,0 +1,56 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Constraints/ConstraintPriorityTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+#include <Renderer/DebugRendererImp.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(ConstraintPriorityTest)
+{
+	JPH_ADD_BASE_CLASS(ConstraintPriorityTest, Test)
+}
+
+void ConstraintPriorityTest::Initialize()
+{
+	float box_size = 1.0f;
+	RefConst<Shape> box = new BoxShape(Vec3(0.5f * box_size, 0.2f, 0.2f));
+
+	const int num_bodies = 20;
+
+	// Bodies attached through fixed constraints
+	for (int priority = 0; priority < 2; ++priority)
+	{
+		RVec3 position(0, 10.0f, 0.2f * priority);
+		Body &top = *mBodyInterface->CreateBody(BodyCreationSettings(box, position, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+		mBodyInterface->AddBody(top.GetID(), EActivation::DontActivate);
+
+		Body *prev = &top;
+		for (int i = 1; i < num_bodies; ++i)
+		{
+			position += Vec3(box_size, 0, 0);
+
+			Body &segment = *mBodyInterface->CreateBody(BodyCreationSettings(box, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::NON_MOVING)); // Putting all bodies in the NON_MOVING layer so they won't collide
+			mBodyInterface->AddBody(segment.GetID(), EActivation::Activate);
+
+			FixedConstraintSettings settings;
+			settings.mAutoDetectPoint = true;
+			settings.mConstraintPriority = priority == 0? i : num_bodies - i; // Priority is reversed for one chain compared to the other
+			Ref<Constraint> c = settings.Create(*prev, segment);
+			mPhysicsSystem->AddConstraint(c);
+			mConstraints.push_back(static_cast<FixedConstraint *>(c.GetPtr()));
+
+			prev = &segment;
+		}
+	}
+}
+
+void ConstraintPriorityTest::PostPhysicsUpdate(float inDeltaTime)
+{
+	for (FixedConstraint *c : mConstraints)
+		mDebugRenderer->DrawText3D(0.5f * (c->GetBody1()->GetCenterOfMassPosition() + c->GetBody2()->GetCenterOfMassPosition()), StringFormat("Priority: %d", c->GetConstraintPriority()), Color::sWhite, 0.2f);
+}

+ 22 - 0
Samples/Tests/Constraints/ConstraintPriorityTest.h

@@ -0,0 +1,22 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+#include <Jolt/Physics/Constraints/FixedConstraint.h>
+
+// Tests constraint priority system to demonstrate that the order of solving can have an effect on the stiffness of the system
+class ConstraintPriorityTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, ConstraintPriorityTest)
+
+	// See: Test
+	virtual void				Initialize() override;
+	virtual void				PostPhysicsUpdate(float inDeltaTime) override;
+
+private:
+	Array<Ref<FixedConstraint>>	mConstraints;
+};