禁止可移动的QGraphicsItem与其他项目冲突

2022-05-26 00:00:00 qt collision c++ qgraphicsitem

最终编辑

我解决了这个问题,并在下面发布了一个解决方案作为答案。如果您无意中从Google找到了一种合适的方式来通过ItemIsMovable标志移动QGraphicsItems/QGraphicsObject,同时避免与其他节点发生冲突,我在答案的末尾提供了一个有效的itemChange方法。

我最初的项目涉及将节点捕捉到任意网格,但这很容易删除,并且根本不是此解决方案工作所必需的。就效率而言,这不是对数据结构或算法的最巧妙使用,因此,如果屏幕上有很多节点,您可能想尝试更智能的方法。


原始问题

我有一个子类QGraphicsItem Node,它为ItemIsMovableItemSendsGeometryChanges设置标志。这些节点对象重写了shape()boundingRect()函数,这些函数为它们提供了一个可四处移动的基本矩形。

用鼠标移动节点会像预期的那样调用itemChange()函数,我可以调用一个snapPoint()函数来强制移动的节点始终捕捉到网格。(这是一个简单的函数,它基于给定的参数返回新的QPointF,其中新点被限制在实际捕捉栅格上最接近的坐标)。这与现在一样工作正常。

我最大的问题是,我希望避免捕捉到与其他节点冲突的位置。也就是说,如果我将一个节点移动到一个结果(捕捉到网格)位置,我希望collidingItems()列表完全为空。

我可以在ItemPositionHasChanged标志被抛入itemChange()之后准确地检测到collidingItems,但不幸的是,这是在Node已经移动之后。更重要的是,我发现此冲突列表在节点从初始itemChange()返回之前无法正确更新(这很糟糕,因为节点已经被移动到与其他一些节点重叠的无效位置)。

以下是我到目前为止得到的itemChange()函数:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
    if ( change == ItemPositionChange && scene() )
    {
        // Snap the value to the grid
        QPointF pt = value.toPointF();
        QPointF snapped = snapPoint(pt);

        // How much we've moved by
        qreal delX = snapped.x() - pos().x();
        qreal delY = snapped.y() - pos().y();

        // We didn't move enough to justify a new snapped position
        // Visually, this results in no changes and no collision testing
        // is required, so we can quit early
        if ( delX == 0 && delY == 0 )
            return snapped;

        // If we're here, then we know the Node's position has actually
        // updated, and we know exactly how far it's moved based on delX
        // and delY (which is constrained to the snap grid dimensions)
        // for example: delX may be -16 and delY may be 0 if we've moved 
        // left on a grid of 16px size squares

        // Ideally, this is the location where I'd like to check for
        // collisions before returning the snapped point. If I can detect
        // if there are any nodes colliding with this one now, I can avoid
        // moving it at all and avoid the ItemPositionHasChanged checks
        // below

        return snapped;
    }
    else if ( change == ItemPositionHasChanged && scene())
    {
        // This gets fired after the Node actually gets moved (once return
        // snapped gets called above)

        // The collidingItems list is now validly set and can be compared
        // but unfortunately its too late, as we've already moved into a
        // potentially offending position that overlaps other Nodes

        if ( collidingItems().empty() )
        {
            // This is okay
        }
        else
        {
            // This is bad! This is what we wanted to avoid
        }
    }

    return QGraphicsItem::itemChange(change, value);
}

我尝试的内容

我尝试存储delXdelY更改,如果我们进入上面最后一个Else块,我尝试用moveBy(-lastDelX, -lastDelY)逆转上一步。这产生了一些可怕的结果,因为在鼠标仍然按住并移动节点本身的情况下调用此函数(这最终导致多次移动节点,同时尝试在冲突周围移动,通常会导致节点很快飞离屏幕一侧)。

我还尝试存储以前的(已知有效)点,并在发现冲突时对该位置调用SETX和Sty以恢复,但这也存在与上面的方法类似的问题。

结论

如果我能找到一种方法,在移动发生之前就准确地检测到collidingItems(),我就能够完全避免ItemPositionHasChanged块,确保ItemPositionChange只返回有效的点,从而避免移动到以前有效位置的任何逻辑。

感谢您抽出时间,如果您需要更多代码或示例来更好地说明问题,请告诉我。

编辑1:

我想我偶然发现了一种技术,它可能会成为实际解决方案的基础,但我仍然需要一些帮助,因为它并不完全有效。

在最后一个return snapped;之前的itemChange()函数中,我添加了以下代码:

// Make a copy of the shape and translate it correctly
// This should be equivalent to the shape formed after a successful
// move (i.e. after return snapped;)
QPainterPath path = shape();
path.translate(delX, delY);

