QGraphicsScene パフォーマンス改善:大量アイテムの衝突判定を高速化

2025-04-26

基本的な機能

  • 正確な衝突判定
    アイテムの形状(例えば、矩形、楕円、複雑なパスなど)に基づいて正確な衝突判定を行います。
  • 指定されたアイテム
    どのアイテムとの衝突を調べたいかを引数として渡します。
  • 衝突アイテムの取得
    衝突しているアイテムが存在する場合、それらのアイテムの QList<QGraphicsItem*> を返します。
  • 衝突判定
    指定されたアイテムと、シーン内の他のすべてのアイテムとの間で衝突があるかどうかを判定します。

関数のシグネチャ

QList<QGraphicsItem *> QGraphicsScene::collidingItems(const QGraphicsItem *item, Qt::ItemSelectionMode mode = Qt::IntersectsItemShape) const
  • mode: 衝突判定のモードを指定するオプションの引数です。デフォルトは Qt::IntersectsItemShape です。主なモードは以下の通りです。
    • Qt::IntersectsItemShape: アイテムの形状が重なり合っている場合に衝突と判定します(デフォルト)。
    • Qt::ContainsItemShape: 指定されたアイテムの形状が、他のアイテムの形状を完全に含んでいる場合に衝突と判定します。
    • Qt::IntersectsItemBoundingRect: アイテムのバウンディング矩形が重なり合っている場合に衝突と判定します。これは高速ですが、形状が複雑な場合は正確でない可能性があります。
    • Qt::ContainsItemBoundingRect: 指定されたアイテムのバウンディング矩形が、他のアイテムのバウンディング矩形を完全に含んでいる場合に衝突と判定します。
  • item: 衝突をチェックしたい QGraphicsItem のポインタ。

使用例

#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QDebug>

int main(int argc, char *argv[])
{
    QGraphicsScene scene;

    // 2つの矩形アイテムを作成
    QGraphicsRectItem *rect1 = scene.addRect(0, 0, 50, 50);
    QGraphicsRectItem *rect2 = scene.addRect(25, 25, 50, 50);
    QGraphicsRectItem *rect3 = scene.addRect(100, 100, 50, 50);

    // rect1 と衝突しているアイテムを取得
    QList<QGraphicsItem *> colliding = scene.collidingItems(rect1);

    qDebug() << "rect1と衝突しているアイテム:";
    for (QGraphicsItem *item : colliding) {
        qDebug() << item;
    }

    return 0;
}

この例では、rect1rect2 は一部が重なり合っているため、collidingItems(rect1)rect2 を含むリストを返します。rect3rect1 と重なっていないため、リストには含まれません。

  • 衝突を処理するためには、通常、collidingItems() の結果を受け取り、衝突しているアイテムの種類や状態に応じて適切な処理を行います。例えば、アイテム同士が接触したら色を変える、特定のイベントを発生させるなどです。
  • 衝突判定は、シーン内のすべての他のアイテムに対して行われます。シーンに多数のアイテムが存在する場合、パフォーマンスに影響を与える可能性があることに注意してください。
  • collidingItems() は、指定されたアイテムの現在の位置と形状に基づいて衝突を判定します。アイテムの位置や形状が変更された場合は、再度この関数を呼び出す必要があります。


期待したアイテムが衝突リストに含まれない

  • トラブルシューティング

    • アイテムのバウンディング矩形を確認
      各アイテムの boundingRect() を取得して、実際に重なり合っているかを確認します。
    • アイテムの形状を確認
      カスタムアイテムの場合は、shape().boundingRect() を確認します。必要であれば、paint() 関数内で描画されている形状と shape() の実装が一致しているか確認します。
    • 衝突判定モードを変更して試す
      mode 引数を変えて、異なる判定基準で結果が変わるか試してみます。
    • デバッグ出力
      衝突判定に関わるアイテムの位置や形状をデバッグ出力して、意図した状態になっているか確認します。
    • アイテムの位置が重なっていない
      明らかですが、視覚的には近くに見えても、実際のアイテムの形状やバウンディング矩形が重なっていない可能性があります。
    • Z値の考慮
      QGraphicsItem には Z値(スタック順序)がありますが、collidingItems() は Z値を考慮しません。視覚的に前面にあるアイテムが、実際には背面にあるアイテムと衝突していると判定されることがあります。
    • アイテムの形状が正しく設定されていない
      カスタムアイテムの場合、shape() 関数が正しく実装されていないと、期待通りの衝突判定が行われません。
    • 衝突判定モード (mode) の誤り
      デフォルトの Qt::IntersectsItemShape で意図した衝突が検出されない場合は、他のモード(Qt::ContainsItemShape, Qt::IntersectsItemBoundingRect など)を試してみる必要があるかもしれません。
    • アイテムがシーンに追加されていない
      衝突をチェックしたいアイテムと、衝突の可能性がある他のアイテムの両方が、QGraphicsScene に追加されていることを確認してください。

