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 }