TankTest.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
  2. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  3. // SPDX-License-Identifier: MIT
  4. #include <TestFramework.h>
  5. #include <Tests/Vehicle/TankTest.h>
  6. #include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
  7. #include <Jolt/Physics/Collision/RayCast.h>
  8. #include <Jolt/Physics/Collision/CastResult.h>
  9. #include <Jolt/Physics/Collision/Shape/BoxShape.h>
  10. #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
  11. #include <Jolt/Physics/Collision/Shape/SphereShape.h>
  12. #include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
  13. #include <Jolt/Physics/Vehicle/TrackedVehicleController.h>
  14. #include <Jolt/Physics/Collision/GroupFilterTable.h>
  15. #include <Jolt/Physics/Body/BodyCreationSettings.h>
  16. #include <Application/DebugUI.h>
  17. #include <Layers.h>
  18. #include <Renderer/DebugRendererImp.h>
  19. JPH_IMPLEMENT_RTTI_VIRTUAL(TankTest)
  20. {
  21. JPH_ADD_BASE_CLASS(TankTest, VehicleTest)
  22. }
  23. TankTest::~TankTest()
  24. {
  25. mPhysicsSystem->RemoveStepListener(mVehicleConstraint);
  26. }
  27. void TankTest::Initialize()
  28. {
  29. VehicleTest::Initialize();
  30. const float wheel_radius = 0.3f;
  31. const float wheel_width = 0.1f;
  32. const float half_vehicle_length = 3.2f;
  33. const float half_vehicle_width = 1.7f;
  34. const float half_vehicle_height = 0.5f;
  35. const float suspension_min_length = 0.3f;
  36. const float suspension_max_length = 0.5f;
  37. const float suspension_frequency = 1.0f;
  38. const float half_turret_width = 1.4f;
  39. const float half_turret_length = 2.0f;
  40. const float half_turret_height = 0.4f;
  41. const float half_barrel_length = 1.5f;
  42. const float barrel_radius = 0.1f;
  43. const float barrel_rotation_offset = 0.2f;
  44. static Vec3 wheel_pos[] = {
  45. Vec3(0.0f, -0.0f, 2.95f),
  46. Vec3(0.0f, -0.3f, 2.1f),
  47. Vec3(0.0f, -0.3f, 1.4f),
  48. Vec3(0.0f, -0.3f, 0.7f),
  49. Vec3(0.0f, -0.3f, 0.0f),
  50. Vec3(0.0f, -0.3f, -0.7f),
  51. Vec3(0.0f, -0.3f, -1.4f),
  52. Vec3(0.0f, -0.3f, -2.1f),
  53. Vec3(0.0f, -0.0f, -2.75f),
  54. };
  55. // Create filter to prevent body, turret and barrel from colliding
  56. GroupFilter *filter = new GroupFilterTable;
  57. // Create tank body
  58. RVec3 body_position(0, 2, 0);
  59. RefConst<Shape> tank_body_shape = OffsetCenterOfMassShapeSettings(Vec3(0, -half_vehicle_height, 0), new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length))).Create().Get();
  60. BodyCreationSettings tank_body_settings(tank_body_shape, body_position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  61. tank_body_settings.mCollisionGroup.SetGroupFilter(filter);
  62. tank_body_settings.mCollisionGroup.SetGroupID(0);
  63. tank_body_settings.mCollisionGroup.SetSubGroupID(0);
  64. tank_body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  65. tank_body_settings.mMassPropertiesOverride.mMass = 4000.0f;
  66. mTankBody = mBodyInterface->CreateBody(tank_body_settings);
  67. mBodyInterface->AddBody(mTankBody->GetID(), EActivation::Activate);
  68. // Create vehicle constraint
  69. VehicleConstraintSettings vehicle;
  70. vehicle.mDrawConstraintSize = 0.1f;
  71. vehicle.mMaxPitchRollAngle = DegreesToRadians(60.0f);
  72. TrackedVehicleControllerSettings *controller = new TrackedVehicleControllerSettings;
  73. vehicle.mController = controller;
  74. for (int t = 0; t < 2; ++t)
  75. {
  76. VehicleTrackSettings &track = controller->mTracks[t];
  77. // Last wheel is driven wheel
  78. track.mDrivenWheel = (uint)(vehicle.mWheels.size() + size(wheel_pos) - 1);
  79. for (uint wheel = 0; wheel < size(wheel_pos); ++wheel)
  80. {
  81. WheelSettingsTV *w = new WheelSettingsTV;
  82. w->mPosition = wheel_pos[wheel];
  83. w->mPosition.SetX(t == 0? half_vehicle_width : -half_vehicle_width);
  84. w->mRadius = wheel_radius;
  85. w->mWidth = wheel_width;
  86. w->mSuspensionMinLength = suspension_min_length;
  87. w->mSuspensionMaxLength = wheel == 0 || wheel == size(wheel_pos) - 1? suspension_min_length : suspension_max_length;
  88. w->mSuspensionSpring.mFrequency = suspension_frequency;
  89. // Add the wheel to the vehicle
  90. track.mWheels.push_back((uint)vehicle.mWheels.size());
  91. vehicle.mWheels.push_back(w);
  92. }
  93. }
  94. mVehicleConstraint = new VehicleConstraint(*mTankBody, vehicle);
  95. mVehicleConstraint->SetVehicleCollisionTester(new VehicleCollisionTesterRay(Layers::MOVING));
  96. #ifdef JPH_DEBUG_RENDERER
  97. static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetRPMMeter(Vec3(0, 2, 0), 0.5f);
  98. #endif // JPH_DEBUG_RENDERER
  99. mPhysicsSystem->AddConstraint(mVehicleConstraint);
  100. mPhysicsSystem->AddStepListener(mVehicleConstraint);
  101. // Create turret
  102. RVec3 turret_position = body_position + Vec3(0, half_vehicle_height + half_turret_height, 0);
  103. BodyCreationSettings turret_body_setings(new BoxShape(Vec3(half_turret_width, half_turret_height, half_turret_length)), turret_position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  104. turret_body_setings.mCollisionGroup.SetGroupFilter(filter);
  105. turret_body_setings.mCollisionGroup.SetGroupID(0);
  106. turret_body_setings.mCollisionGroup.SetSubGroupID(0);
  107. turret_body_setings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  108. turret_body_setings.mMassPropertiesOverride.mMass = 2000.0f;
  109. mTurretBody = mBodyInterface->CreateBody(turret_body_setings);
  110. mBodyInterface->AddBody(mTurretBody->GetID(), EActivation::Activate);
  111. // Attach turret to body
  112. HingeConstraintSettings turret_hinge;
  113. turret_hinge.mPoint1 = turret_hinge.mPoint2 = body_position + Vec3(0, half_vehicle_height, 0);
  114. turret_hinge.mHingeAxis1 = turret_hinge.mHingeAxis2 = Vec3::sAxisY();
  115. turret_hinge.mNormalAxis1 = turret_hinge.mNormalAxis2 = Vec3::sAxisZ();
  116. turret_hinge.mMotorSettings = MotorSettings(0.5f, 1.0f);
  117. mTurretHinge = static_cast<HingeConstraint *>(turret_hinge.Create(*mTankBody, *mTurretBody));
  118. mTurretHinge->SetMotorState(EMotorState::Position);
  119. mPhysicsSystem->AddConstraint(mTurretHinge);
  120. // Create barrel
  121. RVec3 barrel_position = turret_position + Vec3(0, 0, half_turret_length + half_barrel_length - barrel_rotation_offset);
  122. BodyCreationSettings barrel_body_setings(new CylinderShape(half_barrel_length, barrel_radius), barrel_position, Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING);
  123. barrel_body_setings.mCollisionGroup.SetGroupFilter(filter);
  124. barrel_body_setings.mCollisionGroup.SetGroupID(0);
  125. barrel_body_setings.mCollisionGroup.SetSubGroupID(0);
  126. barrel_body_setings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  127. barrel_body_setings.mMassPropertiesOverride.mMass = 200.0f;
  128. mBarrelBody = mBodyInterface->CreateBody(barrel_body_setings);
  129. mBodyInterface->AddBody(mBarrelBody->GetID(), EActivation::Activate);
  130. // Attach barrel to turret
  131. HingeConstraintSettings barrel_hinge;
  132. barrel_hinge.mPoint1 = barrel_hinge.mPoint2 = barrel_position - Vec3(0, 0, half_barrel_length);
  133. barrel_hinge.mHingeAxis1 = barrel_hinge.mHingeAxis2 = -Vec3::sAxisX();
  134. barrel_hinge.mNormalAxis1 = barrel_hinge.mNormalAxis2 = Vec3::sAxisZ();
  135. barrel_hinge.mLimitsMin = DegreesToRadians(-10.0f);
  136. barrel_hinge.mLimitsMax = DegreesToRadians(40.0f);
  137. barrel_hinge.mMotorSettings = MotorSettings(10.0f, 1.0f);
  138. mBarrelHinge = static_cast<HingeConstraint *>(barrel_hinge.Create(*mTurretBody, *mBarrelBody));
  139. mBarrelHinge->SetMotorState(EMotorState::Position);
  140. mPhysicsSystem->AddConstraint(mBarrelHinge);
  141. // Update camera pivot
  142. mCameraPivot = mTankBody->GetPosition();
  143. }
  144. void TankTest::ProcessInput(const ProcessInputParams &inParams)
  145. {
  146. const float min_velocity_pivot_turn = 1.0f;
  147. // Determine acceleration and brake
  148. mForward = 0.0f;
  149. mBrake = 0.0f;
  150. if (inParams.mKeyboard->IsKeyPressed(EKey::RShift))
  151. mBrake = 1.0f;
  152. else if (inParams.mKeyboard->IsKeyPressed(EKey::Up))
  153. mForward = 1.0f;
  154. else if (inParams.mKeyboard->IsKeyPressed(EKey::Down))
  155. mForward = -1.0f;
  156. // Steering
  157. mLeftRatio = 1.0f;
  158. mRightRatio = 1.0f;
  159. float velocity = (mTankBody->GetRotation().Conjugated() * mTankBody->GetLinearVelocity()).GetZ();
  160. if (inParams.mKeyboard->IsKeyPressed(EKey::Left))
  161. {
  162. if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
  163. {
  164. // Pivot turn
  165. mLeftRatio = -1.0f;
  166. mForward = 1.0f;
  167. }
  168. else
  169. mLeftRatio = 0.6f;
  170. }
  171. else if (inParams.mKeyboard->IsKeyPressed(EKey::Right))
  172. {
  173. if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
  174. {
  175. // Pivot turn
  176. mRightRatio = -1.0f;
  177. mForward = 1.0f;
  178. }
  179. else
  180. mRightRatio = 0.6f;
  181. }
  182. // Check if we're reversing direction
  183. if (mPreviousForward * mForward < 0.0f)
  184. {
  185. // Get vehicle velocity in local space to the body of the vehicle
  186. if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
  187. {
  188. // Brake while we've not stopped yet
  189. mForward = 0.0f;
  190. mBrake = 1.0f;
  191. }
  192. else
  193. {
  194. // When we've come to a stop, accept the new direction
  195. mPreviousForward = mForward;
  196. }
  197. }
  198. // Cast ray to find target
  199. RRayCast ray { inParams.mCameraState.mPos, 1000.0f * inParams.mCameraState.mForward };
  200. RayCastSettings ray_settings;
  201. ClosestHitCollisionCollector<CastRayCollector> collector;
  202. IgnoreMultipleBodiesFilter body_filter;
  203. body_filter.Reserve(3);
  204. body_filter.IgnoreBody(mTankBody->GetID());
  205. body_filter.IgnoreBody(mTurretBody->GetID());
  206. body_filter.IgnoreBody(mBarrelBody->GetID());
  207. mPhysicsSystem->GetNarrowPhaseQuery().CastRay(ray, ray_settings, collector, {}, {}, body_filter);
  208. RVec3 hit_pos = collector.HadHit()? ray.GetPointOnRay(collector.mHit.mFraction) : ray.mOrigin + ray.mDirection;
  209. mDebugRenderer->DrawMarker(hit_pos, Color::sGreen, 1.0f);
  210. // Orient the turret towards the hit position
  211. RMat44 turret_to_world = mTankBody->GetCenterOfMassTransform() * mTurretHinge->GetConstraintToBody1Matrix();
  212. Vec3 hit_pos_in_turret = Vec3(turret_to_world.InversedRotationTranslation() * hit_pos);
  213. mTurretHeading = ATan2(hit_pos_in_turret.GetZ(), hit_pos_in_turret.GetY());
  214. // Orient barrel towards the hit position
  215. RMat44 barrel_to_world = mTurretBody->GetCenterOfMassTransform() * mBarrelHinge->GetConstraintToBody1Matrix();
  216. Vec3 hit_pos_in_barrel = Vec3(barrel_to_world.InversedRotationTranslation() * hit_pos);
  217. mBarrelPitch = ATan2(hit_pos_in_barrel.GetZ(), hit_pos_in_barrel.GetY());
  218. // If user wants to fire
  219. mFire = inParams.mKeyboard->IsKeyPressed(EKey::Return);
  220. }
  221. void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
  222. {
  223. VehicleTest::PrePhysicsUpdate(inParams);
  224. const float bullet_radius = 0.061f; // 120 mm
  225. const Vec3 bullet_pos = Vec3(0, 1.6f, 0);
  226. const Vec3 bullet_velocity = Vec3(0, 400.0f, 0); // Normal exit velocities are around 1100-1700 m/s, use a lower variable as we have a limit to max velocity (See: https://tanks-encyclopedia.com/coldwar-usa-120mm-gun-tank-m1e1-abrams/)
  227. const float bullet_mass = 40.0f; // Normal projectile weight is around 7 kg, use an increased value so the momentum is more realistic (with the lower exit velocity)
  228. const float bullet_reload_time = 2.0f;
  229. // Update camera pivot
  230. mCameraPivot = mTankBody->GetPosition();
  231. // Assure the tank stays active as we're controlling the turret with the mouse
  232. mBodyInterface->ActivateBody(mTankBody->GetID());
  233. // Pass the input on to the constraint
  234. static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetDriverInput(mForward, mLeftRatio, mRightRatio, mBrake);
  235. mTurretHinge->SetTargetAngle(mTurretHeading);
  236. mBarrelHinge->SetTargetAngle(mBarrelPitch);
  237. // Update reload time
  238. mReloadTime = max(0.0f, mReloadTime - inParams.mDeltaTime);
  239. // Shoot bullet
  240. if (mReloadTime == 0.0f && mFire)
  241. {
  242. // Create bullet
  243. BodyCreationSettings bullet_creation_settings(new SphereShape(bullet_radius), mBarrelBody->GetCenterOfMassTransform() * bullet_pos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  244. bullet_creation_settings.mMotionQuality = EMotionQuality::LinearCast;
  245. bullet_creation_settings.mFriction = 1.0f;
  246. bullet_creation_settings.mRestitution = 0.0f;
  247. bullet_creation_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  248. bullet_creation_settings.mMassPropertiesOverride.mMass = bullet_mass;
  249. Body *bullet = mBodyInterface->CreateBody(bullet_creation_settings);
  250. bullet->SetLinearVelocity(mBarrelBody->GetRotation() * bullet_velocity);
  251. mBodyInterface->AddBody(bullet->GetID(), EActivation::Activate);
  252. // Start reloading
  253. mReloadTime = bullet_reload_time;
  254. // Apply opposite impulse to turret body
  255. mBodyInterface->AddImpulse(mTurretBody->GetID(), -bullet->GetLinearVelocity() * bullet_mass);
  256. }
  257. // Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
  258. for (uint w = 0; w < mVehicleConstraint->GetWheels().size(); ++w)
  259. {
  260. const WheelSettings *settings = mVehicleConstraint->GetWheels()[w]->GetSettings();
  261. RMat44 wheel_transform = mVehicleConstraint->GetWheelWorldTransform(w, Vec3::sAxisY(), Vec3::sAxisX()); // The cylinder we draw is aligned with Y so we specify that as rotational axis
  262. mDebugRenderer->DrawCylinder(wheel_transform, 0.5f * settings->mWidth, settings->mRadius, Color::sGreen);
  263. }
  264. }
  265. void TankTest::SaveState(StateRecorder &inStream) const
  266. {
  267. VehicleTest::SaveState(inStream);
  268. inStream.Write(mReloadTime);
  269. }
  270. void TankTest::RestoreState(StateRecorder &inStream)
  271. {
  272. VehicleTest::RestoreState(inStream);
  273. inStream.Read(mReloadTime);
  274. }
  275. void TankTest::SaveInputState(StateRecorder &inStream) const
  276. {
  277. inStream.Write(mForward);
  278. inStream.Write(mPreviousForward);
  279. inStream.Write(mLeftRatio);
  280. inStream.Write(mRightRatio);
  281. inStream.Write(mBrake);
  282. inStream.Write(mTurretHeading);
  283. inStream.Write(mBarrelPitch);
  284. inStream.Write(mFire);
  285. }
  286. void TankTest::RestoreInputState(StateRecorder &inStream)
  287. {
  288. inStream.Read(mForward);
  289. inStream.Read(mPreviousForward);
  290. inStream.Read(mLeftRatio);
  291. inStream.Read(mRightRatio);
  292. inStream.Read(mBrake);
  293. inStream.Read(mTurretHeading);
  294. inStream.Read(mBarrelPitch);
  295. inStream.Read(mFire);
  296. }
  297. void TankTest::GetInitialCamera(CameraState &ioState) const
  298. {
  299. // Position camera behind tank
  300. ioState.mPos = RVec3(0, 4.0f, 0);
  301. ioState.mForward = Vec3(0, -2.0f, 10.0f).Normalized();
  302. }
  303. RMat44 TankTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const
  304. {
  305. // Pivot is center of tank + a distance away from the tank based on the heading and pitch of the camera
  306. Vec3 fwd = Vec3(Cos(inCameraPitch) * Cos(inCameraHeading), Sin(inCameraPitch), Cos(inCameraPitch) * Sin(inCameraHeading));
  307. return RMat44::sTranslation(mCameraPivot - 10.0f * fwd);
  308. }