意図しないアイテムが衝突リストに含まれる

  • トラブルシューティング

    • 形状の微調整
      アイテムの形状をわずかに調整して、不要なオーバーラップを解消します。
    • より厳密な衝突判定モードを使用
      Qt::IntersectsItemShape を使用して、形状レベルでの衝突判定を行います。
    • 衝突リストの内容を精査
      返ってきた衝突リストに含まれるアイテムを一つずつ確認し、なぜそれが含まれているのかを理解します。
  • 原因

    • 形状のオーバーラップ
      アイテムの形状が、意図せず他のアイテムとわずかに重なっている可能性があります。特に複雑な形状のアイテムで起こりやすいです。
    • バウンディング矩形のオーバーラップ (モードが Qt::IntersectsItemBoundingRect の場合)
      バウンディング矩形は重なっているが、実際の形状は重なっていない場合に発生します。より正確な衝突判定が必要な場合は、Qt::IntersectsItemShape を使用します。
    • 誤ったアイテムへの衝突チェック
      意図しないアイテムに対して collidingItems() を呼び出している可能性があります。

パフォーマンスの問題

  • トラブルシューティング

    • 衝突チェックの頻度を減らす
      必要な時だけ衝突チェックを行うようにします。例えば、アイテムが一定距離以上移動した場合や、特定のイベントが発生した場合などです。
    • 空間分割構造の利用 (高度なテクニック)
      大規模なシーンでは、空間分割(例:四分木、グリッド)などのデータ構造を自力で実装または利用することで、衝突判定の対象となるアイテムを絞り込み、パフォーマンスを向上させることができます。Qt自体には直接的なサポートはありませんが、そのような構造を自分で管理することができます。
    • 近似的な衝突判定の利用
      厳密な衝突判定が不要な場合は、バウンディング矩形による判定 (Qt::IntersectsItemBoundingRect) を利用することで高速化を図ることができます。ただし、精度は低下します。
    • 衝突判定の最適化
      カスタムアイテムの shape() 関数が効率的に実装されているか確認します。
  • 原因

    • シーン内のアイテム数が多すぎる
      collidingItems() はシーン内のすべてのアイテムとの衝突をチェックするため、アイテム数が非常に多いと処理に時間がかかることがあります。
    • 頻繁な呼び出し
      アニメーションや高速な動きの中で collidingItems() を頻繁に呼び出すと、アプリケーションの応答性が低下する可能性があります。
    • 複雑な形状のアイテムが多い
      複雑な形状のアイテム同士の衝突判定は、単純な矩形などに比べて計算コストが高くなります。

カスタムアイテムにおける問題

  • トラブルシューティング

    • shape() 関数の実装を再確認
      shape() 関数がアイテムの形状を正確に表しているか確認します。paint() 関数での描画と対応しているかを確認します。
    • 変換行列の適用
      必要であれば、アイテムの変換行列 (transform()) を shape() で作成する QPainterPath に適用します。
  • 原因

    • shape() 関数の実装ミス
      カスタムアイテムで shape() 関数をオーバーライドしている場合、返される QPainterPath がアイテムの実際の描画と一致していないと、期待通りの衝突判定が行われません。
    • 変換 (Transformation) の考慮漏れ
      アイテムに回転やスケーリングなどの変換が適用されている場合、shape() 関数は変換後の形状を反映した QPainterPath を返す必要があります。

