Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qtriggerbody_p.h"
#include "physxnode/qphysxtriggerbody_p.h"
#include <QtCore/QPointer>
#include <QtCore/QVarLengthArray>
QT_BEGIN_NAMESPACE
/*!
\qmltype TriggerBody
\inherits PhysicsNode
\inqmlmodule QtQuick3D.Physics
\since 6.4
\brief Reports when objects enter a given volume.
This type defines a trigger body. A trigger body is a body that does not interact
physically but is used to detect when objects intersect with its volume.
*/
/*!
\qmlproperty int TriggerBody::collisionCount
This property returns the number of bodies currently colliding with the trigger body.
A body counts once, however many of its collision shapes are inside the trigger body.
Only a body that had \l {PhysicsNode::sendTriggerReports}{sendTriggerReports} set when it
entered is counted, and it goes on counting for as long as it is inside, whatever it sets
that property to in the meantime.
*/
/*!
\qmlsignal TriggerBody::bodyEntered(PhysicsNode *body)
This signal is emitted when the trigger body is penetrated by the specified \a body.
Emitted once per body, however many of its collision shapes penetrate the trigger body.
*/
/*!
\qmlsignal TriggerBody::bodyExited(PhysicsNode *body)
This signal is emitted when the trigger body is no longer penetrated by the specified \a body.
\note This signal is not emitted for a body that is deleted while it is still inside the
trigger body, since there is no longer a body to report. \l collisionCount does count such a
body out, so a list kept by hand from these signals has to be cleared of deleted bodies
another way.
*/
QTriggerBody::QTriggerBody() = default;
void QTriggerBody::registerCollision(QAbstractPhysicsNode *collision, ShapePair shapes)
{
// PhysX reports one pair per pair of shapes, so a body with several shapes
// inside the trigger, or a trigger with several shapes around one body, is
// reported once per pair. Keeping the pairs apart means that one of them no
// longer overlapping does not report the whole body as having left, and that
// the same pair reported again is the same overlap.
const auto it = m_collisions.find(collision);
if (it != m_collisions.end()) {
// Another pair of a body already inside, or one of its pairs reported
// again after a rebuild, which is what says it still overlaps. A body
// stays followed for as long as it is inside, whatever it asks for in
// the meantime, or the report it is owed on the way out never comes.
it->pairs.insert(shapes);
it->unreported.remove(shapes);
return;
}
// What a body asks for is read once, as it enters, so that the reports on
// the way out are the ones that were made on the way in. One that asks for
// neither is followed all the same, or it would be asked again every time
// its shapes are rebuilt and could be reported as entering without having
// moved.
const bool sends = collision->sendTriggerReports();
const bool receives = collision->receiveTriggerReports();
Overlaps &overlaps = m_collisions[collision];
overlaps.pairs.insert(shapes);
overlaps.sends = sends;
overlaps.receives = receives;
if (sends)
++m_reportedCount;
// Nothing takes a body off this list when it is deleted while still inside,
// so the count would keep reporting a body that no longer exists.
connect(collision, &QObject::destroyed, this, &QTriggerBody::dropDestroyedBody,
Qt::UniqueConnection);
reportEntered(collision, sends, receives);
}
void QTriggerBody::deregisterCollision(QAbstractPhysicsNode *collision, ShapePair shapes)
{
const auto it = m_collisions.find(collision);
if (it == m_collisions.end())
return;
it->pairs.remove(shapes);
it->unreported.remove(shapes);
if (!it->pairs.isEmpty())
return;
const bool sends = it->sends;
const bool receives = it->receives;
if (sends)
--m_reportedCount;
m_collisions.erase(it);
disconnect(collision, &QObject::destroyed, this, &QTriggerBody::dropDestroyedBody);
reportExited(collision, sends, receives);
}
// The trigger holds every body that asked to hear about one of the two, since
// both are about a body being inside it, not about a pair of shapes overlapping
// Both take what to report by value and hold the body and this trigger weakly,
// since the first report can leave the rest with a body that is gone, a trigger
// that is gone, and an entry taken out from under a reference into the list.
// That is reachable from dropUnreportedOverlaps(), which runs from the frame and
// not from a callback holding the world's lock
void QTriggerBody::reportEntered(QAbstractPhysicsNode *body, bool sends, bool receives)
{
const QPointer<QTriggerBody> self(this);
const QPointer<QAbstractPhysicsNode> weakBody(body);
if (sends) {
emit bodyEntered(body);
// Off the list again is a handler having deleted the body it was just
// handed, which counted it out and reported that itself.
if (!self.isNull() && m_collisions.contains(body))
emit collisionCountChanged();
}
if (receives && !self.isNull() && !weakBody.isNull())
emit body->enteredTriggerBody(this);
}
void QTriggerBody::reportExited(QAbstractPhysicsNode *body, bool sends, bool receives)
{
const QPointer<QTriggerBody> self(this);
const QPointer<QAbstractPhysicsNode> weakBody(body);
if (sends) {
emit bodyExited(body);
if (!self.isNull())
emit collisionCountChanged();
}
if (receives && !self.isNull() && !weakBody.isNull())
emit body->exitedTriggerBody(this);
}
void QTriggerBody::invalidateOverlaps(QAbstractPhysicsNode *node)
{
// Every shape of node has just been replaced by a new one. PhysX does report
// the pairs of the shapes it took away, but as pairs of a shape that is
// already gone, which cannot be told apart and is ignored. So the pairs of
// those shapes are held on to until the next round of reports, which says
// which of them the new shapes are in: overlapping shapes are reported as
// found again, and what is not reported again is no longer overlapping.
//
// Holding on to them, rather than dropping them and letting the reports
// build the list up again, is what keeps a body inside a trigger from being
// reported as leaving and entering again every time its shapes are rebuilt,
// which is once per frame if any of them is animated.
if (node == this) {
for (Overlaps &overlaps : m_collisions)
overlaps.unreported = overlaps.pairs;
return;
}
const auto it = m_collisions.find(node);
if (it != m_collisions.end())
it->unreported = it->pairs;
}
void QTriggerBody::dropUnreportedOverlaps()
{
struct PendingExit
{
QPointer<QAbstractPhysicsNode> body;
bool sends = false;
bool receives = false;
};
QVarLengthArray<PendingExit, 4> pending;
for (auto it = m_collisions.begin(); it != m_collisions.end();) {
if (it->unreported.isEmpty()) {
++it;
continue;
}
it->pairs.subtract(it->unreported);
it->unreported.clear();
if (it->pairs.isEmpty()) {
QAbstractPhysicsNode *body = it.key();
disconnect(body, &QObject::destroyed, this, &QTriggerBody::dropDestroyedBody);
if (it->sends)
--m_reportedCount;
pending.append({ body, it->sends, it->receives });
it = m_collisions.erase(it);
} else {
++it;
}
}
// Reported once the list is settled, since a handler is free to register or
// deregister a collision on this trigger. A handler is also free to delete
// one of the bodies still waiting to be reported, or this trigger: one that
// no longer exists is not handed out, the same way a body destroyed while
// inside the trigger is not, and a trigger that no longer exists reports
// nothing further.
//
// Several bodies can go at once here, and all of them are out of the list
// before any of them is reported, so collisionCount is what it will be for
// the whole round rather than counting down through it.
const QPointer<QTriggerBody> self(this);
for (const PendingExit &exit : pending) {
if (self.isNull())
return;
if (!exit.body.isNull())
reportExited(exit.body.data(), exit.sends, exit.receives);
else if (exit.sends)
emit collisionCountChanged();
}
}
void QTriggerBody::dropDestroyedBody(QObject *body)
{
// Runs from ~QObject, so only the QObject part of the body is left: it is
// compared as a QObject and never passed on. No bodyExited() or
// exitedTriggerBody() either, since those would pass a half destroyed body
// to a handler, or read from one.
for (auto it = m_collisions.cbegin(); it != m_collisions.cend(); ++it) {
if (static_cast<QObject *>(it.key()) == body) {
const bool wasCounted = it->sends;
if (wasCounted)
--m_reportedCount;
m_collisions.erase(it);
if (wasCounted)
emit collisionCountChanged();
return;
}
}
}
int QTriggerBody::collisionCount() const
{
return m_reportedCount;
}
QAbstractPhysXNode *QTriggerBody::createPhysXBackend()
{
return new QPhysXTriggerBody(this);
}
QT_END_NAMESPACE