flock-collision
flock-collision
CPU collision detection and rigid-body resolution for the Flock ECS engine — 2D and 3D, with box / circle-sphere / capsule shapes and full rotational dynamics. GPU acceleration is intentionally out of scope here and will live in a separate library.
Prerequisites
Flock and its native stack — see the Flock prerequisites (brew install sdl3 sdl3_image sdl3_ttf, wgpu-native via ../wgpu-cr). Crystal >= 1.16. The physics core itself is headless (no GPU); only running the demos needs the window/render stack.
Install / build
flock-collision is a sibling shard: its entry point pulls Flock in by relative path (../flock), so no shards install is needed.
crystal spec # headless physics tests
crystal run examples/demo2d.cr # 2D: stacking / bouncing bodies
crystal run examples/demo3d.cr # 3D: falling / resting crates
As a dependency of your own project, add it to shard.yml and require "flock-collision".
Quick start (2D)
require "flock-collision"
app = Flock::App.new
app.add_plugin(Flock::Collision::Physics2DPlugin.new(gravity: Flock::Vec2.new(0, -20)))
app.add_startup do |_world, cmd|
cmd.spawn( # static floor
Flock::Transform2D.at(0, 0),
Flock::Collision::Collider2D.box(10, 0.5),
Flock::Collision::RigidBody2D.static(friction: 0.6))
cmd.spawn( # a bouncing box
Flock::Transform2D.at(0, 5),
Flock::Collision::Collider2D.box(0.5, 0.5),
Flock::Collision::RigidBody2D.dynamic(mass: 1.0, inertia: 0.17, restitution: 0.4))
end
app.add_system(Flock::Schedule::Update) do |world, _cmd|
world.each_event(Flock::Collision::Contact2D) do |c|
# c.a, c.b, c.normal, c.penetration, c.point
end
end
3D is symmetric: Physics3DPlugin, Collider3D (.sphere/.box/.capsule), RigidBody3D (.sphere/.box/.dynamic/.static), Contact3D. In 3D the rotation authority is RigidBody3D.orientation (a quaternion); the step writes each dynamic entity's Transform3D.position + matrix_override.
Colliders carry layer/mask bitfields for filtering and a sensor flag (reports contacts but is not resolved).
Ray casting
hit = Flock::Collision.raycast2d(world, origin, direction, max_distance, mask)
hit = Flock::Collision.raycast3d(world, origin, direction, max_distance, mask)
# => RayHit* { entity, point, normal, distance } or nil
Pipeline (per fixed step)
- Integrate forces (gravity) into velocities
- Broadphase — uniform spatial hash
- Narrowphase — exact tests → contact manifolds (+
Contact*events) - Islands + sleeping — group touching bodies; wake islands touched by motion
- Resolve velocities — warm-started sequential impulse: restitution + Coulomb friction
- Positional correction — Baumgarte / slop
- Integrate velocities into positions + orientation, then update sleep timers
Feature coverage
| 2D | 3D | |
|---|---|---|
| shapes | circle, box, capsule | sphere, box, capsule |
| rotation | full | full (quaternion) |
| narrowphase | all pairs, rotated | all pairs, rotated |
| box manifold | 2 points (clipping) | up to 4 points (face clipping) |
| friction | yes | yes |
| warm starting | yes | yes |
| sleeping / islands | yes | yes |
| joints | distance, spring, pin | distance, spring, pin |
| continuous collision | yes (vs static) | yes (vs static) |
| broadphase | spatial hash / SAP | spatial hash / SAP |
| ray cast | circle, box, capsule | sphere, box, capsule |
Joints, CCD, broadphase
phys = app.world.resource(Flock::Collision::Physics2D)
phys.add_joint(Flock::Collision::Joint2D.pin(anchor, bob)) # distance / spring / pin
# fast bodies opt into continuous collision:
Flock::Collision::RigidBody2D.dynamic(mass: 1.0, ccd: true)
# choose the broadphase (default :hash):
Flock::Collision::Physics2DPlugin.new # ... then
phys.broadphase = Flock::Collision::SweepPrune2D.new # or pass broadphase: :sweep
Known limits / TODO
- 3D box-box uses face clipping (multi-point); sphere/capsule contacts and edge-edge box contacts are single-point.
- Deep capsule-box penetration is resolved exactly with GJK+EPA (also available directly as
Flock::Collision::GJK3D.penetrationfor any convex support pair). - Sleeping wakes down a stack one layer per step; disable with
allow_sleeping: false. Jointed bodies do not sleep. - CCD (swept) handles fast bodies vs static and moving bodies; 3D revolute motors and limits are not provided (2D revolute has both).
Layout
src/flock/collision/
# shared
shape2d/3d collider2d/3d rigid_body2d/3d contact2d/3d
aabb2d/3d broadphase2d/3d obb2d/3d
narrowphase2d/3d solver2d/3d physics2d/3d physics2d/3d_plugin
raycast2d/3d
License
MIT — see LICENSE.
flock-collision
- 0
- 0
- 0
- 0
- 1
- about 7 hours ago
- August 19, 2026
MIT License
Wed, 19 Aug 2026 12:18:25 GMT