デバッグのヒント

  • ログ出力
    衝突リストの内容や、衝突判定に関わる変数の値をログ出力して、処理の流れを追跡します。
  • デバッガの利用
    collidingItems() の呼び出し前後のアイテムの状態(位置、形状など)をデバッガで確認します。
  • アイテムの境界線を描画
    デバッグ目的で、すべてのアイテムのバウンディング矩形や形状を画面に描画すると、視覚的に衝突状況を確認しやすくなります。


例1: 2つの矩形アイテムの衝突検出

これは最も基本的な例で、2つの矩形アイテムが重なっているかどうかを検出し、その結果をコンソールに出力します。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QDebug>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view.show();

    // 1つ目の矩形アイテムを作成し、シーンに追加
    QGraphicsRectItem *rect1 = scene.addRect(0, 0, 50, 50);
    rect1->setPos(10, 10);

    // 2つ目の矩形アイテムを作成し、シーンに追加
    QGraphicsRectItem *rect2 = scene.addRect(0, 0, 50, 50);
    rect2->setPos(30, 30);

    // rect1 と衝突しているアイテムを取得
    QList<QGraphicsItem *> collidingItems = scene.collidingItems(rect1);

    qDebug() << "rect1 と衝突しているアイテムの数:" << collidingItems.count();

    if (!collidingItems.isEmpty()) {
        qDebug() << "rect1 と衝突しているアイテム:";
        for (QGraphicsItem *item : collidingItems) {
            if (item == rect2) {
                qDebug() << "- rect2";
            } else {
                qDebug() << "- その他アイテム:" << item;
            }
        }
    } else {
        qDebug() << "rect1 と衝突しているアイテムはありません。";
    }

    return a.exec();
}

説明

  1. QGraphicsSceneQGraphicsView を作成し、表示します。
  2. 2つの QGraphicsRectItem (rect1rect2) を作成し、それぞれ異なる位置に配置してシーンに追加します。
  3. scene.collidingItems(rect1) を呼び出すことで、rect1 と衝突しているすべてのアイテムのリストを取得します。
  4. 取得したリストの数と、衝突しているアイテム(ここでは rect2 であることを期待)の名前をコンソールに出力します。

例2: マウスで移動するアイテムと他のアイテムの衝突検出と反応

この例では、マウスで移動できる矩形アイテムを作成し、それが別の固定された矩形アイテムと衝突した際に、移動するアイテムの色を変更します。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsItem>
#include <QBrush>

class MovableRect : public QGraphicsRectItem
{
public:
    MovableRect(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent = nullptr)
        : QGraphicsRectItem(x, y, width, height, parent)
    {
        setFlag(ItemIsMovable);
        setBrush(Qt::blue);
    }

protected:
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override
    {
        QGraphicsRectItem::mouseMoveEvent(event);
        QList<QGraphicsItem *> collidingItems = scene()->collidingItems(this);
        setBrush(Qt::blue); // デフォルトの色に戻す
        for (QGraphicsItem *item : collidingItems) {
            if (item != this) {
                setBrush(Qt::red); // 衝突したら赤色にする
                break;
            }
        }
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->setSceneRect(-100, -100, 200, 200); // シーンの範囲を設定
    view->show();

    // 移動可能な矩形アイテムを作成
    MovableRect *movableRect = new MovableRect(0, 0, 50, 50);
    scene.addItem(movableRect);
    movableRect->setPos(-20, -20);

    // 固定された矩形アイテムを作成
    QGraphicsRectItem *fixedRect = scene.addRect(30, 30, 50, 50);
    fixedRect->setBrush(Qt::green);

    return a.exec();
}

説明

  1. MovableRect クラスは QGraphicsRectItem を継承し、ItemIsMovable フラグを設定することでマウスでの移動を可能にします。
  2. mouseMoveEvent をオーバーライドし、マウスが移動するたびに scene()->collidingItems(this) を呼び出して、自身と衝突しているアイテムのリストを取得します。
  3. 衝突しているアイテムの中に自身以外のアイテム(ここでは fixedRect)があれば、自身のブラシの色を赤に変更します。移動が止まったり、衝突がなくなったりすると、デフォルトの青色に戻ります。

例3: 複数のアイテムとの衝突検出とフィルタリング

この例では、複数の円形のアイテムが存在するシーンで、特定の円形のアイテムが他の円形のアイテムと衝突しているかどうかを検出します。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QDebug>
#include <QRandomGenerator>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->setSceneRect(-100, -100, 200, 200);
    view->show();

