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 }