Обнаружение столкновений объектов на графической сцене Qt

Программирование Программирование на С++ Использование библиотеки Qt/QML Обнаружение столкновений объектов на графической сцене Qt

Помечено: ,

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 8 мес., 3 нед. назад.

  • Автор
    Сообщения
  • #4583
    @admin

    Напомню, что мы уже научились добавлять объекты на графическую сцену, анимировать их перемещение, а также использовать спрайтовую анимацию. Однако, если мы разрабатывали бы игру, то наши объекты должны были бы взаимодействовать, а нам пришлось бы обнаруживать их столкновения.

    Сегодня мы добавим в предыдущий пример деревья, через которые нельзя пройти, и еду, которую можно собрать. Исходный код можно взять в репозитории, а примерный вид того, что получится посмотреть:

    Если объекты на сцене имеют ограниченное число положений (например как в игре «полный квадрат«) — то все объекты можно поместить в матрицу. Если они попали в одной ячейке — значит произошло столкновение. Такой подход не является универсальным, Qt предлагает более гибкие механизмы.

    Пример Colliding Mice

    Чаще всего знакомство с обнаружением столкновений начинают с «Colliding Mice» из примеров стандартной библиотеки. В этом примере на сцену добавляется ряд мышек, которые перемещаются по некоторому закону, пр этом не накладываются друг на друга (в случае столкновения — расходятся). Нам важно знать, что для реализации такого поведения:

    1. Для мышки определен метод реализован метод shape:

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

    Тут видно, что вне зависимости от реальной формы мышки (ушей и хвоста), ее форма аппроксимируется овалом. При обнаружении столкновений все эти мышки рассматриваются именно как овалы. Однако, метод возвращает QPainterPath, а это мог бы быть гораздо более сложный объект.

    2. Работа графической сцены в этом примере «тактируется» таймером, объявленным в функции main. К этому таймеру привязан слот advance графической сцены, который по умолчанию вызывается метод advance для всех объектов графической сцены (он есть в QGraphicsItem):

    main() {
      QGraphicsScene scene;
      QTimer timer;
      // ...
      QObject::connect(&timer, SIGNAL(timeout()), &scene, SLOT(advance()));
      // ...
    }

    В методе Mouse::advance и выполняется «обнаружение столкновений», для этого со сцены выбираются все объекты, попадающие в некоторую область и с их четом для мышки выбирается новое направление и осуществляется перемещение:

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

    Несмотря на то, что это выдается как «хороший пример», мне он не кажется удачным. Мне не нравится подход с использованием слота advance, в предыдущей статье я описывал стандартный механизм анимации свойств Qt, который позволяет в частности изменять координаты объекта на сцене. Однако, при использовании такого механизма, каждый объект «тактируется» своим собственным таймером. Мы говорим лишь «перемести объект из (X1, Y1) в (X2, Y2) за N секунд». Поэтому рассмотрим другой пример…

    Пример обнаружения коллизий на графической сцене Qt

    Для начала опишем классы для объектов еды и деревьев:

    class GraphicsTree : public QObject, public QGraphicsEllipseItem {
      Q_OBJECT
    public:
      explicit GraphicsTree(QRect rect, QObject *parent = 0);
    };
    
    // ...
    
    #include <QBrush>
    
    GraphicsTree::GraphicsTree(QRect rect, QObject *parent)
      : QGraphicsEllipseItem(rect), QObject(parent) {
      this->setBrush(QColor(0, 255, 0));
    }

    Аналогично выглядит класс GraphicsFood. В примере используются круги, поэтому я наследовал стандартный класс QGraphicsEllipseItem, для которого уже реализован метод shape(). Если у вас более сложный объект — придется реализовывать этот метод самостоятельно.

    Класс AnimatedGraphicsItem (анимированного ежика) наследуется от стандартного класса QGraphicsPixmapItem, который отвечает за отображение картинки на сцене (в нашем случае растровой). Для этого класса я также не стал реализовывать метод shape(), однако обратите внимание на приведенную выше запись экрана с процессом «игры» — там где картинка прозрачная столкновение не детектируется, в Qt это уже реализовано (вручную реализовать было бы весьма сложно).

    Добавим объекты на сцену. Я сделал это в конструкторе класса View:

      m_scene.addItem(new GraphicsTree(QRect(50, 80, 100, 100)));
      m_scene.addItem(new GraphicsTree(QRect(250, 60, 150, 150)));
      m_scene.addItem(new GraphicsTree(QRect(-60, -30, 50, 50)));
    
      for (int i = 0; i < 80; ++i)
        m_scene.addItem(new GraphicsFood(QPointF(qrand()%400 - 50,qrand()%400 - 50)));

    Теперь объекты отображаются, а нашим ежиком мы по прежнему можем управлять с помощью клавиатуры. Несмотря на то, что границы объектов нам определять не пришлось (хватило стандартной реализации метода shape), наш ежик свободно может ходить по деревьям и не кушает еду, исправим это.

    Получить список объектов сцены, с которыми пересекается текущий (this) объект можно получить так:
    QList<QGraphicsItem *> colliding = scene()->collidingItems(this);

    Нам бы хотелось получать такой список в методе изменения координат объекта, однако сделать так нельзя, ведь метод setPos не является виртуальным. Однако, после каждого изменения координат наш объект перерисовывается, т.е. вызывается метод paint, поэтому обработать коллизии можно в нем. Кстати, в случае коллизии наш объект «уже переместился куда не следовало», поэтому чтобы вернуть его назад — сохраним позицию до коллизии, в класс GraphicsActor я добавил поле QPointF m_posBeforeCollision. Итак:

    bool GraphicsActor::processCollidings(QList<QGraphicsItem *> collidins) {
      bool can_move = true;
      for (QGraphicsItem* item: collidins) {
        if (dynamic_cast<GraphicsTree*> (item)) {
          can_move = false;
        }
        else if (dynamic_cast<GraphicsFood*> (item)) {
          static_cast<GraphicsFood*>(item)->deleteLater();
        }
      }
      return can_move;
    }
    
    void GraphicsActor::paint(QPainter* painter,
                              const QStyleOptionGraphicsItem* option,
                              QWidget* widget) {
      QPointF currentPos = pos();
      QList<QGraphicsItem *> colliding = scene()->collidingItems(this);
    
      if (processCollidings(colliding) == false) {
        if (m_moveAnimation) {
          m_moveAnimation->deleteLater();
          m_moveAnimation = nullptr;
        }
        setSprites(ActorActions::Stay);
        m_currectAction = ActorActions::Stay;
    
        setPos(m_posBeforeCollision);
      }
      else {
        m_posBeforeCollision = currentPos;
      }
      AnimatedGraphicsItem::paint(painter, option, widget);
    }

    В методе paint получаем список объектов, с которыми есть наложение. Этот список передаем в функцию processCollidings, которую сами написали. В этой функции перебираем элементы списка: если встречаем объект типа GraphicsFood — то уничтожаем этот объект (тут мы могли бы например увеличивать счетчик игровых очков); если же встречается объект типа GraphicsTree — то устанавливается флажок запрещающий перемещение. Собственно этот флажок функция и возвращает.

    Если функция processCollidings вернула true — то перемещаться можно (мы не уперлись в дерево или какую-нибудь стену), поэтому обновим значение поля m_posBeforeCollision. Если же перемещаться нельзя — удаляем объект анимации свойств и обнуляем указатель на него, устанавливаем состояние (и набор спрайтов), соответствующее остановке ежика и возвращаем объект на позицию до коллизии.

    PS. После публикации предыдущих статей по разработке игр мне писало множество ребят, которые просили «научить писать игры». Процентов 40 из них — студенты-программисты. Всем им в качестве «проверки на вшивость» давал задание — добавить в заготовку со спрайтовой анимацией обнаружение коллизии. Не справился никто, хотя: 1) требовалось разобраться в совсем небольшом кусочке кода, написанном достаточно «чисто»; 2) к этому коду были написаны пояснения в виде статей на форуме; 3) добавить надо было всего строчек 100 кода. Удивляют эти «разработчики».

Для ответа в этой теме необходимо авторизоваться.