Skip to content

Contacts, Contact Events, and Sensors ​

FxScene::step() has always computed a full contact manifold for every touching pair, then thrown it away. These APIs retain it, so gameplay and RL code can ask "what touched what this step?" without redoing collision math.

Everything here is read-only and refreshed by each step(). The values stay valid until the next step() or reset().

Buffered contacts ​

cpp
const std::vector<FxContact>& contacts() const;

One entry per pair that touched during the step. Each FxContact carries:

FieldMeaning
entity1, entity2the two bodies, as borrowed FxEntity* — see the lifetime note below
countnumber of valid contact points (1 or 2)
positionup to 2 contact points, in world coordinates
normalunit contact normal
penetration_depthpositive when overlapping
jn_accumulated, jt_accumulatednormal and tangent impulse actually applied

Contacts carry solver bookkeeping alongside the geometry — packed body indices, mixed material constants, and the pair's warm-start cache slot. Those fields are internal; read normal, position, penetration_depth and the accumulated impulses, and treat the rest as private. See the collision pipeline for the full field list.

The entity1 / entity2 handles on both contacts and events are raw pointers into the scene, valid until the next step(). Read what you need from them inside the frame; do not store a contact or an event and expect it to stay good. The scene keeps the entities alive for exactly that long, including ones deleted while they were touching, so an end-contact event can still name a body that has just been removed.

A pair is usually found in several substeps. The buffer keeps the last one seen, so the impulses are the ones accumulated by the end of the step.

On impulses. jn_accumulated is the velocity-level impulse — what the solver applied to cancel closing speed. It is large at impact and decays to zero once a body settles: a box resting on the ground reports a zero normal impulse, because nothing is left to cancel. What holds it up from then on is the position-level penetration solve, which is not an impulse. Use it to measure impact strength (hit detection, damage, audio), not to measure resting load.

Begin / end contact events ​

cpp
const std::vector<FxContactEvent>& begin_contact_events() const;
const std::vector<FxContactEvent>& end_contact_events() const;

begin holds pairs touching this step that were not touching last step; end holds pairs that were touching last step and are not now. Each FxContactEvent names entity1, entity2, and is_trigger (true when either is a sensor).

A pair that stays in contact reports begin exactly once, not every step. End events still name their entities even if one was deleted from the scene, because the previous step's buffer holds a shared_ptr to both.

Both lists are sorted by entity id, so repeated runs of the same scene deliver events in the same order.

Sensors (triggers) ​

Set is_sensor on an entity, or sensor: true under physics: in YAML:

yaml
physics:
    mass: 0.0
    gravity_scale: 0.0
    sensor: true

A sensor is detected like any other collider but is skipped by every solver stage: no penetration resolution, no impulses, no warm starting. Bodies pass straight through it. Its overlaps appear in contacts() (with zero impulses) and drive begin/end events with is_trigger == true.

A sensor also never wakes a sleeping body, since it applies no force that could disturb one. An overlap with a sleeper is still reported.

Reading them ​

Events are built before the step callback runs, so callback code sees them:

cpp
scene.set_step_callback([](FxScene& scene, double dt) {
    for (const auto& event : scene.begin_contact_events()) {
        if (event.is_trigger) {
            std::cout << event.entity1->get_name() << " entered "
                      << event.entity2->get_name() << "\n";
        }
    }
});

Reading them straight after step() works equally well:

cpp
scene.step(dt);
for (const auto& contact : scene.contacts()) {
    float impact = std::fabs(contact.jn_accumulated[0]);
    if (impact > kDamageThreshold) apply_damage(contact.entity1, contact.entity2);
}

Not covered yet ​

For ray casts, overlap queries, and shape queries, see spatial queries.

Released under the BSD-3-Clause License.