이 페이지에서

마우스 충돌 예제

그래픽 뷰에서 항목에 애니메이션을 적용하는 방법을 보여줍니다.

'마우스 충돌' 예제는 그래픽 뷰 프레임워크를 사용하여 애니메이션 항목을 구현하고 항목 간의 충돌을 감지하는 방법을 보여줍니다.

쥐와 치즈 그래픽이 등장하는 애플리케이션이 실행 중입니다.

Graphics View는 ` QGraphicsItem ` 클래스에서 파생된 다수의 사용자 정의 2D 그래픽 항목을 관리하고 상호 작용하기 위한 ` QGraphicsScene ` 클래스와, 확대/축소 및 회전 기능을 지원하며 항목을 시각화하는 ` QGraphicsView ` 위젯을 제공합니다.

이 예제는 항목 클래스와 메인 함수로 구성되어 있습니다. ` Mouse ` 클래스는 ` QGraphicsItem`을 상속받은 개별 마우스를 나타내며, ` main() ` 함수는 메인 애플리케이션 창을 제공합니다.

먼저 Mouse 클래스를 살펴보며 아이템을 애니메이션 처리하고 아이템 간 충돌을 감지하는 방법을 확인한 다음, main() 함수를 살펴보며 아이템을 씬에 배치하고 해당 뷰를 구현하는 방법을 알아보겠습니다.

Mouse 클래스 정의

mouse 클래스는 QGraphicsItem 클래스에서 상속받습니다. QGraphicsItem 클래스는 Graphics View 프레임워크의 모든 그래픽 항목에 대한 기본 클래스이며, 사용자 정의 항목을 작성하기 위한 경량 기반을 제공합니다.

class Mouse : public QGraphicsItem
{
public:
    Mouse();

    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget) override;

protected:
    void advance(int step) override;

private:
    qreal angle = 0;
    qreal speed = 0;
    qreal mouseEyeDirection = 0;
    QColor color;
};

사용자 정의 그래픽 항목을 작성할 때는 QGraphicsItem 의 두 가지 순수 가상 공용 함수, 즉 항목이 채색하는 면적의 추정값을 반환하는 boundingRect()와 실제 채색을 구현하는 paint()를 구현해야 합니다. 또한 shape()와 advance()도 재구현합니다. shape()를 재구현하여 마우스 항목의 정확한 모양을 반환하도록 했습니다. 기본 구현은 단순히 항목의 경계 사각형을 반환할 뿐입니다. 또한 advance()를 재구현하여 애니메이션 처리를 한 번의 업데이트 내에서 모두 수행되도록 했습니다.

Mouse 클래스 정의

마우스 항목을 생성할 때, 먼저 클래스 내에서 직접 초기화되지 않은 모든 항목의 비공개 변수가 올바르게 초기화되도록 합니다:

Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256),
                       QRandomGenerator::global()->bounded(256),
                       QRandomGenerator::global()->bounded(256))
{
    setRotation(QRandomGenerator::global()->bounded(360 * 16));
}

마우스 색상의 다양한 구성 요소를 계산하기 위해 ` QRandomGenerator`를 사용합니다.

그런 다음 ` QGraphicsItem`에서 상속받은 ` setRotation()` 함수를 호출합니다. 아이템은 자체적인 국소 좌표계에 존재합니다. 아이템의 좌표는 대개 (0, 0)을 중심으로 하며, 이곳은 모든 변환의 중심점이기도 합니다. 아이템의 ` setRotation()` 함수를 호출하여 마우스가 이동을 시작할 방향을 변경합니다.

QGraphicsScene 가 장면을 한 프레임씩 진행시키기로 결정하면, 각 아이템에 대해 QGraphicsItem::advance()를 호출합니다. 이를 통해 advance() 함수를 재구현하여 마우스를 애니메이션화할 수 있습니다.

