1 module re.phys.nudge; 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 std.math; 14 import std.string : format; 15 static import nudge; 16 import nudge_ext; 17 18 /// represents a manager for bodies in a NudgeRealm 19 class NudgeManager : Manager { 20 private NudgeRealm realm; 21 private uint item_limit = 1024; 22 23 /// dual map from NudgeBody to physics body index 24 private DualMap!(NudgeBody, uint) _body_map; 25 26 /// holds the list of indices that point to colliders owned by a certain body 27 class ColliderRefs { 28 /// the list of collider indices 29 uint[] indices; 30 } 31 32 // map from body component to colliders 33 private DualMap!(NudgeBody, ColliderRefs) _box_collider_map; 34 // private DualMap!(NudgeBody, ColliderRefs) _sphr_collider_map; 35 36 /// checks whether this scene has a nudge manager installed 37 public static bool is_installed(Scene scene) { 38 auto existing = scene.get_manager!NudgeManager(); 39 return !existing.isNull; 40 } 41 42 /// get the nudge manager in a scene 43 public static NudgeManager get_current(Scene scene) { 44 return scene.get_manager!NudgeManager().get; 45 } 46 47 /// enable a nudge manager in a scene 48 public static void install(Scene scene) { 49 auto manager = new NudgeManager(); 50 manager.allocate(); 51 scene.managers ~= manager; 52 } 53 54 @property public size_t body_count() { 55 return _body_map.count; 56 } 57 58 /// allocate resources to run physics 59 public void allocate() { 60 realm = new NudgeRealm(item_limit, item_limit, item_limit); 61 realm.allocate(); 62 _body_map = new DualMap!(NudgeBody, uint); 63 _box_collider_map = new DualMap!(NudgeBody, ColliderRefs); 64 } 65 66 override void destroy() { 67 realm.destroy(); 68 realm = null; 69 _body_map.clear(); 70 _body_map = null; 71 } 72 73 private void simulate() { 74 // TODO: update the stuff in the nudge realm 75 // TODO: consider refactoring into nudge_ext when finished 76 } 77 78 override void update() { 79 simulate(); 80 } 81 82 /// register all colliders in this body 83 private void register_colliders(NudgeBody body_comp) { 84 // we need to use the body's collider list to populate our internal collider registration list 85 // then add the colliders to the realm 86 87 auto body_id = body_comp.nudge_body_id; 88 89 // ensure that nothing is already registered 90 assert(!_box_collider_map.has(body_comp), 91 "a collider registration list already exists for this body. call unregister first."); 92 93 // get the collider list 94 auto box_colliders = body_comp.entity.get_components!BoxCollider(); 95 // auto sphere_colliders = body_comp.entity.get_components!SphereCollider(); 96 97 _box_collider_map.set(body_comp, new ColliderRefs()); 98 99 // add to the realm, and populate our internal registration list 100 foreach (box; box_colliders) { 101 // add collider to realm 102 auto box_index = realm.append_box_collider(body_id, 103 nudge.BoxCollider([box.size.x, box.size.y, box.size.z], 104 0), nudge.Transform([ 105 box.offset.x, box.offset.y, box.offset.z 106 ], 0, [0, 0, 0, 0])); 107 108 // add to registration list 109 _box_collider_map.get(body_comp).indices ~= box_index; 110 } 111 } 112 113 /// unregister all colliders in this body 114 private void unregister_colliders(NudgeBody body_comp) { 115 import std.range : front; 116 import std.algorithm : countUntil, remove; 117 118 // for this, we need to use our internal map of a body's colliders, since its own list may have changed 119 // we need to remove from the realm each collider that we internally have registered to that body 120 // then clear our internal collider list 121 // we don't touch the body's collider list 122 123 auto body_id = body_comp.nudge_body_id; 124 125 // get the registration maps 126 auto box_regs = _box_collider_map.get(body_comp); 127 128 // go through and remove each one from the realm 129 while (box_regs.indices.length > 0) { 130 auto box_ix = box_regs.indices.front; 131 // zero the box 132 realm.clear_box_collider(box_ix); 133 auto tail_index = realm.colliders.boxes.count - 1; 134 if (realm.colliders.boxes.count > 1 && box_ix < tail_index) { 135 realm.swap_box_colliders(box_ix, tail_index); 136 137 // update anything pointing to that (used to be in tail, now is in box_ix) 138 // a. find out what body owns that collider 139 auto owner_body = realm.colliders.boxes.transforms[box_ix].body; 140 // b. get the component 141 auto owner_comp = _body_map.get(owner_body); 142 // c. get the collider list registration for that component 143 auto owner_regs = _box_collider_map.get(owner_comp); 144 // d. replace the box index 145 auto index_of_ref = owner_regs.indices.countUntil(tail_index); 146 owner_regs.indices[index_of_ref] = box_ix; 147 } 148 // pop the tail box 149 realm.pop_last_box_collider(); 150 151 // remove the index from the box regs 152 box_regs.indices = box_regs.indices.remove!(x => x == box_ix); 153 } 154 155 // clear box registrations 156 _box_collider_map.remove(body_comp, box_regs); 157 } 158 159 /// registers a body 160 public void register(NudgeBody body_comp) { 161 auto inertia_inverse = 1 / body_comp.inertia; 162 nudge.BodyProperties properties = nudge.BodyProperties([ 163 inertia_inverse, inertia_inverse, inertia_inverse 164 ], 1 / body_comp.mass); 165 166 // request a body from nudge 167 immutable auto body_id = realm.append_body(NudgeRealm.identity_transform, 168 properties, NudgeRealm.zero_momentum); 169 170 body_comp.nudge_body_id = body_id; 171 body_comp.physics_synced = true; 172 173 // store in map 174 _body_map.set(body_comp, body_id); 175 176 // add colliders 177 register_colliders(body_comp); 178 } 179 180 /// unregisters a body 181 public void unregister(NudgeBody body_comp) { 182 auto body_id = body_comp.nudge_body_id; 183 184 // remove colliders 185 unregister_colliders(body_comp); 186 187 // remove from map 188 _body_map.remove(body_id); 189 190 // - reorganize the bodies array so it is packed again 191 192 // 1. clear the body we are disposing 193 realm.clear_body(body_id); 194 // 2. check if it needs to be swapped 195 auto cleared_slot = body_id; 196 auto tail_index = realm.bodies.count - 1; 197 if (realm.bodies.count > 1 && body_id < tail_index) { 198 // 3. it needs to be swapped: move our now cleared slot to the end 199 // and move the body there back 200 // then update our references to them 201 realm.swap_bodies(body_id, tail_index); 202 // specifically, the body that previously pointed to tail 203 // should now point to the leftward slot, which we just cleared 204 205 // a. get the component's body 206 auto swap_comp = _body_map.get(tail_index); 207 // b. drop the map entry 208 _body_map.remove(swap_comp, tail_index); 209 // c. set the body index 210 swap_comp.nudge_body_id = cleared_slot; 211 // d. update the map 212 _body_map.set(swap_comp, cleared_slot); 213 214 } 215 // 4. our removed body is at the end, we can safely pop 216 realm.pop_last_body(); 217 218 // mark body as unsynced 219 body_comp.nudge_body_id = 0; 220 body_comp.physics_synced = false; 221 } 222 223 /// used to sync a body's properties with the physics system when they change 224 public void refresh(NudgeBody body_comp) { 225 auto body_id = body_comp.nudge_body_id; 226 // update mass 227 realm.bodies.properties[body_id].mass_inverse = (1 / body_comp.mass); 228 229 // update inertia 230 auto inertia_inverse = 1 / body_comp.inertia; 231 realm.bodies.properties[body_id].inertia_inverse = [ 232 inertia_inverse, inertia_inverse, inertia_inverse 233 ]; 234 235 // update colliders 236 // this means, remove all existing colliders we own, and (re)add colliders 237 unregister_colliders(body_comp); 238 register_colliders(body_comp); 239 } 240 } 241 242 /// represents a physics body that uses the nudge physics system 243 class NudgeBody : Component, Updatable { 244 /// reference to the body id inside the nudge realm (used internally by the nudge manager) 245 private uint nudge_body_id; 246 247 /// whether this body is currently in sync with the physics system 248 public bool physics_synced = false; 249 250 private NudgeManager mgr; 251 252 private float _mass; 253 private float _inertia; 254 255 override void setup() { 256 // ensure the nudge system is installed 257 if (!NudgeManager.is_installed(entity.scene)) { 258 NudgeManager.install(entity.scene); 259 } 260 261 mgr = NudgeManager.get_current(entity.scene); 262 263 // register with nudge 264 mgr.register(this); 265 } 266 267 /// gets the body's mass 268 @property public float mass() { 269 return _mass; 270 } 271 272 /// sets the body's mass 273 @property public float mass(float value) { 274 _mass = value; 275 mgr.refresh(this); 276 return value; 277 } 278 279 /// gets the body's moment of inertia 280 @property public float inertia() { 281 return _inertia; 282 } 283 284 /// sets the body's moment of inertia 285 @property public float inertia(float value) { 286 _inertia = value; 287 mgr.refresh(this); 288 return _inertia; 289 } 290 291 void update() { 292 // TODO: copy data from nudge system? 293 // this could also be handled by the manager 294 } 295 296 /// used to notify the physics engine to update colliders if they have changed 297 public void sync_colliders() { 298 mgr.refresh(this); 299 } 300 301 override void destroy() { 302 mgr.unregister(this); 303 } 304 } 305 306 @("phys-nudge-basic") unittest { 307 import re.ng.scene : Scene2D; 308 import re.util.test : test_scene; 309 310 class TestScene : Scene2D { 311 override void on_start() { 312 auto nt = create_entity("block"); 313 // add nudge physics 314 nt.add_component(new NudgeBody()); 315 } 316 } 317 318 auto test = test_scene(new TestScene()); 319 test.game.run(); 320 321 // check conditions 322 323 test.game.destroy(); 324 } 325 326 @("phys-nudge-lifecycle") unittest { 327 import re.ng.scene : Scene2D; 328 import re.util.test : test_scene; 329 import re.ecs.entity : Entity; 330 331 class TestScene : Scene2D { 332 private Entity nt1; 333 334 override void on_start() { 335 nt1 = create_entity("one"); 336 nt1.add_component(new NudgeBody()); 337 auto nt2 = create_entity("two"); 338 nt2.add_component(new NudgeBody()); 339 auto nt3 = create_entity("three"); 340 nt3.add_component(new NudgeBody()); 341 } 342 343 public void kill_one() { 344 nt1.destroy(); 345 } 346 } 347 348 auto test = test_scene(new TestScene()); 349 test.game.run(); 350 351 // check conditions 352 auto mgr = test.scene.get_manager!NudgeManager; 353 assert(!mgr.isNull); 354 assert(mgr.get.body_count == 3, "physics body count does not match"); 355 356 (cast(TestScene) test.scene).kill_one(); 357 assert(mgr.get.body_count == 2, "physics body was not unregistered on component destroy"); 358 359 test.game.destroy(); 360 } 361 362 @("phys-nudge-colliders") unittest { 363 import re.ng.scene : Scene2D; 364 import re.util.test : test_scene; 365 import re.ecs.entity : Entity; 366 367 class TestScene : Scene2D { 368 private Entity nt1; 369 370 override void on_start() { 371 nt1 = create_entity("block"); 372 nt1.add_component(new BoxCollider(Vector3(1, 1, 1), Vector3Zero)); 373 nt1.add_component(new NudgeBody()); 374 } 375 376 /// inform the physics body that the colliders need to be synced 377 public void reload_colliders() { 378 nt1.get_component!NudgeBody().sync_colliders(); 379 } 380 381 /// remove the existing box collider, and replace with a new one, then reload 382 public void replace_colliders() { 383 nt1.remove_component!BoxCollider(); 384 nt1.add_component(new BoxCollider(Vector3(2, 2, 2), Vector3Zero)); 385 reload_colliders(); 386 } 387 } 388 389 auto test = test_scene(new TestScene()); 390 test.game.run(); 391 392 // check conditions 393 auto scn = cast(TestScene) test.scene; 394 auto mgr = test.scene.get_manager!NudgeManager.get; 395 auto bod = test.scene.get_entity("block").get_component!NudgeBody(); 396 397 // check that colliders are registered 398 assert(mgr._box_collider_map.has(bod), "missing box collider registration entry for body"); 399 auto bod_reg1 = mgr._box_collider_map.get(bod); 400 assert(bod_reg1.indices.length > 0, "registration entry exists, but no index for collider"); 401 auto collider1_ix = bod_reg1.indices[0]; 402 immutable auto collider1_size_x = mgr.realm.colliders.boxes.data[collider1_ix].size[0]; 403 assert(collider1_size_x == 1, 404 "collider #1 size from physics engine does not match provided collider size"); 405 406 // sync the colliders, then ensure that the registration is different 407 scn.reload_colliders(); 408 auto bod_reg2 = mgr._box_collider_map.get(bod); 409 assert(bod_reg1 != bod_reg2, 410 "colliders were synced, which was supposed to reset collider registration, but entry was not changed"); 411 412 // replace the colliders 413 scn.replace_colliders(); 414 auto bod_reg3 = mgr._box_collider_map.get(bod); 415 assert(bod_reg3.indices.length > 0, "registration entry for new collider missing"); 416 auto collider3_ix = bod_reg3.indices[0]; 417 immutable auto collider3_size_x = mgr.realm.colliders.boxes.data[collider3_ix].size[0]; 418 assert(collider3_size_x == 2, 419 "collider #3 size from physics engine does not match replaced collider size"); 420 421 test.game.destroy(); 422 } 423 }