// Examine all the other items to see if they collide with the desired
// shape....
for (QGraphicsItem* other : canvas->items() )
{
    if ( other == this ) // don't care about this node, obviously
       continue;

    if ( other->collidesWithPath(path) )
    {
       // We should hopefully only reach this if another
       // item collides with the desired path. If that happens,
       // we don't want to move this node at all. Unfortunately,
       // we always seem to enter this block even when the nodes
       // shouldn't actually collide.
       qDebug() << "Found collision?";
       return pos();
    }
}

// Should only reach this if no collisions found, so we're okay to move
return snapped;

...但这也不起作用。任何新节点总是进入collidesWithPath块,即使它们彼此相距很远。上面代码中的画布对象只是保存所有这些Node对象的QGraphicsView,如果这一点也不清楚的话。

有什么办法让collidesWithPath正常工作吗?复制shape()的路径是否错误?我不确定这里出了什么问题,因为在移动完成后,两个形状的比较似乎工作得很好(即,我可以在之后准确地检测到碰撞)。如果有帮助,我可以上传整个代码。


解决方案

我想出来了。通过修改shape()调用,我确实走上了正确的道路。我并没有实际测试完整的Shape对象本身,而是编写了一个基于this thread的额外函数来比较两个QRectF(这实际上就是Shape调用返回的内容):

bool rectsCollide(QRectF a, QRectF b)
{
    qreal ax1, ax2, ay1, ay2;
    a.getCoords(&ax1, &ay1, &ax2, &ay2);

    qreal bx1, bx2, by1, by2;
    b.getCoords(&bx1, &by1, &bx2, &by2);

    return ( ax1 < bx2 &&
             ax2 > bx1 &&
             ay1 < by2 &&
             ay2 > by1 );
}

然后,在与原始帖子编辑相同的区域中:

QRectF myTranslatedRect = getShapeRect();
myTranslatedRect.translate(delX, delY);

for (QGraphicsItem* other : canvas->items() )
{
    if ( other == this )
        continue;

    Node* n = (Node*)other;
    if ( rectsCollide( myTranslatedRect, n->getShapeRect() ) )
        return pos();
}

return snapped;

这与一个小帮助器函数getShapeRect()一起使用,该函数基本上只返回被覆盖的shape()函数中使用的相同矩形,但形式是QRectF,而不是QPainterPath

这个解决方案似乎工作得很好。节点可以自由移动(同时仍受捕捉栅格的约束),除非它可能与另一个节点重叠。在这种情况下,不会显示任何移动。

编辑1:

我在这方面花了一些时间,以获得稍微更好的结果,我想我应该分享一下,因为我觉得使用碰撞来移动节点可能是很常见的事情。在上面的代码中,在一个长节点/一组节点上"拖动"一个节点会与当前拖动的节点发生冲突,这会产生一些不想要的效果;即,由于我们只是默认在任何冲突时返回pos(),所以我们根本不会移动--即使可以成功地在X或Y方向上移动。这发生在对撞机上的对角拖动上(这有点难解释,但如果您正在使用上面的代码构建类似的项目,当您尝试将一个节点拖到另一个节点旁边时,您可以看到它的作用)。我们可以通过几个微小的更改轻松地检测到这种情况:

我们可以有两个单独的矩形;一个在X方向上平移,另一个在Y方向上平移。for循环现在检查以确保任何X移动独立于Y移动是有效的。在for循环之后,我们只需对照这些案例来确定最终的移动。

下面是您可以在代码中自由使用的最后itemChange()方法:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
    if ( change == ItemPositionChange && scene() )
    {
        // Figure out the new point
        QPointF snapped = snapPoint(value.toPointF());

        // deltas to store amount of change
        qreal delX = snapped.x() - pos().x();
        qreal delY = snapped.y() - pos().y();

        // No changes
        if ( delX == 0 && delY == 0 )
            return snapped;

        // Check against all the other items for collision
        QRectF translateX = getShapeRect();
        QRectF translateY = getShapeRect();
        translateX.translate(delX, 0);
        translateY.translate(0, delY);

        bool xOkay = true;
        bool yOkay = true;

        for ( QGraphicsItem* other : canvas->items())
        {
            if (other == this)
                continue;

            if ( rectsCollide(translateX, ((Node*)other)->getShapeRect()) )
                xOkay = false;
            if ( rectsCollide(translateY, ((Node*)other)->getShapeRect()) )
                yOkay = false;
        }

        if ( xOkay && yOkay ) // Both valid, so move to the snapped location
            return snapped;
        else if ( xOkay ) // Move only in the X direction
        {
            QPointF r = pos();
            r.setX(snapped.x());
            return r;
        }
        else if ( yOkay ) // Move only in the Y direction
        {
            QPointF r = pos();
            r.setY(snapped.y());
            return r;
        }
        else // Don't move at all
            return pos();
    }

    return QGraphicsItem::itemChange(change, value);
}

对于上面的代码,如果您希望自由移动不受网格的限制,那么避免任何类型的捕捉行为实际上是非常简单的。解决这一问题的方法是更改调用snapPoint()函数的行,并使用原始值。如果您有任何问题,请随时发表意见。

相关文章