Fx2D
A C++20 2D rigid-body physics engine
Loading...
Searching...
No Matches
Scene.h
Go to the documentation of this file.
1#pragma once
2
3#include "Fx2D/Execution.h"
4#include <algorithm>
5#include <functional>
6#include <iostream>
7#include <memory>
8#include <stdexcept>
9#include <string>
10#include <unordered_map>
11#include <unordered_set>
12#include <utility>
13#include <vector>
14
15#include "Fx2D/Entity.h"
16#include "Fx2D/Input.h"
17#include "Fx2D/Joints.h"
18#include "Fx2D/Math.h"
19#include "Fx2D/Registry.h"
20#include "Fx2D/Solver.h"
21
22// A named set of entities managed as one thing: deleted together, enabled together, and by
23// default exempt from colliding with one another. Membership carries no connectivity or
24// uniformity requirement; builders that populate groups may impose their own.
26 friend class FxScene;
27 std::string m_name;
28 int32_t m_collision_group = 0; // unique negative id when self-collision is off, else 0
29 std::vector<std::shared_ptr<FxEntity>> m_members;
30
31 public:
32 explicit FxEntityGroup(const std::string& name) : m_name(name) {}
33 const std::string& get_name() const { return m_name; }
34 const std::vector<std::shared_ptr<FxEntity>>& members() const { return m_members; }
35 size_t size() const { return m_members.size(); }
36 void set_enabled(bool enabled) {
37 for (auto& e : m_members) {
38 if (!e) continue;
39 e->enabled = enabled;
40 if (enabled) e->wake();
41 }
42 }
43};
44
45// A pair of entities that started or stopped touching during a step.
47 // Borrowed, like FxContact's -- and kept alive by the same pins. Valid until the next
48 // step(); do not hold on to an event past the frame that produced it.
49 FxEntity* entity1 = nullptr;
50 FxEntity* entity2 = nullptr;
51 // True if either entity is a sensor, so no impulse was applied for this pair.
52 bool is_trigger = false;
53};
54
55// Scene class takes care of entities motion and collisions
56class FxScene {
57 private:
58 // Per-pair solver state surviving between substeps and steps. Held in a slot vector rather
59 // than looked up by key each time: at 14 substeps that lookup was the bulk of the step's
60 // hashing.
61 struct FxContactPairSlot {
62 // Warm-start impulses. Scalars only, so the cache never keeps entities alive.
63 float jn[2] = {0.0f, 0.0f};
64 float jt[2] = {0.0f, 0.0f};
65 FxVec2f normal{0.0f, 0.0f}; // contact normal at time of caching, used to detect basis flips
66
67 // Which step this pair was last seen in, and where its contact sits in m_step_contacts
68 // for that step. Together they let the per-substep write-back skip the hash entirely
69 // after the first substep that sees the pair.
70 uint64_t step_stamp = 0;
71 size_t step_slot = kNoStepSlot;
72 };
73 static constexpr size_t kNoStepSlot = static_cast<size_t>(-1);
74
75 // no of entities in the scene can not exceed 4096
76 static constexpr size_t m_enitities_limit = 4096;
77 // max and min time step values that can be use in step method
78 static constexpr double m_max_time_step = 0.06;
79 static constexpr double m_min_time_step = 1e-3;
80 // 14x4 measured fastest among configurations passing the full quality suite;
81 // fewer passes fail tall stacks, more substeps at 8 passes just cost more.
82 size_t m_substeps = 14;
83 size_t m_velocity_passes = 4;
84 // dirty flag to track when any entity is deleted
85 bool m_entities_dirty = false;
86 // total time elapsed since scene start
87 double m_time_elapsed = 0.0;
88 // Pair slots, plus the key -> slot map and the free list that recycles slots of pairs that
89 // stopped touching. A contact carries its slot index, so only the first substep that sees a
90 // pair in a given step pays for a lookup.
91 std::vector<FxContactPairSlot> m_contact_slots;
92 std::unordered_map<uint64_t, uint32_t> m_contact_slot_index;
93 std::vector<uint32_t> m_free_contact_slots;
94 // Slot already resolved for each entry of the current broad-phase pair list, or kNoSlot.
95 // Reset whenever that list is rebuilt; the pair list is stable across the substeps of a
96 // step, so after the first substep a contact finds its slot by index instead of by hash.
97 std::vector<uint32_t> m_pair_slot;
98 // Monotonic step counter, used only to stamp pair slots. Never reset by reset(), because a
99 // stale stamp equal to the current step would revive a dead slot's step_slot.
100 uint64_t m_step_counter = 0;
101
102 // This step's touching pairs and the previous step's, keeping the last occurrence of each
103 // so impulses are final. The previous buffer lets end events still name their entities.
104 std::vector<FxContact> m_step_contacts;
105 std::unordered_map<uint64_t, size_t> m_step_contact_index;
106 std::vector<FxContact> m_prev_contacts;
107 std::unordered_map<uint64_t, size_t> m_prev_contact_index;
108 std::vector<FxContactEvent> m_begin_contacts;
109 std::vector<FxContactEvent> m_end_contacts;
110
111 // Ownership for the borrowed pointers in the contact buffers. Rebuilt once per step and
112 // swapped alongside them, so an entity deleted between steps stays alive exactly as long as
113 // a buffer still names it.
114 std::vector<std::shared_ptr<FxEntity>> m_step_pins;
115 std::vector<std::shared_ptr<FxEntity>> m_prev_pins;
116
117 // Solver-local velocity columns, gathered and scattered once per substep around the
118 // velocity passes. A member so the allocation happens once per scene, not once per step.
119 FxSolverBodies m_solver_bodies;
120 // Per-substep velocity-solver scratch, one entry per live contact.
121 std::vector<FxContactSolverData> m_contact_solver_data;
122
123 // Colour partition of the broad-phase pair list, the colour each contact inherited from its
124 // pair, the contact order that groups them, and the transposed batch the sweeps run over.
125 // All members for their capacity: rebuilt every substep, allocating only on the first.
126 FxContactGraph m_contact_graph;
127 std::vector<uint32_t> m_contact_colors;
128 std::vector<uint32_t> m_colored_contacts;
129 FxContactBatch m_contact_batch;
130
131 // One contiguous run of the batch: a single colour and a single manifold size.
132 struct FxBatchRun {
133 uint32_t begin = 0;
134 uint32_t end = 0;
135 int slots = 2;
136 };
137 std::vector<FxBatchRun> m_batch_runs;
138
139 // Inserts a contact into the current step buffer, replacing any earlier one for the pair.
140 void record_contact(const FxContact& contact);
141 // Takes ownership of every entity the current step buffer names, so the buffer can outlive
142 // a delete_entity call.
143 void pin_contact_entities();
144 // The velocity half of one substep: gather, solve colour by colour, scatter.
145 void solve_contact_velocities(std::vector<FxContact>& contacts,
146 const std::vector<std::shared_ptr<FxEntity>>& entities_vec,
147 size_t iter);
148 // Resolves (or allocates) the pair slot for a contact, stamping the key and slot onto it.
149 // `pair_index` addresses the broad-phase pair list, which is what lets the hash lookup
150 // happen once per pair per step rather than once per pair per substep.
151 void bind_contact_slot(FxContact& contact, size_t pair_index);
152 // Drops slots for pairs that produced no contact this step.
153 void evict_stale_contact_slots();
154 // Diffs the current step buffer against the previous one to build begin/end events.
155 void build_contact_events();
156
157 protected:
158 FxEntityRegistry m_entities; // stores pointers to all entities with collision management
161 FxNamedRegistry<FxJoint> m_joints; // stores all joints
162 FxMouseJoint m_mouse_joint; // the one transient click-drag spring
163 bool m_mouse_drag = false; // drive m_mouse_joint from input() each step
164 // Attaches, moves and releases the mouse joint from the left button and cursor.
166
167 public:
168 // scene size [x, y] units
170 // background color or texture path
171 FxVec4ui8 fillColor{230, 230, 230, 255};
172 std::string fillTexturePath = "";
173 // gravity config [x, y]
174 FxVec2f gravity{0.0f, -9.81f};
175
176 // constructor, destructor
177 FxScene(FxVec2ui SceneSize) : m_entities(m_enitities_limit), size(SceneSize) {}
178 ~FxScene() = default;
179
180 // Restores the captured composition and every entity to its initial state, clearing sleep,
181 // PID, contact and input state. Unconditional: nothing running can decline it.
182 void reset();
183 // Marks the current composition as what reset() restores. Taken on the first step() if not
184 // called explicitly; call it when entities are added or removed before that.
186 // simulation step
187 void step(double step_dt);
188 // get total time elapsed since scene start
189 double time_elapsed() const { return m_time_elapsed; }
190 void set_substeps(const size_t& substeps) { m_substeps = substeps; }
191 // Velocity sweeps per substep; convergence trades against cost jointly with substeps.
192 void set_velocity_passes(size_t passes) { m_velocity_passes = passes; }
193 void set_gravity(const FxVec2f& o_gravity) { gravity = o_gravity; }
194 // custom call back function called after every time step, user gets access to the scene.
195 void set_step_callback(const std::function<void(FxScene&, double dt)>& callback) {
196 m_func_step_callback = callback;
197 }
198 // Called at the end of reset(), once entities are restored, so state layered on top of the
199 // scene can be restored too.
200 void set_reset_callback(const std::function<void(FxScene&)>& callback) {
201 m_func_reset_callback = callback;
202 }
203 // Method to set a fillColor
204 void set_fillColor(const FxVec4ui8& color) {
205 fillColor = color;
206 fillTexturePath = "";
207 }
208 void set_fillTexture(const std::string& filePath) { fillTexturePath = filePath; }
209 // Returns true if added; false if an entity with the name already exists.
210 bool add_entity(const std::shared_ptr<FxEntity>& entity);
211 // Returns true if deletion succeeded, false if the entity wasn't found.
212 bool delete_entity(const std::string& name);
213 // Returns the entity pointer if found; otherwise returns nullptr.
214 std::shared_ptr<FxEntity> get_entity(const std::string& name) const;
215
216 // Returns true if added; false if a constraint with the name already exists
217 bool add_constraint(const std::shared_ptr<FxConstraint>& constraint);
218 // Returns true if deletion succeeded, false if the constraint wasn't found
219 bool delete_constraint(const std::string& name);
220 // Returns the constraint pointer if found; otherwise returns nullptr.
221 std::shared_ptr<FxConstraint> get_constraint(const std::string& name) const;
222
223 // Returns true if added; false if the name is taken or a constraint fails to register, in
224 // which case the joint and any constraints it already added are removed.
225 bool add_joint(const std::shared_ptr<FxJoint>& joint);
226 // Returns true if deletion succeeded, false if the joint wasn't found
227 bool delete_joint(const std::string& name);
228 // Returns the joint pointer if found; otherwise returns nullptr.
229 std::shared_ptr<FxJoint> get_joint(const std::string& name) const;
230
231 // Keyboard and mouse state, filled by a renderer each frame. A headless scene has none
232 // until user code injects it, which is how it scripts triggers.
233 const FxInput& input() const { return m_input; }
234 FxInput& input() { return m_input; }
235
236 // The click-drag spring. Drive it yourself through attach()/set_target()/release(), or
237 // enable_mouse_drag() to have the scene do so from input(): left button on a dynamic
238 // body attaches at the cursor, holding drags, releasing lets go. Off by default; the
239 // YAML key `scene: mouse_drag: true` turns it on.
241 const FxMouseJoint& mouse_joint() const { return m_mouse_joint; }
242 void enable_mouse_drag(bool enable) { m_mouse_drag = enable; }
243 bool mouse_drag_enabled() const { return m_mouse_drag; }
244
245 // Contacts from the most recent step, one per touching pair, in a reproducible but
246 // unspecified order. Sensor pairs appear with zero impulses. Valid until step() or reset().
247 const std::vector<FxContact>& contacts() const { return m_step_contacts; }
248 // Pairs that began touching during the most recent step (not touching in the step before).
249 const std::vector<FxContactEvent>& begin_contact_events() const { return m_begin_contacts; }
250 // Pairs that stopped touching during the most recent step (touching in the step before).
251 const std::vector<FxContactEvent>& end_contact_events() const { return m_end_contacts; }
252
253 // ---------------------------------------------------------------- spatial queries
254 // Overlap runs the simulation's own narrow phase, so queries and contacts agree. Disabled
255 // entities and those without collision geometry are never reported.
256
257 // Nearest entity struck, searching `max_distance` along `direction`. A ray starting inside
258 // a body reports it at distance 0.
259 bool raycast(const FxVec2f& origin, const FxVec2f& direction, float max_distance,
260 FxRayHit& out_hit) const;
261 // Every entity along the ray, nearest first.
262 void raycast_all(const FxVec2f& origin, const FxVec2f& direction, float max_distance,
263 std::vector<FxRayHit>& out_hits) const;
264
265 // Entities overlapping an arbitrary shape placed at `pose` (x, y, theta).
266 void overlap_shape(const FxShape& shape, const FxVec3f& pose,
267 std::vector<std::shared_ptr<FxEntity>>& out) const;
268 // Entities overlapping a circle, an axis-aligned box, or a single point.
269 void overlap_circle(const FxVec2f& centre, float radius,
270 std::vector<std::shared_ptr<FxEntity>>& out) const;
271 void overlap_box(const FxVec2f& centre, const FxVec2f& extents,
272 std::vector<std::shared_ptr<FxEntity>>& out) const;
273 void overlap_point(const FxVec2f& point, std::vector<std::shared_ptr<FxEntity>>& out) const;
274 // First entity covering the point, or nullptr — "what is under the cursor".
275 std::shared_ptr<FxEntity> entity_at_point(const FxVec2f& point) const;
276
277 // Registry access methods
278 size_t entity_count() const { return m_entities.size(); }
279 size_t constraint_count() const { return m_constraints.size(); }
280 size_t joint_count() const { return m_joints.size(); }
281 bool entity_exists(const std::string& name) const {
282 return m_entities.get_rawptr(name) != nullptr;
283 }
284 bool constraint_exists(const std::string& name) const {
285 return m_constraints.get_rawptr(name) != nullptr;
286 }
287 bool joint_exists(const std::string& name) const {
288 return m_joints.get_rawptr(name) != nullptr;
289 }
290
291 // ---------------------------------------------------------------- entity groups
292 // Returns the new group, or nullptr if the name is taken. With self_collide false the
293 // group's members never collide with one another; outsiders are unaffected.
294 std::shared_ptr<FxEntityGroup> create_group(const std::string& name, bool self_collide = false);
295 // Adds the entity to the scene if needed, then to the group.
296 bool add_to_group(const std::shared_ptr<FxEntityGroup>& group,
297 const std::shared_ptr<FxEntity>& entity);
298 std::shared_ptr<FxEntityGroup> get_group(const std::string& name) const {
299 return m_groups.get(name);
300 }
301 // Deletes every member entity, then the group itself.
302 bool delete_group(const std::string& name);
303 size_t group_count() const { return m_groups.size(); }
304 // base if free, else base_1, base_2, ... — for builders generating entity names.
305 std::string unique_entity_name(const std::string& base) const {
306 return m_entities.make_unique_name(base);
307 }
308
309 // Collision pair management (delegated to entity registry)
310 void enable_collision(const std::string& entity1_name, const std::string& entity2_name) {
311 m_entities.enable_collision(entity1_name, entity2_name);
312 }
313 void disable_collision(const std::string& entity1_name, const std::string& entity2_name) {
314 m_entities.disable_collision(entity1_name, entity2_name);
315 }
316
317 // for_each_entity applies the given function on each entity in a given execution mode
318 template<typename ExecPolicy, typename Func>
319 void for_each_entity(ExecPolicy&& policy, Func&& func) {
320 m_entities.for_each(std::forward<ExecPolicy>(policy), std::forward<Func>(func));
321 }
322
323 // transform_entities collects return values vector in a given execution mode
324 template<typename ExecPolicy, typename Func>
326 ExecPolicy&& policy, Func&& func,
327 std::vector<std::invoke_result_t<Func, std::shared_ptr<FxEntity>>>& results) {
328 m_entities.transform(std::forward<ExecPolicy>(policy), std::forward<Func>(func), results);
329 }
330
331 private:
332 // Removes constraints with dead entities
333 void sweep_dead_constraints();
334 // Removes joints with dead entities
335 void sweep_dead_joints();
336
337 // custom callback function invoked in the step method
338 std::function<void(FxScene&, double dt)> m_func_step_callback;
339 std::function<void(FxScene&)> m_func_reset_callback;
340
341 // Composition reset() restores; shared_ptr so a deleted entity can be put back.
342 std::vector<std::shared_ptr<FxEntity>> m_initial_entities;
343 std::vector<std::shared_ptr<FxConstraint>> m_initial_constraints;
344 std::vector<std::shared_ptr<FxJoint>> m_initial_joints;
345 // Membership is copied at capture, since the live group object mutates afterwards.
346 std::vector<std::pair<std::shared_ptr<FxEntityGroup>, std::vector<std::shared_ptr<FxEntity>>>>
347 m_initial_groups;
348 int32_t m_next_collision_group = -1;
349 bool m_has_initial_snapshot = false;
350
351 // Never read by the solver; input reaches the simulation only through user callbacks.
352 FxInput m_input;
353};
A named collection of entities managed as one unit.
Definition Scene.h:25
void set_enabled(bool enabled)
Definition Scene.h:36
const std::string & get_name() const
Definition Scene.h:33
size_t size() const
Definition Scene.h:35
const std::vector< std::shared_ptr< FxEntity > > & members() const
Definition Scene.h:34
FxEntityGroup(const std::string &name)
Definition Scene.h:32
void enable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Registry.h:340
void disable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Registry.h:349
A named rigid body with state, material properties, geometry, and forces.
Definition Entity.h:49
Input state read by gameplay code or injected by a headless producer.
Definition Input.h:28
std::string make_unique_name(const std::string &base) const
Definition Registry.h:114
size_t size() const noexcept
Definition Registry.h:83
std::shared_ptr< T > get(const std::string &name) const
Definition Registry.h:97
T * get_rawptr(const std::string &name) const noexcept
Definition Registry.h:107
void for_each(ExecPolicy &&policy, Func &&func)
Definition Registry.h:183
void transform(ExecPolicy &&policy, Func &&func, std::vector< std::invoke_result_t< Func, std::shared_ptr< T > > > &results)
Definition Registry.h:199
Owns a simulated world and advances it with step().
Definition Scene.h:56
void reset()
const FxVec2ui size
Definition Scene.h:169
void set_gravity(const FxVec2f &o_gravity)
Definition Scene.h:193
FxNamedRegistry< FxConstraint > m_constraints
Definition Scene.h:159
size_t entity_count() const
Definition Scene.h:278
~FxScene()=default
void transform_entities(ExecPolicy &&policy, Func &&func, std::vector< std::invoke_result_t< Func, std::shared_ptr< FxEntity > > > &results)
Definition Scene.h:325
const std::vector< FxContactEvent > & begin_contact_events() const
Definition Scene.h:249
std::shared_ptr< FxEntity > entity_at_point(const FxVec2f &point) const
FxVec4ui8 fillColor
Definition Scene.h:171
bool add_joint(const std::shared_ptr< FxJoint > &joint)
bool delete_joint(const std::string &name)
FxMouseJoint m_mouse_joint
Definition Scene.h:162
void enable_mouse_drag(bool enable)
Definition Scene.h:242
FxScene(FxVec2ui SceneSize)
Definition Scene.h:177
void capture_initial_state()
void step(double step_dt)
void overlap_shape(const FxShape &shape, const FxVec3f &pose, std::vector< std::shared_ptr< FxEntity > > &out) const
void for_each_entity(ExecPolicy &&policy, Func &&func)
Definition Scene.h:319
FxInput & input()
Definition Scene.h:234
void overlap_point(const FxVec2f &point, std::vector< std::shared_ptr< FxEntity > > &out) const
void raycast_all(const FxVec2f &origin, const FxVec2f &direction, float max_distance, std::vector< FxRayHit > &out_hits) const
void set_reset_callback(const std::function< void(FxScene &)> &callback)
Definition Scene.h:200
void overlap_circle(const FxVec2f &centre, float radius, std::vector< std::shared_ptr< FxEntity > > &out) const
double time_elapsed() const
Definition Scene.h:189
void overlap_box(const FxVec2f &centre, const FxVec2f &extents, std::vector< std::shared_ptr< FxEntity > > &out) const
void enable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Scene.h:310
size_t group_count() const
Definition Scene.h:303
bool constraint_exists(const std::string &name) const
Definition Scene.h:284
bool mouse_drag_enabled() const
Definition Scene.h:243
bool m_mouse_drag
Definition Scene.h:163
void set_fillTexture(const std::string &filePath)
Definition Scene.h:208
void drive_mouse_joint()
FxVec2f gravity
Definition Scene.h:174
const FxMouseJoint & mouse_joint() const
Definition Scene.h:241
std::shared_ptr< FxEntityGroup > create_group(const std::string &name, bool self_collide=false)
const std::vector< FxContactEvent > & end_contact_events() const
Definition Scene.h:251
std::shared_ptr< FxConstraint > get_constraint(const std::string &name) const
std::shared_ptr< FxJoint > get_joint(const std::string &name) const
size_t constraint_count() const
Definition Scene.h:279
bool delete_constraint(const std::string &name)
bool joint_exists(const std::string &name) const
Definition Scene.h:287
size_t joint_count() const
Definition Scene.h:280
const FxInput & input() const
Definition Scene.h:233
FxMouseJoint & mouse_joint()
Definition Scene.h:240
void set_substeps(const size_t &substeps)
Definition Scene.h:190
std::shared_ptr< FxEntity > get_entity(const std::string &name) const
std::string unique_entity_name(const std::string &base) const
Definition Scene.h:305
void set_step_callback(const std::function< void(FxScene &, double dt)> &callback)
Definition Scene.h:195
const std::vector< FxContact > & contacts() const
Definition Scene.h:247
void set_velocity_passes(size_t passes)
Definition Scene.h:192
bool entity_exists(const std::string &name) const
Definition Scene.h:281
void set_fillColor(const FxVec4ui8 &color)
Definition Scene.h:204
std::shared_ptr< FxEntityGroup > get_group(const std::string &name) const
Definition Scene.h:298
FxEntityRegistry m_entities
Definition Scene.h:158
bool delete_entity(const std::string &name)
bool add_entity(const std::shared_ptr< FxEntity > &entity)
bool add_constraint(const std::shared_ptr< FxConstraint > &constraint)
std::string fillTexturePath
Definition Scene.h:172
void disable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Scene.h:313
FxNamedRegistry< FxEntityGroup > m_groups
Definition Scene.h:160
bool delete_group(const std::string &name)
bool add_to_group(const std::shared_ptr< FxEntityGroup > &group, const std::shared_ptr< FxEntity > &entity)
FxNamedRegistry< FxJoint > m_joints
Definition Scene.h:161
bool raycast(const FxVec2f &origin, const FxVec2f &direction, float max_distance, FxRayHit &out_hit) const
Single-precision 2D vector.
Definition Math.h:47
Single-precision three-component vector, including poses.
Definition Math.h:159
A begin/end contact event from the latest simulation step.
Definition Scene.h:46
bool is_trigger
Definition Scene.h:52
FxEntity * entity2
Definition Scene.h:50
FxEntity * entity1
Definition Scene.h:49
Contact data produced by the narrow phase and solver.
Definition Solver.h:71
Result from a ray cast against the scene.
Definition Geometry.h:477
Unified circle, capsule, polygon, edge, and chain representation.
Definition Geometry.h:46