Physics
2D physics here means three things: moving things, noticing when they overlap, and deciding what happens next.
The concept
A simple engine splits collision into a broad step (which pairs might touch), a narrow step (do these two shapes overlap, and by how much) and resolution (push them apart, or just report it). Shapes carry a layer and a mask so, for example, bullets can ignore other bullets.
Why a broad phase
Testing every shape against every other costs O(n²) checks per frame: 1,000 colliders is about half a million pairs, almost all of them far apart. The broad phase throws away the obvious non-pairs cheaply, using only bounding boxes, so the expensive narrow-phase test runs on a handful of candidates.
Quadtrees
A quadtree is a tree that recursively splits 2D space. The root covers the whole world; when a node holds too many elements it is cut into four equal quadrants (NW, NE, SW, SE) and its elements move down into the child that contains them. Nodes in crowded areas keep splitting, empty areas stay as big cheap nodes. An element that straddles a split line fits no single quadrant, so it stays in the parent.
Two things follow. A query ("what overlaps this rect?") skips every subtree whose quadrant does not touch the rect, so it costs roughly O(log n + k) for k results instead of O(n). And a pair search only has to compare an element with the others in its own node and in the nodes above and below it, never with distant quadrants. The price is rebuilding the tree when things move; for a moving scene the usual choice is to rebuild it every frame, which is cheap because inserts are cheap.
How ASGE does it
Components
VelocityandTransform: position changes by velocity each frame.Collider: aRectorCirclehitbox, independent of the sprite, with am_ResolutionofSolid(default, pushed out),Trigger(sensor, fires events, never pushed) orUnknown(ignored), plusm_Layerandm_Maskbitmasks. Two colliders only interact if each one's layer is in the other's mask.Rigidbody: marks an entity as physics-driven (gravity and push-out); an entity needs bothVelocityandRigidbodyto be movable, otherwise it acts as static level geometry.
Systems
These run inside Game::Update in this order (see Game loop & timing):
GravitySystemandMovementSystem: plain Euler integration,position += velocity * dt.CollisionSystem: detection, resolution and trigger events in one pass, described below.
Collision pipeline
- Build the broad phase. Every frame,
CollisionSystemcomputes the rect enclosing all colliders,Resets aspatial::QuadTree<Entity>to that area andInserts each entity with its world-space bounds. A circle is inserted as its bounding rect, so the tree only ever deals in rectangles. - Find candidate pairs.
QuadTree::ForEachPairreports each unordered pair of overlapping bounding rects exactly once, never an element with itself. - Narrow phase, per pair. Pairs whose layer/mask do not match, with an
Unknownresolution, or on the same body are skipped. The rest go through the exact shape test,math::PenetrationVector(rect/rect, circle/circle or rect/circle), which returns a push-out vector or nothing.Solidpairs are pushed apart (mass-weighted; only bodies withVelocityandRigidbodymove).Triggerpairs are recorded instead. - Emit trigger events. Only after the traversal ends, so handlers cannot touch the registry mid-walk,
OnCollisionTriggerEnterandStayfire, thenExitfor last frame's pairs that are gone, usingPhysicsStateto remember them.
The QuadTree
asge::spatial::QuadTree<T> (in Core/Spatial) is a region quadtree over axis-aligned rectangles. A leaf splits when it holds more than 16 elements (s_MaxNofElements), down to a depth of 10 (s_MaxDepth). Each element lives in the deepest node whose quadrant fully contains it. Nodes and element blocks are carved out of a fixed-size arena, so Insert never touches the heap; if the arena or element budget runs out the tree degrades (the system logs a warning) instead of failing. Elements cannot be removed one by one, which is why the physics system rebuilds the tree each frame.
Besides ForEachPair it offers Query by rect, point or circle, and Nearest for the closest element to a point.
using namespace asge::game::components;
auto& collider = registry.GetOrAddComponent<Collider>( entity ).get();
collider.m_Resolution = ResolutionType::Trigger; // sensor: reports overlaps, never pushes
collider.m_Layer = 0b01; // I live on layer 1...
collider.m_Mask = 0b10; // ...and only care about layer 2
See also
Source: Game/Systems/PhysicsSystem.hpp, Game/Components/Collider.hpp, Core/Math/Geometry/Collision.hpp, Core/Spatial/QuadTree.hpp.
Physics demo · Math