void Mouse::advance(int step)
{
    if (!step)
        return;
    QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0));
    if (lineToCenter.length() > 150) {
        qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx());
        angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2);

        if (angleToCenter < Pi && angleToCenter > Pi / 4) {
            // Rotate left
            angle += (angle < -Pi / 2) ? 0.25 : -0.25;
        } else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) {
            // Rotate right
            angle += (angle < Pi / 2) ? 0.25 : -0.25;
        }
    } else if (::sin(angle) < 0) {
        angle += 0.25;
    } else if (::sin(angle) > 0) {
        angle -= 0.25;
    }

먼저, step이 0 인 경우에는 진행 처리를 수행하지 않습니다. 이는 advance() 함수가 두 번 호출되기 때문입니다. 첫 번째는 step == 0 일 때로, 아이템이 곧 진행될 것임을 나타내며, 두 번째는 step == 1 일 때로 실제 진행을 처리합니다. 또한 마우스가 반지름 150 픽셀의 원 안에 머물도록 보장합니다.

QGraphicsItem 에서 제공하는 mapFromScene() 함수에 주목하십시오. 이 함수는 씬 좌표계로 주어진 위치를 아이템의 좌표계로 매핑합니다.

    const QList<QGraphicsItem *> dangerMice = scene()->items(QPolygonF()
                           << mapToScene(0, 0)
                           << mapToScene(-30, -50)
                           << mapToScene(30, -50));

    for (const QGraphicsItem *item : dangerMice) {
        if (item == this)
            continue;

        QLineF lineToMouse(QPointF(0, 0), mapFromItem(item, 0, 0));
        qreal angleToMouse = std::atan2(lineToMouse.dy(), lineToMouse.dx());
        angleToMouse = normalizeAngle((Pi - angleToMouse) + Pi / 2);

        if (angleToMouse >= 0 && angleToMouse < Pi / 2) {
            // Rotate right
            angle += 0.5;
        } else if (angleToMouse <= TwoPi && angleToMouse > (TwoPi - Pi / 2)) {
            // Rotate left
            angle -= 0.5;
        }
    }

    if (dangerMice.size() > 1 && QRandomGenerator::global()->bounded(10) == 0) {
        if (QRandomGenerator::global()->bounded(1))
            angle += QRandomGenerator::global()->bounded(1 / 500.0);
        else
            angle -= QRandomGenerator::global()->bounded(1 / 500.0);
    }

그런 다음 다른 마우스와 충돌하지 않도록 합니다.

    speed += (-50 + QRandomGenerator::global()->bounded(100)) / 100.0;

    qreal dx = ::sin(angle) * 10;
    mouseEyeDirection = (qAbs(dx / 5) < 1) ? 0 : dx / 5;

    setRotation(rotation() + dx);
    setPos(mapToParent(0, -(3 + sin(speed) * 3)));
}

마지막으로, 마우스의 속도와 시선 방향(마우스를 그릴 때 사용)을 계산하고, 새로운 위치를 설정합니다.

아이템의 위치는 부모 좌표계에서 해당 아이템의 원점(로컬 좌표 (0, 0))을 나타냅니다. QGraphicsItem::setPos() 함수는 아이템의 위치를 부모 좌표계에서 주어진 위치로 설정합니다. 부모가 없는 항목의 경우, 지정된 위치는 씬 좌표로 해석됩니다. ` QGraphicsItem `는 또한 항목 좌표로 주어진 위치를 부모의 좌표계로 매핑하는 ` mapToParent()` 함수를 제공합니다. 항목에 부모가 없는 경우, 위치는 대신 씬의 좌표계로 매핑됩니다.

이제 ` QGraphicsItem`에서 상속받은 순수 가상 함수에 대한 구현을 제공할 차례입니다. 먼저 ` boundingRect()` 함수를 살펴보겠습니다:

QRectF Mouse::boundingRect() const
{
    qreal adjust = 0.5;
    return QRectF(-18 - adjust, -22 - adjust,
                  36 + adjust, 60 + adjust);
}

boundingRect() 함수는 아이템의 외부 경계를 직사각형으로 정의합니다. Graphics View 프레임워크는 이 경계 직사각형을 사용하여 아이템의 재그리기 필요 여부를 판단하므로, 모든 그리기 작업은 이 직사각형 내부에서 이루어져야 합니다.