    QList<QGraphicsEllipseItem *> circles;
    int numCircles = 10;
    QRandomGenerator *generator = QRandomGenerator::global();

    // 複数の円形アイテムを作成し、ランダムな位置に配置
    for (int i = 0; i < numCircles; ++i) {
        QGraphicsEllipseItem *circle = scene.addEllipse(-10, -10, 20, 20);
        circle->setPos(generator->bounded(-90.0, 90.0), generator->bounded(-90.0, 90.0));
        circles.append(circle);
    }

    // 最初の円形アイテムとの衝突を検出
    QGraphicsEllipseItem *firstCircle = circles.first();
    QList<QGraphicsItem *> collidingItems = scene.collidingItems(firstCircle);

    qDebug() << "最初の円と衝突しているアイテム:";
    for (QGraphicsItem *item : collidingItems) {
        if (item != firstCircle && qgraphicsitem_cast<QGraphicsEllipseItem *>(item)) {
            qDebug() << "- 円:" << item;
        }
    }

    return a.exec();
}
  1. 複数の QGraphicsEllipseItem を作成し、シーン内のランダムな位置に配置します。
  2. 最初の円形のアイテム (firstCircle) を選択し、scene.collidingItems(firstCircle) を呼び出して衝突しているアイテムのリストを取得します。
  3. 取得したリストをループ処理し、衝突しているアイテムが firstCircle 自身ではなく、かつ QGraphicsEllipseItem 型である場合に、そのアイテムをコンソールに出力します。qgraphicsitem_cast は、アイテムが特定の型であるかどうかを安全にチェックするために使用されます。


QGraphicsItem::collidesWithItem() の利用

特定の2つのアイテム間の衝突のみをチェックしたい場合に有効です。シーン内のすべてのアイテムとの衝突を調べる collidingItems() よりも効率的な場合があります。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QDebug>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->show();

    QGraphicsRectItem *rect1 = scene.addRect(0, 0, 50, 50);
    rect1->setPos(10, 10);

    QGraphicsRectItem *rect2 = scene.addRect(0, 0, 50, 50);
    rect2->setPos(30, 30);

    if (rect1->collidesWithItem(rect2)) {
        qDebug() << "rect1 と rect2 は衝突しています。";
    } else {
        qDebug() << "rect1 と rect2 は衝突していません。";
    }

    return a.exec();
}

利点

  • 特定の2つのアイテム間の衝突判定に限定されるため、シーン内のアイテム数が多い場合に collidingItems() より高速である可能性があります。

欠点

  • 複数のアイテムとの衝突を調べる場合は、複数の collidesWithItem() の呼び出しが必要になります。

QGraphicsItem::collidesWithPath() の利用

アイテムの形状と特定のパスとの衝突を判定したい場合に利用できます。複雑な形状との衝突判定や、特定の領域との接触を調べたい場合に便利です。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QPainterPath>
#include <QDebug>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->show();

    QGraphicsRectItem *rect = scene.addRect(10, 10, 50, 50);

    QPainterPath path;
    path.addEllipse(30, 30, 40, 40); // 楕円のパス

    if (rect->collidesWithPath(path, Qt::IntersectsItemShape)) {
        qDebug() << "矩形は楕円のパスと衝突しています。";
    } else {
        qDebug() << "矩形は楕円のパスと衝突していません。";
    }

    return a.exec();
}

利点

  • アイテムの形状だけでなく、任意のパスとの衝突をチェックできます。
  • 複雑な形状や特定の領域との衝突判定が可能です。

欠点

  • アイテムの形状との衝突判定と同様の計算コストがかかります。
  • パスの作成と管理が必要です。

手動での衝突判定

アイテムの位置、サイズ、形状などの情報に基づいて、数学的な計算や幾何学的なアルゴリズムを用いて衝突判定を独自に実装する方法です。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QRectF>
#include <QDebug>

