1 module re.phys.rigid3d; 2 3 version (physics) { 4 import re.ecs.component; 5 import re.ecs.updatable; 6 import re.math; 7 import re.time; 8 import re.core; 9 import re.ng.manager; 10 import re.ng.scene; 11 import re.phys.collider; 12 import re.util.dual_map; 13 import re.util.dlib; 14 import std.math; 15 import std.string : format; 16 import std.typecons; 17 18 import geom = dmech.geometry; 19 import rb = dmech.rigidbody; 20 import dm_ray = dmech.raycast; 21 import mech = dmech.world; 22 import shape = dmech.shape; 23 import dl_vec = dlib.math.vector; 24 import dl_quat = dlib.math.quaternion; 25 import dl_mat = dlib.math.matrix; 26 import dlib.container.array; 27 28 /// represents a manager for physics bodies 29 class PhysicsManager : Manager { 30 /// the maximum number of collisions to support in this world 31 public int max_collisions = 1024; 32 /// position correction iterations used by physics engine 33 public int pos_correction_iterations = 20; 34 /// velocity correction iterations used by physics engine 35 public int vel_correction_iterations = 40; 36 /// gravity in the physics world 37 public Vector3 gravity = Vector3(0.0f, -9.80665f, 0.0f); // earth's gravity 38 39 /// the internal dmech physics world 40 private mech.PhysicsWorld world; 41 42 /// physics target timestep 43 private float _timestep; 44 /// used to track time to keep physics timestep fixed 45 private float _phys_time = 0; 46 47 private PhysicsBody[rb.RigidBody] _bodies; 48 49 /// the number of dynamic bodies in this physics world 50 @property public size_t dynamic_body_count() { 51 return world.dynamicBodies.length; 52 } 53 54 /// the number of static bodies in this physics world 55 @property public size_t static_body_count() { 56 return world.staticBodies.length; 57 } 58 59 /// the total number of bodies in this physics world 60 @property public size_t body_count() { 61 return dynamic_body_count + static_body_count; 62 } 63 64 override void setup() { 65 /// allocate resources to run physics 66 import dlib.core.memory : New; 67 68 world = New!(mech.PhysicsWorld)(null, max_collisions); 69 world.broadphase = true; 70 world.positionCorrectionIterations = pos_correction_iterations; 71 world.constraintIterations = vel_correction_iterations; 72 world.gravity = convert_vec3(gravity); 73 _timestep = 1f / Core.target_fps; // set target _timestep 74 } 75 76 override void destroy() { 77 import dlib.core.memory : Delete; 78 79 Delete(world); 80 } 81 82 override void update() { 83 // step the simulation 84 _phys_time += Time.delta_time; 85 // TODO: should this be while? 86 if (_phys_time >= _timestep) { 87 _phys_time -= _timestep; 88 89 // sync FROM bodies: physical properties (mass, inertia) 90 // sync TO bodies: transforms, momentum 91 foreach (comp; _bodies.byValue()) { 92 rb.RigidBody bod = comp._phys_body; 93 94 // sync properties -> physics engine 95 if (abs(bod.mass - comp.mass) > float.epsilon) { 96 bod.mass = comp.mass; 97 bod.invMass = 1f / comp.mass; 98 // TODO: update mass contribtion of shapes? 99 // foreach (shape; body_comp._phys_shapes) { 100 101 // } 102 } 103 // TODO: sync inertia? right now it's automatically set from mass 104 105 // sync velocity 106 bod.linearVelocity = convert_vec3(comp.velocity); 107 bod.angularVelocity = convert_vec3(comp.angular_velocity); 108 bod.maxSpeed = comp.max_speed; 109 110 // apply forces and impulses 111 // these are queued up, then we apply them all to the object 112 foreach (force; comp._forces) { 113 bod.applyForceAtPos(convert_vec3(force.value), 114 convert_vec3(force.pos) + bod.position); 115 } 116 comp._forces.removeBack(cast(uint) comp._forces.length); 117 foreach (impulse; comp._impulses) { 118 bod.applyImpulse(convert_vec3(impulse.value), 119 convert_vec3(impulse.pos) + bod.position); 120 } 121 comp._impulses.removeBack(cast(uint) comp._impulses.length); 122 foreach (torque; comp._torques) { 123 bod.applyTorque(convert_vec3(torque)); 124 } 125 comp._torques.removeBack(cast(uint) comp._torques.length); 126 } 127 128 // sync options to world 129 world.gravity = convert_vec3(gravity); 130 131 world.update(_timestep); 132 133 foreach (comp; _bodies.byValue()) { 134 rb.RigidBody bod = comp._phys_body; 135 136 // sync physics engine -> components 137 138 // sync position 139 auto bod_pos = bod.position; 140 comp.transform.position = convert_vec3(bod_pos); 141 142 // sync rotation/orientation 143 auto bod_rot = bod.orientation; 144 comp.transform.orientation = convert_quat(bod_rot); 145 146 // sync velocity 147 comp.velocity = convert_vec3(bod.linearVelocity); 148 comp.angular_velocity = convert_vec3(bod.angularVelocity); 149 } 150 } 151 } 152 153 /// register all colliders in this body 154 private void register_colliders(PhysicsBody body_comp) { 155 // add colliders to the physics world 156 import dlib.core.memory : New; 157 158 auto bod = body_comp._phys_body; 159 160 auto box_colliders = body_comp.entity.get_components!BoxCollider(); 161 162 foreach (box; box_colliders) { 163 auto box_geom = New!(geom.GeomBox)(world, convert_vec3(box.size)); 164 auto shape = world.addShapeComponent(bod, box_geom, 165 convert_vec3(box.offset), bod.mass); 166 body_comp._shapes[shape] = box; 167 } 168 } 169 170 /// unregister all colliders in this body 171 private void unregister_colliders(PhysicsBody body_comp) { 172 import std.range : front; 173 import std.algorithm : countUntil, remove; 174 175 // we need to remove from the world each collider that we internally have registered to that body 176 foreach (shape; body_comp._shapes.byKey()) { 177 world.shapeComponents.removeFirst(shape); 178 } 179 180 // then clear the internal collider list 181 body_comp._shapes.clear(); 182 } 183 184 /// registers a body 185 private void register(PhysicsBody body_comp) { 186 rb.RigidBody bod; 187 switch (body_comp._body_type) { 188 case PhysicsBody.BodyType.Static: 189 bod = world.addStaticBody(convert_vec3(body_comp.transform.position)); 190 break; 191 case PhysicsBody.BodyType.Dynamic: 192 bod = world.addDynamicBody(convert_vec3(body_comp.transform.position), 193 body_comp.mass); 194 break; 195 default: 196 assert(0); 197 } 198 199 bod.orientation = convert_quat(body_comp.transform.orientation); 200 201 // update registration 202 body_comp._phys_body = bod; 203 _bodies[bod] = body_comp; 204 205 // add colliders 206 register_colliders(body_comp); 207 208 // sync properties 209 sync_properties(body_comp); 210 211 // mark as synced 212 body_comp.physics_synced = true; 213 } 214 215 /// unregisters a body 216 private void unregister(PhysicsBody body_comp) { 217 // remove colliders 218 unregister_colliders(body_comp); 219 220 auto bod = body_comp._phys_body; 221 222 // remove body 223 switch (body_comp._body_type) { 224 case PhysicsBody.BodyType.Static: 225 world.staticBodies.removeFirst(bod); 226 break; 227 case PhysicsBody.BodyType.Dynamic: 228 world.dynamicBodies.removeFirst(bod); 229 break; 230 default: 231 assert(0); 232 } 233 234 // mark as unsynced 235 body_comp.physics_synced = false; 236 237 // clear registration 238 _bodies.remove(bod); 239 body_comp._phys_body = null; 240 } 241 242 /// sync a body's colliders in the physics engine, necessary when shapes change 243 private void sync_colliders(PhysicsBody body_comp) { 244 unregister_colliders(body_comp); 245 register_colliders(body_comp); 246 } 247 248 /// synchronize the transform from body to physics engine 249 private void sync_transform(PhysicsBody body_comp) { 250 // sync position 251 body_comp._phys_body.position = convert_vec3(body_comp.transform.position); 252 253 // sync rotation 254 body_comp._phys_body.orientation = convert_quat(body_comp.transform.orientation); 255 } 256 257 /// sync physical properties from body to physics engine 258 private void sync_properties(PhysicsBody body_comp) { 259 auto bod = body_comp._phys_body; 260 if (body_comp.custom_gravity) { 261 bod.useOwnGravity = true; 262 bod.gravity = convert_vec3(body_comp.gravity); 263 } 264 bod.damping = body_comp.damping; 265 bod.bounce = body_comp.bounce; 266 bod.friction = body_comp.friction; 267 } 268 269 /// cast a ray of a given length in a given direction and return the result. null if no hits. 270 public Nullable!RaycastResult raycast(Vector3 start, Vector3 direction, float dist) { 271 import std.algorithm.searching : countUntil; 272 273 dm_ray.CastResult cr; 274 if (world.raycast(convert_vec3(start), convert_vec3(direction), dist, cr, true, true)) { 275 // get matching physics body 276 auto body_comp = _bodies[cr.rbody]; 277 // get matching collider 278 auto collider = body_comp._shapes[cr.shape]; 279 auto res = RaycastResult(convert_vec3(cr.point), 280 convert_vec3(cr.normal), body_comp, collider); 281 return Nullable!RaycastResult(res); 282 } 283 // no result 284 return Nullable!RaycastResult.init; 285 } 286 } 287 288 public struct RaycastResult { 289 Vector3 point; 290 Vector3 normal; 291 PhysicsBody pbody; 292 Collider collider; 293 } 294 295 /// represents a physics body 296 abstract class PhysicsBody : Component { 297 // - references to things in the physics engine 298 private rb.RigidBody _phys_body; 299 // private shape.ShapeComponent[] _phys_shapes; 300 private Collider[shape.ShapeComponent] _shapes; 301 302 // - physical properties 303 /// object mass 304 public float mass = 0; 305 /// moment of inertia 306 // public float inertia = 1; 307 /// max speed of object 308 public float max_speed = float.max; 309 /// current linear velocity of object 310 public Vector3 velocity = Vector3(0, 0, 0); 311 /// current angular velocity of object 312 public Vector3 angular_velocity = Vector3(0, 0, 0); 313 /// damping amount 314 public float damping = 0.5; 315 /// bounce amount 316 public float bounce = 0; 317 /// coefficient of friction 318 public float friction = 0.5; 319 /// whether to use a custom gravity value 320 public bool custom_gravity = false; 321 /// if custom gravity is enabled, the gravity to use 322 public Vector3 gravity = Vector3(0, 0, 0); 323 324 /// whether this body is currently in sync with the physics system 325 public bool physics_synced = false; 326 327 private PhysicsManager _mgr; 328 private BodyType _body_type; 329 330 /// physics body mode: dynamic or static 331 public enum BodyType { 332 Dynamic, 333 Static 334 } 335 336 // - used to queue forces and impulses to be applied by the physics engine 337 private DynamicArray!VecAtPoint _forces; 338 private DynamicArray!Vector3 _torques; 339 private DynamicArray!VecAtPoint _impulses; 340 341 private struct VecAtPoint { 342 Vector3 value; 343 Vector3 pos; 344 } 345 346 /// creates a physics body with a given mass and type 347 this(float mass, BodyType type) { 348 this.mass = mass; 349 _body_type = type; 350 } 351 352 override void setup() { 353 // register the body in the physics manager 354 auto mgr = entity.scene.get_manager!PhysicsManager(); 355 assert(!mgr.isNull, "scene did not have PhysicsManager registered." 356 ~ "please add that to the scene before creating this component."); 357 _mgr = mgr.get; 358 _mgr.register(this); 359 } 360 361 /// apply an impulse to the physics body 362 public void apply_impulse(Vector3 value, Vector3 pos) { 363 _impulses.append(VecAtPoint(value, pos)); 364 } 365 366 /// apply a force to the physics body 367 public void apply_force(Vector3 value, Vector3 pos) { 368 _forces.append(VecAtPoint(value, pos)); 369 } 370 371 /// apply a torque to the physics body 372 public void apply_torque(Vector3 value) { 373 _torques.append(value); 374 } 375 376 /// notify physics engine about new physical properties, such as gravity 377 public void sync_properties() { 378 _mgr.sync_properties(this); 379 } 380 381 /// used to notify the physics engine to update colliders if they have changed 382 public void sync_colliders() { 383 _mgr.sync_colliders(this); 384 } 385 386 /// used to notify the physics engine when transform is directly modified 387 public void sync_transform() { 388 _mgr.sync_transform(this); 389 } 390 391 override void destroy() { 392 _mgr.unregister(this); 393 } 394 } 395 396 /// a dynamic physics body that is affected by forces 397 public class DynamicBody : PhysicsBody { 398 this(float mass = 1f) { 399 super(mass, PhysicsBody.BodyType.Dynamic); 400 } 401 } 402 403 /// a static physics body that is not affected by forces 404 public class StaticBody : PhysicsBody { 405 this(float mass = 1f) { 406 super(mass, PhysicsBody.BodyType.Static); 407 } 408 } 409 410 @("phys-rigid3d-basic") unittest { 411 import re.ng.scene : Scene2D; 412 import re.util.test : test_scene; 413 414 class TestScene : Scene2D { 415 override void on_start() { 416 add_manager(new PhysicsManager()); 417 418 auto nt = create_entity("block"); 419 nt.add_component(new DynamicBody()); 420 } 421 } 422 423 auto test = test_scene(new TestScene()); 424 test.game.run(); 425 426 // check conditions 427 428 test.game.destroy(); 429 } 430 431 @("phys-rigid3d-lifecycle") unittest { 432 import re.ng.scene : Scene2D; 433 import re.util.test : test_scene; 434 import re.ecs.entity : Entity; 435 436 class TestScene : Scene2D { 437 private Entity nt1; 438 439 override void on_start() { 440 add_manager(new PhysicsManager()); 441 442 nt1 = create_entity("one"); 443 nt1.add_component(new DynamicBody()); 444 auto nt2 = create_entity("two"); 445 nt2.add_component(new DynamicBody()); 446 auto nt3 = create_entity("three"); 447 nt3.add_component(new DynamicBody()); 448 } 449 450 public void kill_one() { 451 nt1.destroy(); 452 } 453 } 454 455 auto test = test_scene(new TestScene()); 456 test.game.run(); 457 458 // check conditions 459 auto mgr = test.scene.get_manager!PhysicsManager; 460 assert(!mgr.isNull); 461 assert(mgr.get.body_count == 3, "physics body count does not match"); 462 463 (cast(TestScene) test.scene).kill_one(); 464 assert(mgr.get.body_count == 2, "physics body was not unregistered on component destroy"); 465 466 test.game.destroy(); 467 } 468 469 @("phys-rigid3d-colliders") unittest { 470 import re.ng.scene : Scene2D; 471 import re.util.test : test_scene; 472 import re.ecs.entity : Entity; 473 import std.algorithm : canFind; 474 475 class TestScene : Scene2D { 476 private Entity nt1; 477 478 override void on_start() { 479 add_manager(new PhysicsManager()); 480 481 nt1 = create_entity("block"); 482 nt1.add_component(new BoxCollider(Vector3(1, 1, 1), Vector3Zero)); 483 nt1.add_component(new DynamicBody()); 484 } 485 486 /// inform the physics body that the colliders need to be synced 487 public void reload_colliders() { 488 nt1.get_component!DynamicBody().sync_colliders(); 489 } 490 491 /// remove the existing box collider, and replace with a new one, then reload 492 public void replace_colliders() { 493 nt1.remove_component!BoxCollider(); 494 nt1.add_component(new BoxCollider(Vector3(2, 2, 2), Vector3Zero)); 495 reload_colliders(); 496 } 497 } 498 499 auto test = test_scene(new TestScene()); 500 test.game.run(); 501 502 // check conditions 503 auto scn = cast(TestScene) test.scene; 504 auto mgr = test.scene.get_manager!PhysicsManager.get; 505 auto bod = test.scene.get_entity("block").get_component!DynamicBody(); 506 507 // check that colliders are registered 508 assert(mgr.world.dynamicBodies.data.canFind(bod._phys_body)); 509 auto shape1 = bod._shapes.keys[0]; 510 immutable auto collider1_size_x = (cast(geom.GeomBox)(shape1.geometry)).halfSize.x; 511 assert(collider1_size_x == 1, 512 "collider #1 size from physics engine does not match provided collider size"); 513 514 // sync the colliders, then ensure that the registration is different 515 scn.reload_colliders(); 516 auto shape2 = bod._shapes.keys[0]; 517 assert(shape1 != shape2, 518 "colliders were synced, which was supposed to reset collider registration, but entry was not changed"); 519 520 // replace the colliders 521 scn.replace_colliders(); 522 assert(bod._shapes.length > 0, "registration entry for new collider missing"); 523 auto shape3 = bod._shapes.keys[0]; 524 immutable auto collider3_size_x = (cast(geom.GeomBox)(shape3.geometry)).halfSize.x; 525 assert(collider3_size_x == 2, 526 "collider #3 size from physics engine does not match replaced collider size"); 527 528 test.game.destroy(); 529 } 530 }