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