void Mouse::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    // Body
    painter->setBrush(color);
    painter->drawEllipse(-10, -20, 20, 40);

    // Eyes
    painter->setBrush(Qt::white);
    painter->drawEllipse(-10, -17, 8, 8);
    painter->drawEllipse(2, -17, 8, 8);

    // Nose
    painter->setBrush(Qt::black);
    painter->drawEllipse(QRectF(-2, -22, 4, 4));

    // Pupils
    painter->drawEllipse(QRectF(-8.0 + mouseEyeDirection, -17, 4, 4));
    painter->drawEllipse(QRectF(4.0 + mouseEyeDirection, -17, 4, 4));

    // Ears
    painter->setBrush(scene()->collidingItems(this).isEmpty() ? Qt::darkYellow : Qt::red);
    painter->drawEllipse(-17, -12, 16, 16);
    painter->drawEllipse(1, -12, 16, 16);

    // Tail
    QPainterPath path(QPointF(0, 20));
    path.cubicTo(-5, 22, -5, 22, 0, 25);
    path.cubicTo(5, 27, 5, 32, 0, 30);
    path.cubicTo(-5, 32, -5, 42, 0, 35);
    painter->setBrush(Qt::NoBrush);
    painter->drawPath(path);
}

Graphics View 프레임워크는 paint() 함수를 호출하여 항목의 내용을 그리며, 이 함수는 로컬 좌표계에서 항목을 그립니다.

귀의 채색에 주목하십시오. 마우스 아이템이 다른 마우스 아이템과 충돌할 때마다 귀는 빨간색으로 채워지고, 그렇지 않은 경우에는 짙은 노란색으로 채워집니다. 우리는 ` QGraphicsScene::collidingItems()` 함수를 사용하여 충돌하는 마우스 항목이 있는지 확인합니다. 실제 충돌 감지는 Graphics View 프레임워크가 도형 간 교차 연산을 통해 처리합니다. 우리가 해야 할 일은 ` QGraphicsItem::shape()` 함수가 해당 항목에 대한 정확한 도형을 반환하도록 보장하는 것뿐입니다:

QPainterPath Mouse::shape() const
{
    QPainterPath path;
    path.addRect(-10, -20, 20, 40);
    return path;
}

도형이 복잡해지면 임의의 도형 간 교차 연산의 복잡도가 한 차수씩 증가하기 때문에, 이 작업은 상당히 많은 시간이 소요될 수 있습니다. 대안으로, ` collidesWithItem()` 함수를 재구현하여 사용자 정의 아이템 및 도형 충돌 알고리즘을 제공할 수 있습니다.

이로써 Mouse 클래스 구현이 완료되었으며, 이제 사용할 준비가 되었습니다. 이제 main() 함수를 살펴보며, 마우스용 씬과 씬의 내용을 표시하는 뷰를 구현하는 방법을 알아보겠습니다.

Main() 함수

main() 함수는 메인 애플리케이션 창을 제공하며, 아이템과 해당 장면, 그리고 이에 대응하는 뷰를 생성합니다.

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

먼저, 애플리케이션 객체를 생성하고 장면을 만듭니다:

    QGraphicsScene scene;
    scene.setSceneRect(-300, -300, 600, 600);

QGraphicsScene 클래스는 QGraphicsItem의 컨테이너 역할을 합니다. 또한 항목의 위치를 효율적으로 파악하고, 장면 내 임의의 영역에서 어떤 항목이 보이는지 판단할 수 있는 기능을 제공합니다.

씬을 생성할 때는 씬의 범위를 정의하는 사각형인 씬의 직사각형을 설정하는 것이 좋습니다. 이 직사각형은 주로 QGraphicsView 가 뷰의 기본 스크롤 가능 영역을 결정하는 데 사용되며, QGraphicsScene 가 항목 인덱싱을 관리하는 데 사용됩니다. 명시적으로 설정하지 않으면, 장면의 기본 사각형은 장면이 생성된 이후 장면 내 모든 항목의 가장 큰 경계 사각형이 됩니다. 즉, 장면에서 항목이 추가되거나 이동되면 사각형이 커지지만, 절대 줄어들지는 않습니다.

    scene.setItemIndexMethod(QGraphicsScene::NoIndex);