bool areRectsColliding(const QRectF &r1, const QRectF &r2)
{
    return r1.intersects(r2);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->show();

    QGraphicsRectItem *rect1 = scene.addRect(0, 0, 50, 50);
    rect1->setPos(10, 10);

    QGraphicsRectItem *rect2 = scene.addRect(0, 0, 50, 50);
    rect2->setPos(30, 30);

    QRectF rect1_bounds = rect1->sceneBoundingRect();
    QRectF rect2_bounds = rect2->sceneBoundingRect();

    if (areRectsColliding(rect1_bounds, rect2_bounds)) {
        qDebug() << "rect1 と rect2 (バウンディング矩形) は衝突しています。";
    } else {
        qDebug() << "rect1 と rect2 (バウンディング矩形) は衝突していません。";
    }

    return a.exec();
}

利点

  • 複雑な物理シミュレーションなど、特殊な要件に対応できます。
  • 特定の形状や衝突条件に合わせて、高度な最適化やカスタマイズが可能です。

欠点

  • アイテムの移動や変形に合わせて、手動で衝突判定を更新する必要があります。
  • 正確な衝突判定を実装するには、幾何学的な知識が必要です。
  • 衝突判定ロジックを自分で実装する必要があるため、開発コストが高くなる可能性があります。

空間分割構造の利用 (高度なテクニック)

シーン内のアイテム数が非常に多い場合に、衝突判定のパフォーマンスを向上させるために利用されます。シーンを小さな領域に分割し、衝突の可能性のあるアイテムのペアのみをチェックします。


  • 四分木 (Quadtree)、グリッドベースの手法など

利点

  • パフォーマンスが重要なアプリケーションに適しています。
  • 大規模なシーンでも効率的な衝突判定が可能です。

欠点

  • Qt自体には直接的なサポートはないため、自分で実装または外部ライブラリを利用する必要があります。
  • 空間分割構造の維持にオーバーヘッドが発生します。
  • 実装が複雑になる場合があります。

シグナルとスロットの利用 (間接的な衝突処理)

直接的な衝突判定を行うのではなく、アイテムの状態変化(位置の変更など)に応じてシグナルを発行し、それを受け取ったスロットで間接的に衝突を処理する方法です。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QObject>
#include <QDebug>

class MovableRect : public QGraphicsRectItem
{
    Q_OBJECT
public:
    MovableRect(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent = nullptr)
        : QGraphicsRectItem(x, y, width, height, parent)
    {
        setFlag(ItemIsMovable);
    }

signals:
    void moved(QPointF newPos);

protected:
    void itemChange(GraphicsItemChange change, const QVariant &value) override
    {
        if (change == ItemPositionChange && scene()) {
            emit moved(value.toPointF());
        }
        return QGraphicsRectItem::itemChange(change, value);
    }
};

class CollisionHandler : public QObject
{
    Q_OBJECT
public:
    CollisionHandler(MovableRect *movable, QGraphicsRectItem *fixed)
        : movableRect(movable), fixedRect(fixed)
    {
        connect(movableRect, &MovableRect::moved, this, &CollisionHandler::checkCollision);
    }

private slots:
    void checkCollision(QPointF)
    {
        if (movableRect->collidesWithItem(fixedRect)) {
            qDebug() << "移動する矩形が固定された矩形と衝突しました!";
        }
    }

private:
    MovableRect *movableRect;
    QGraphicsRectItem *fixedRect;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view->show();

    MovableRect *movableRect = new MovableRect(0, 0, 50, 50);
    movableRect->setPos(-20, -20);
    scene.addItem(movableRect);

    QGraphicsRectItem *fixedRect = scene.addRect(30, 30, 50, 50);

    CollisionHandler handler(movableRect, fixedRect);

    return a.exec();
}

#include "main.moc"

利点

  • 衝突処理を特定のアイテムクラスに含める必要がなくなります。
  • アイテム間の依存関係を疎結合に保つことができます。

欠点

  • シグナルとスロットの接続と管理が必要です。
  • 衝突判定自体は依然として collidesWithItem() などで行う必要があります。
  • 基本的な衝突検出
    QGraphicsScene::collidingItems() は手軽に利用できます。
  • 疎結合な設計
    シグナルとスロットを利用した間接的な衝突処理が有効です。
  • 大規模なシーンでのパフォーマンス
    空間分割構造の利用を検討します。
  • 高度なカスタマイズや最適化
    手動での衝突判定が必要になる場合があります。
  • 特定の形状との衝突
    QGraphicsItem::collidesWithPath() が適しています。
  • 少数のアイテム間の衝突
    QGraphicsItem::collidesWithItem() がシンプルで効率的です。