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