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 auto res = RaycastResult(convert_vec3(cr.point), convert_vec3(cr.normal), body_comp); 278 // get matching collider 279 if (cr.shape !is null) { 280 auto col = body_comp._shapes[cr.shape]; 281 res.collider = Nullable!Collider(col); 282 } 283 return Nullable!RaycastResult(res); 284 } 285 // no result 286 return Nullable!RaycastResult.init; 287 } 288 289 /// raycast from a physics body in a certain direction, excluding the source body 290 public Nullable!RaycastResult raycast_from(PhysicsBody source, 291 Vector3 offset, Vector3 direction, float dist) { 292 // we want to exclude this body, so we're temporarily going to disable it 293 source._phys_body.raycastable = false; 294 auto res = raycast(source.transform.position + offset, direction, dist); 295 source._phys_body.raycastable = true; 296 return res; 297 } 298 } 299 300 public struct RaycastResult { 301 Vector3 point; 302 Vector3 normal; 303 PhysicsBody pbody; 304 Nullable!Collider collider; 305 } 306 307 /// represents a physics body 308 abstract class PhysicsBody : Component { 309 // - references to things in the physics engine 310 private rb.RigidBody _phys_body; 311 // private shape.ShapeComponent[] _phys_shapes; 312 private Collider[shape.ShapeComponent] _shapes; 313 314 // - physical properties 315 /// object mass 316 public float mass = 0; 317 /// moment of inertia 318 // public float inertia = 1; 319 /// max speed of object 320 public float max_speed = float.max; 321 /// current linear velocity of object 322 public Vector3 velocity = Vector3(0, 0, 0); 323 /// current angular velocity of object 324 public Vector3 angular_velocity = Vector3(0, 0, 0); 325 /// damping amount 326 public float damping = 0.5; 327 /// bounce amount 328 public float bounce = 0; 329 /// coefficient of friction 330 public float friction = 0.5; 331 /// whether to use a custom gravity value 332 public bool custom_gravity = false; 333 /// if custom gravity is enabled, the gravity to use 334 public Vector3 gravity = Vector3(0, 0, 0); 335 336 /// whether this body is currently in sync with the physics system 337 public bool physics_synced = false; 338 339 private PhysicsManager _mgr; 340 private BodyType _body_type; 341 342 /// physics body mode: dynamic or static 343 public enum BodyType { 344 Dynamic, 345 Static 346 } 347 348 // - used to queue forces and impulses to be applied by the physics engine 349 private DynamicArray!VecAtPoint _forces; 350 private DynamicArray!Vector3 _torques; 351 private DynamicArray!VecAtPoint _impulses; 352 353 private struct VecAtPoint { 354 Vector3 value; 355 Vector3 pos; 356 } 357 358 /// creates a physics body with a given mass and type 359 this(float mass, BodyType type) { 360 this.mass = mass; 361 _body_type = type; 362 } 363 364 override void setup() { 365 // register the body in the physics manager 366 auto mgr = entity.scene.get_manager!PhysicsManager(); 367 assert(!mgr.isNull, "scene did not have PhysicsManager registered." 368 ~ "please add that to the scene before creating this component."); 369 _mgr = mgr.get; 370 _mgr.register(this); 371 } 372 373 /// apply an impulse to the physics body 374 public void apply_impulse(Vector3 value, Vector3 pos) { 375 _impulses.append(VecAtPoint(value, pos)); 376 } 377 378 /// apply a force to the physics body 379 public void apply_force(Vector3 value, Vector3 pos) { 380 _forces.append(VecAtPoint(value, pos)); 381 } 382 383 /// apply a torque to the physics body 384 public void apply_torque(Vector3 value) { 385 _torques.append(value); 386 } 387 388 /// notify physics engine about new physical properties, such as gravity 389 public void sync_properties() { 390 _mgr.sync_properties(this); 391 } 392 393 /// used to notify the physics engine to update colliders if they have changed 394 public void sync_colliders() { 395 _mgr.sync_colliders(this); 396 } 397 398 /// used to notify the physics engine when transform is directly modified 399 public void sync_transform() { 400 _mgr.sync_transform(this); 401 } 402 403 override void destroy() { 404 _mgr.unregister(this); 405 } 406 } 407 408 /// a dynamic physics body that is affected by forces 409 public class DynamicBody : PhysicsBody { 410 this(float mass = 1f) { 411 super(mass, PhysicsBody.BodyType.Dynamic); 412 } 413 } 414 415 /// a static physics body that is not affected by forces 416 public class StaticBody : PhysicsBody { 417 this(float mass = 1f) { 418 super(mass, PhysicsBody.BodyType.Static); 419 } 420 } 421 422 @("phys-rigid3d-basic") unittest { 423 import re.ng.scene : Scene2D; 424 import re.util.test : test_scene; 425 426 class TestScene : Scene2D { 427 override void on_start() { 428 add_manager(new PhysicsManager()); 429 430 auto nt = create_entity("block"); 431 nt.add_component(new DynamicBody()); 432 } 433 } 434 435 auto test = test_scene(new TestScene()); 436 test.game.run(); 437 438 // check conditions 439 440 test.game.destroy(); 441 } 442 443 @("phys-rigid3d-lifecycle") unittest { 444 import re.ng.scene : Scene2D; 445 import re.util.test : test_scene; 446 import re.ecs.entity : Entity; 447 448 class TestScene : Scene2D { 449 private Entity nt1; 450 451 override void on_start() { 452 add_manager(new PhysicsManager()); 453 454 nt1 = create_entity("one"); 455 nt1.add_component(new DynamicBody()); 456 auto nt2 = create_entity("two"); 457 nt2.add_component(new DynamicBody()); 458 auto nt3 = create_entity("three"); 459 nt3.add_component(new DynamicBody()); 460 } 461 462 public void kill_one() { 463 nt1.destroy(); 464 } 465 } 466 467 auto test = test_scene(new TestScene()); 468 test.game.run(); 469 470 // check conditions 471 auto mgr = test.scene.get_manager!PhysicsManager; 472 assert(!mgr.isNull); 473 assert(mgr.get.body_count == 3, "physics body count does not match"); 474 475 (cast(TestScene) test.scene).kill_one(); 476 assert(mgr.get.body_count == 2, "physics body was not unregistered on component destroy"); 477 478 test.game.destroy(); 479 } 480 481 @("phys-rigid3d-colliders") unittest { 482 import re.ng.scene : Scene2D; 483 import re.util.test : test_scene; 484 import re.ecs.entity : Entity; 485 import std.algorithm : canFind; 486 487 class TestScene : Scene2D { 488 private Entity nt1; 489 490 override void on_start() { 491 add_manager(new PhysicsManager()); 492 493 nt1 = create_entity("block"); 494 nt1.add_component(new BoxCollider(Vector3(1, 1, 1), Vector3Zero)); 495 nt1.add_component(new DynamicBody()); 496 } 497 498 /// inform the physics body that the colliders need to be synced 499 public void reload_colliders() { 500 nt1.get_component!DynamicBody().sync_colliders(); 501 } 502 503 /// remove the existing box collider, and replace with a new one, then reload 504 public void replace_colliders() { 505 nt1.remove_component!BoxCollider(); 506 nt1.add_component(new BoxCollider(Vector3(2, 2, 2), Vector3Zero)); 507 reload_colliders(); 508 } 509 } 510 511 auto test = test_scene(new TestScene()); 512 test.game.run(); 513 514 // check conditions 515 auto scn = cast(TestScene) test.scene; 516 auto mgr = test.scene.get_manager!PhysicsManager.get; 517 auto bod = test.scene.get_entity("block").get_component!DynamicBody(); 518 519 // check that colliders are registered 520 assert(mgr.world.dynamicBodies.data.canFind(bod._phys_body)); 521 auto shape1 = bod._shapes.keys[0]; 522 immutable auto collider1_size_x = (cast(geom.GeomBox)(shape1.geometry)).halfSize.x; 523 assert(collider1_size_x == 1, 524 "collider #1 size from physics engine does not match provided collider size"); 525 526 // sync the colliders, then ensure that the registration is different 527 scn.reload_colliders(); 528 auto shape2 = bod._shapes.keys[0]; 529 assert(shape1 != shape2, 530 "colliders were synced, which was supposed to reset collider registration, but entry was not changed"); 531 532 // replace the colliders 533 scn.replace_colliders(); 534 assert(bod._shapes.length > 0, "registration entry for new collider missing"); 535 auto shape3 = bod._shapes.keys[0]; 536 immutable auto collider3_size_x = (cast(geom.GeomBox)(shape3.geometry)).halfSize.x; 537 assert(collider3_size_x == 2, 538 "collider #3 size from physics engine does not match replaced collider size"); 539 540 test.game.destroy(); 541 } 542 }