ContactListenerTests.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  2. // SPDX-License-Identifier: MIT
  3. #include "UnitTestFramework.h"
  4. #include "PhysicsTestContext.h"
  5. #include "Layers.h"
  6. #include "LoggingContactListener.h"
  7. TEST_SUITE("ContactListenerTests")
  8. {
  9. // Gravity vector
  10. const Vec3 cGravity = Vec3(0.0f, -9.81f, 0.0f);
  11. using LogEntry = LoggingContactListener::LogEntry;
  12. using EType = LoggingContactListener::EType;
  13. // Let a sphere bounce on the floor with restition = 1
  14. TEST_CASE("TestContactListenerElastic")
  15. {
  16. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  17. const float cSimulationTime = 1.0f;
  18. const Vec3 cDistanceTraveled = c.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  19. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  20. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  21. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  22. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  23. // Register listener
  24. LoggingContactListener listener;
  25. c.GetSystem()->SetContactListener(&listener);
  26. // Create sphere
  27. Body &floor = c.CreateFloor();
  28. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  29. body.SetRestitution(1.0f);
  30. CHECK(floor.GetID() < body.GetID());
  31. // Simulate until at floor
  32. c.Simulate(cSimulationTime);
  33. // Assert collision not yet processed
  34. CHECK(listener.GetEntryCount() == 0);
  35. // Simulate one more step to process the collision
  36. c.Simulate(c.GetDeltaTime());
  37. // We expect a validate and a contact point added message
  38. CHECK(listener.GetEntryCount() == 2);
  39. if (listener.GetEntryCount() == 2)
  40. {
  41. // Check validate callback
  42. const LogEntry &validate = listener.GetEntry(0);
  43. CHECK(validate.mType == EType::Validate);
  44. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  45. CHECK(validate.mBody2 == floor.GetID());
  46. // Check add contact callback
  47. const LogEntry &add_contact = listener.GetEntry(1);
  48. CHECK(add_contact.mType == EType::Add);
  49. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID should be first
  50. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  51. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID should be second
  52. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  53. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  54. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  55. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  56. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  57. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  58. }
  59. listener.Clear();
  60. // Simulate same time, with a fully elastic body we should reach the initial position again
  61. c.Simulate(cSimulationTime);
  62. // We should only have a remove contact point
  63. CHECK(listener.GetEntryCount() == 1);
  64. if (listener.GetEntryCount() == 1)
  65. {
  66. // Check remove contact callback
  67. const LogEntry &remove = listener.GetEntry(0);
  68. CHECK(remove.mType == EType::Remove);
  69. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID should be first
  70. CHECK(remove.mBody2 == body.GetID()); // Highest ID should be second
  71. }
  72. }
  73. // Let a sphere fall on the floor with restition = 0, then give it horizontal velocity, then take it away from the floor
  74. TEST_CASE("TestContactListenerInelastic")
  75. {
  76. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  77. const float cSimulationTime = 1.0f;
  78. const Vec3 cDistanceTraveled = c.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  79. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  80. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  81. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  82. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  83. // Register listener
  84. LoggingContactListener listener;
  85. c.GetSystem()->SetContactListener(&listener);
  86. // Create sphere
  87. Body &floor = c.CreateFloor();
  88. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  89. body.SetRestitution(0.0f);
  90. body.SetAllowSleeping(false);
  91. CHECK(floor.GetID() < body.GetID());
  92. // Simulate until at floor
  93. c.Simulate(cSimulationTime);
  94. // Assert collision not yet processed
  95. CHECK(listener.GetEntryCount() == 0);
  96. // Simulate one more step to process the collision
  97. c.Simulate(c.GetDeltaTime());
  98. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  99. // We expect a validate and a contact point added message
  100. CHECK(listener.GetEntryCount() == 2);
  101. if (listener.GetEntryCount() == 2)
  102. {
  103. // Check validate callback
  104. const LogEntry &validate = listener.GetEntry(0);
  105. CHECK(validate.mType == EType::Validate);
  106. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  107. CHECK(validate.mBody2 == floor.GetID());
  108. // Check add contact callback
  109. const LogEntry &add_contact = listener.GetEntry(1);
  110. CHECK(add_contact.mType == EType::Add);
  111. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID first
  112. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  113. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID second
  114. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  115. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  116. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  117. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  118. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  119. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  120. }
  121. listener.Clear();
  122. // Simulate 10 steps
  123. c.Simulate(10 * c.GetDeltaTime());
  124. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  125. // We're not moving, we should have persisted contacts only
  126. CHECK(listener.GetEntryCount() == 10);
  127. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  128. {
  129. // Check persist callback
  130. const LogEntry &persist_contact = listener.GetEntry(i);
  131. CHECK(persist_contact.mType == EType::Persist);
  132. CHECK(persist_contact.mBody1 == floor.GetID()); // Lowest ID first
  133. CHECK(persist_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  134. CHECK(persist_contact.mBody2 == body.GetID()); // Highest ID second
  135. CHECK(persist_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  136. CHECK_APPROX_EQUAL(persist_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  137. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  138. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  139. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  140. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  141. }
  142. listener.Clear();
  143. // Make the body able to go to sleep
  144. body.SetAllowSleeping(true);
  145. // Let the body go to sleep
  146. c.Simulate(1.0f);
  147. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  148. // Check it went to sleep and that we received a contact removal callback
  149. CHECK(!body.IsActive());
  150. CHECK(listener.GetEntryCount() > 0);
  151. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  152. {
  153. // Check persist / removed callbacks
  154. const LogEntry &entry = listener.GetEntry(i);
  155. CHECK(entry.mBody1 == floor.GetID());
  156. CHECK(entry.mBody2 == body.GetID());
  157. CHECK(entry.mType == ((i == listener.GetEntryCount() - 1)? EType::Remove : EType::Persist)); // The last entry should remove the contact as the body went to sleep
  158. }
  159. listener.Clear();
  160. // Wake the body up again
  161. c.GetBodyInterface().ActivateBody(body.GetID());
  162. CHECK(body.IsActive());
  163. // Simulate 1 time step to detect the collision with the floor again
  164. c.SimulateSingleStep();
  165. // Check that the contact got readded
  166. CHECK(listener.GetEntryCount() == 2);
  167. CHECK(listener.Contains(EType::Validate, floor.GetID(), body.GetID()));
  168. CHECK(listener.Contains(EType::Add, floor.GetID(), body.GetID()));
  169. listener.Clear();
  170. // Prevent body from going to sleep again
  171. body.SetAllowSleeping(false);
  172. // Make the sphere move horizontal
  173. body.SetLinearVelocity(Vec3::sAxisX());
  174. // Simulate 10 steps
  175. c.Simulate(10 * c.GetDeltaTime());
  176. // We should have 10 persisted contacts events
  177. int validate = 0;
  178. int persisted = 0;
  179. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  180. {
  181. const LogEntry &entry = listener.GetEntry(i);
  182. switch (entry.mType)
  183. {
  184. case EType::Validate:
  185. ++validate;
  186. break;
  187. case EType::Persist:
  188. // Check persist callback
  189. CHECK(entry.mBody1 == floor.GetID()); // Lowest ID first
  190. CHECK(entry.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  191. CHECK(entry.mBody2 == body.GetID()); // Highest ID second
  192. CHECK(entry.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  193. CHECK_APPROX_EQUAL(entry.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  194. CHECK(entry.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  195. CHECK(entry.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  196. CHECK(abs(entry.mManifold.mWorldSpaceContactPointsOn1[0].GetY()) < cPenetrationSlop);
  197. CHECK(abs(entry.mManifold.mWorldSpaceContactPointsOn2[0].GetY()) < cPenetrationSlop);
  198. ++persisted;
  199. break;
  200. case EType::Add:
  201. case EType::Remove:
  202. default:
  203. CHECK(false); // Unexpected event
  204. }
  205. }
  206. CHECK(validate <= 10); // We may receive extra validate callbacks when the object is moving
  207. CHECK(persisted == 10);
  208. listener.Clear();
  209. // Move the sphere away from the floor
  210. c.GetBodyInterface().SetPosition(body.GetID(), cInitialPos, EActivation::Activate);
  211. // Simulate 10 steps
  212. c.Simulate(10 * c.GetDeltaTime());
  213. // We should only have a remove contact point
  214. CHECK(listener.GetEntryCount() == 1);
  215. if (listener.GetEntryCount() == 1)
  216. {
  217. // Check remove contact callback
  218. const LogEntry &remove = listener.GetEntry(0);
  219. CHECK(remove.mType == EType::Remove);
  220. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID first
  221. CHECK(remove.mBody2 == body.GetID()); // Highest ID second
  222. }
  223. }
  224. }