항목 인덱싱 기능은 항목 검색 속도를 높이는 데 사용됩니다. ` NoIndex `는 씬에 있는 모든 항목을 검색하므로 항목 위치 파악에 선형 시간 복잡도가 소요됨을 의미합니다. 그러나 항목 추가, 이동 및 제거는 상수 시간에 수행됩니다. 이 방식은 많은 항목이 지속적으로 추가, 이동 또는 제거되는 동적 씬에 이상적입니다. 대안으로는 BspTreeIndex 가 있으며, 이 방법은 이진 검색을 활용하여 로그급 근사적인 복잡도를 갖는 아이템 위치 파악 알고리즘을 구현합니다.

    for (int i = 0; i < MouseCount; ++i) {
        Mouse *mouse = new Mouse;
        mouse->setPos(::sin((i * 6.28) / MouseCount) * 200,
                      ::cos((i * 6.28) / MouseCount) * 200);
        scene.addItem(mouse);
    }

그런 다음 장면에 마우스를 추가합니다.

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));

씬을 볼 수 있도록 하려면 ` QGraphicsView ` 위젯도 만들어야 합니다. ` QGraphicsView ` 클래스는 스크롤 가능한 뷰포트 내에서 씬의 내용을 시각화합니다. 또한 내용이 앤티앨리어싱을 사용하여 렌더링되도록 하고, 뷰의 배경 브러시를 설정하여 치즈 배경을 만듭니다.

배경으로 사용되는 이미지는 Qt의 리소스 시스템을 사용하여 애플리케이션 실행 파일 내에 바이너리 파일로 저장됩니다. ` QPixmap ` 생성자는 디스크에 실제로 존재하는 파일을 가리키는 파일 이름과 애플리케이션에 내장된 리소스를 가리키는 파일 이름을 모두 받아들입니다.

    view.setCacheMode(QGraphicsView::CacheBackground);
    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
    view.setDragMode(QGraphicsView::ScrollHandDrag);

그런 다음 캐시 모드를 설정합니다. ` QGraphicsView `은 미리 렌더링된 콘텐츠를 픽스맵에 캐시할 수 있으며, 이 콘텐츠는 이후 뷰포트에 그려집니다. 이러한 캐싱의 목적은 텍스처, 그라데이션, 알파 블렌딩된 배경과 같이 렌더링 속도가 느린 영역의 전체 렌더링 시간을 단축하는 데 있습니다. ` CacheMode ` 속성은 뷰의 어느 부분이 캐시되는지를 지정하며, ` CacheBackground ` 플래그는 뷰의 배경 캐싱을 활성화합니다.

dragMode 속성을 설정함으로써, 사용자가 씬 배경을 클릭하고 마우스를 드래그할 때 어떤 동작이 수행될지 정의합니다. ScrollHandDrag 플래그를 활성화하면 커서가 가리키는 손 모양으로 바뀌며, 마우스를 드래그하면 스크롤바가 이동합니다.

    view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice"));
    view.resize(400, 300);
    view.show();

    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance);
    timer.start(1000 / 33);

    return app.exec();
}

마지막으로, QApplication::exec() 함수를 사용하여 메인 이벤트 루프에 진입하기 전에 애플리케이션 창의 제목과 크기를 설정합니다.

마지막으로, QTimer 를 생성하고, 그 timeout() 신호를 씬의 advance() 슬롯에 연결합니다. 타이머가 발동할 때마다 씬은 한 프레임씩 진행됩니다.

그런 다음 타이머가 1000/33 밀리초마다 발동되도록 설정합니다. 이렇게 하면 초당 30프레임의 프레임 속도가 나오며, 이는 대부분의 애니메이션에 충분히 빠른 속도입니다. 단일 타이머 연결을 통해 장면을 진행시키며 애니메이션을 수행하면 모든 마우스가 동시에 이동하게 되며, 무엇보다도 모든 마우스가 이동한 후에야 화면에 단 한 번의 업데이트만 전송됩니다.

예제 프로젝트 @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.