- 1. Программный код урока
- 2. apple.h
- 3. apple.cpp
- 4. triangle.h
- 5. triangle.cpp
- 6. widget.h
- 7. widget.cpp
- 8. Итог
- 9. Видеоурок
После того, как в игре присутствует анимированная Муха , которая передвигается под воздействием клавиш клавиатуры, настало время добавить смысл в игру. Добавим цель Мухе , например, она будет поедать яблоки, которые будут подсчитываться. То есть необходимо настроить взаимодействие Мухи с другими объектами, в данном случае с яблоками.
Для яблок необходимо создать отдельный класс, который также будет отнаследован от QGraphicsItem. При создании яблоки должны иметь случайный цвет, что также будет инициализироваться в конструкторе класса яблок с помощью функции qrand() .
Когда Муха натыкается на яблоко, необходимо передавать данную информацию в ядро игры, которое находится в классе widget. Для этого будем при движении Мухи анализировать объекты, которые попадают в поле её зрения, и с которыми необходимо отработать взаимодействие.
/* Производим проверку на то, наткнулась ли муха на какой-нибудь * элемент на графической сцене. * Для этого определяем небольшую область перед мухой, * в которой будем искать элементы * */ QList<QGraphicsItem *> foundItems = scene()->items(QPolygonF() << mapToScene(0, 0) << mapToScene(-20, -20) << mapToScene(20, -20)); /* После чего проверяем все элементы. * Один из них будет сама Муха - с ней ничего не делаем. * А с остальными высылаем сигнал в ядро игры * */ foreach (QGraphicsItem *item, foundItems) { if (item == this) continue; emit signalCheckItem(item); }
Как только Муха обнаружила объект, отличный от неё самой, то передаём его в ядро игры, где игра проверит, что объект является яблоком и удалит его, увеличив счёт игры на единицу.
void Widget::slotDeleteApple(QGraphicsItem *item) { /* Получив сигнал от Мухи * Перебираем весь список яблок и удаляем найденное яблоко * */ foreach (QGraphicsItem *apple, apples) { if(apple == item){ scene->removeItem(apple); // Удаляем со сцены apples.removeOne(item); // Удаляем из списка delete apple; // Вообще удаляем ui->lcdNumber->display(count++); /* Увеличиваем счёт на единицу * и отображаем на дисплее * */ } } }
Создание же яблок, с которыми будет происходить взаимодействие должно осуществляться с определённой периодичностью, что будет инициализироваться специальным таймером.
/* Раз в секунду отсылаем сигнал на создание яблока в игре * */ timerCreateApple = new QTimer(); connect(timerCreateApple, &QTimer::timeout, this, &Widget::slotCreateApple); timerCreateApple->start(1000); /* Подключаем сигнал от Мухи, в котором передаются Объекты, на которые * наткнулась Муха * */ connect(triangle, &Triangle::signalCheckItem, this, &Widget::slotDeleteApple);
Создание яблока производится в функции slotCreateApple()
void Widget::slotCreateApple() { Apple *apple = new Apple(); // Создаём яблоко scene->addItem(apple); // Помещаем его в сцену со случайной позицией apple->setPos((qrand() % (251)) * ((qrand()%2 == 1)?1:-1), (qrand() % (251)) * ((qrand()%2 == 1)?1:-1)); apple->setZValue(-1); /* Помещаем яблоко ниже Мухи, то есть Муха * на сцене будет выше яблок * */ apples.append(apple); // Добавляем Яблоко в Список }
Программный код урока
В данном уроке вносятся изменения в программный код предыдущих из предыдущих уроков.
После того, как описан процесс взаимодействия, привожу полный программный код класса, отвечающего за создание яблок.
apple.h
В данном файле добавлена лишь переменная, которая будет отвечать за
#ifndef APPLE_H #define APPLE_H #include <QObject> #include <QGraphicsItem> #include <QGraphicsScene> #include <QPainter> class Apple : public QObject, public QGraphicsItem { Q_OBJECT public: explicit Apple(QObject *parent = 0); ~Apple(); signals: public slots: protected: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); private: int color; }; #endif // APPLE_H
apple.cpp
#include "apple.h" Apple::Apple(QObject *parent) : QObject(parent), QGraphicsItem() { /* Устанавливаем улчай номер цвета яблока * */ color = qrand() % ((3 + 1) - 1) + 1; } Apple::~Apple() { } QRectF Apple::boundingRect() const { return QRectF(-20,-20,40,40); // Ограничиваем область, в которой лежит яблоко } void Apple::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // Тушка яблока QPainterPath path(QPointF(0,-10)); path.cubicTo(0,-10, -5,-14, -12,-8); path.cubicTo(-12,-8, -20,12, -10,15); path.cubicTo(-10,15, -5,20, 0,16); path.cubicTo(0,16, 5,20, 10,15); path.cubicTo(10,15, 20,12, 12,-8); path.cubicTo(12,-8, 5,-14, 0,-10); /* Выбираем цвет яблока по случайно сгенерированному номеру * */ switch (color) { case 1: painter->setBrush(Qt::red); break; case 2: painter->setBrush(Qt::green); break; case 3: painter->setBrush(Qt::yellow); break; } painter->drawPath(path); // Хвостик яблока painter->setPen(QPen(Qt::black, 2)); QPainterPath path2(QPointF(0,-10)); path2.cubicTo(0,-10,4,-18,10,-20); painter->setBrush(Qt::NoBrush); painter->drawPath(path2); //Листик яблока painter->setPen(QPen(Qt::black, 1)); QPainterPath path3(QPointF(0,-10)); path3.cubicTo(0,-10,-2,-20,-15,-20); path3.cubicTo(-15,-20,-14,-12,0,-10); painter->setBrush(Qt::green); painter->drawPath(path3); Q_UNUSED(option); Q_UNUSED(widget); }
triangle.h
Также необходимо добавить сигнал в заголовочный файл Мухи , который будет передавать в ядро игры объекты, на которые натолкнулась Муха.
signals: /* Сигнал, который передаётся в ядро игры с элементом QGraphicsItem, * на который наткнулась муха, и требуется принять решение о том, * что с этим элементом делать. * */ void signalCheckItem(QGraphicsItem *item);
triangle.cpp
В данном классе изменения вносятся лишь в функцию-слот, которая отвечает за обработку сигнала от игрового таймера.
void Triangle::slotGameTimer() { /* Проверяем, нажата ли была какая-либо из кнопок управления объектом. * Прежде чем считать шажки * */ /* Программный код из прошлого урока */ /* Производим проверку на то, наткнулась ли муха на какой-нибудь * элемент на графической сцене. * Для этого определяем небольшую область перед мухой, * в которой будем искать элементы * */ QList<QGraphicsItem *> foundItems = scene()->items(QPolygonF() << mapToScene(0, 0) << mapToScene(-20, -20) << mapToScene(20, -20)); /* После чего проверяем все элементы. * Один из них будет сама Муха - с ней ничего не делаем. * А с остальными высылаем сигнал в ядро игры * */ foreach (QGraphicsItem *item, foundItems) { if (item == this) continue; emit signalCheckItem(item); } /* Проверка выхода за границы поля * Если объект выходит за заданные границы, то возвращаем его назад * */ /* Программный код из прошлого урока */ }
widget.h
Ядро игры развивается и усложняется. На этот раз необходимо добавить таймер для периодического создания яблок в игре, Список, в котором будут храниться все яблоки и счетчик очков. А также необходимо добавить слоты для создания и удаления яблок.
private: QTimer *timerCreateApple; // Таймер для периодического создания яблок в игре QList<QGraphicsItem *> apples; // Список со всеми яблоками, присутствующими в игре double count; // Переменная, которая хранит счёт игре private slots: // Слот для удаления яблок если Муха наткнулая на это яблоко void slotDeleteApple(QGraphicsItem * item); void slotCreateApple(); // Слот для создания яблок, срабатывает по таймеру
widget.cpp
Реализуем функции-слоты, а также настраиваем коннекты для работы с объектами в игре.
#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); this->resize(600,640); /// Задаем размеры виджета, то есть окна this->setFixedSize(600,640); /// Фиксируем размеры виджета count = 0; scene = new QGraphicsScene(); /// Инициализируем графическую сцену triangle = new Triangle(); /// Инициализируем треугольник ui->graphicsView->setScene(scene); /// Устанавливаем графическую сцену в graphicsView ui->graphicsView->setRenderHint(QPainter::Antialiasing); /// Устанавливаем сглаживание ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); /// Отключаем скроллбар по вертикали ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); /// Отключаем скроллбар по горизонтали scene->setSceneRect(-250,-250,500,500); /// Устанавливаем область графической сцены scene->addItem(triangle); /// Добавляем на сцену треугольник triangle->setPos(0,0); /// Устанавливаем треугольник в центр сцены /* Инициализируем таймер и вызываем слот обработки сигнала таймера * у Треугольника 100 раз в секунду. * Управляя скоростью отсчётов, соответственно управляем скоростью * изменения состояния графической сцены * */ timer = new QTimer(); connect(timer, &QTimer::timeout, triangle, &Triangle::slotGameTimer); timer->start(1000 / 100); /* Раз в секунду отсылаем сигнал на создание яблока в игре * */ timerCreateApple = new QTimer(); connect(timerCreateApple, &QTimer::timeout, this, &Widget::slotCreateApple); timerCreateApple->start(1000); /* Подключаем сигнал от Мухи, в котором передаются Объекты, на которые * наткнулась Муха * */ connect(triangle, &Triangle::signalCheckItem, this, &Widget::slotDeleteApple); } Widget::~Widget() { delete ui; } void Widget::slotDeleteApple(QGraphicsItem *item) { /* Получив сигнал от Мухи * Перебираем весь список яблок и удаляем найденное яблоко * */ foreach (QGraphicsItem *apple, apples) { if(apple == item){ scene->removeItem(apple); // Удаляем со сцены apples.removeOne(item); // Удаляем из списка delete apple; // Вообще удаляем ui->lcdNumber->display(count++); /* Увеличиваем счёт на единицу * и отображаем на дисплее * */ } } } void Widget::slotCreateApple() { Apple *apple = new Apple(); // Создаём яблоко scene->addItem(apple); // Помещаем его в сцену со случайной позицией apple->setPos((qrand() % (251)) * ((qrand()%2 == 1)?1:-1), (qrand() % (251)) * ((qrand()%2 == 1)?1:-1)); apple->setZValue(-1); /* Помещаем яблоко ниже Мухи, то есть Муха * на сцене будет выше яблок * */ apples.append(apple); // Добавляем Яблоко в Список }
Итог
В результате у Вас на графической сцене будут случайно возникать яблоки, которые будет поедать Муха , а также количество съеденных яблок будет учитываться в счётчике.
Также в видеоуроке по данной статье даны дополнительные комментарии и продемонстрирована работа игры.
Полный список статей данного цикла:
- Урок 1. Как написать игру на Qt. Управление объектом
- Урок 2. Как написать игру на Qt. Анимация героя игры (2D)
- Урок 3. Как написать игру на Qt. Взаимодействие с другими объектами
- Урок 4. Как написать игру на Qt. Враг - смысл в выживании
- Урок 5. Как написать игру на Qt. Добавляем звук с QMediaPlayer
Классно пишешь! Мне прям нравится правильная подача "логики" ООП! Легкая корректировка: apples.append(apple); // Добавляем яблоко в Список И вопрос: foreach (QGraphicsItem *item, foundItems) { if (item == this) continue; emit signalCheckItem(item); } где формируется указатель item?
В данном случае он формируется в следующем участке кода:
Класс Apple является наследником QGraphicsItem
И еще пару вопросов! =) Почему именно в .h файле устанавливается указатель на родителя предка? Почему ты использовал ключевое слово explicit?
Почему указатель на предка передается в QObject? Почему мы обделили QGraphicsItem?excplicit - убирает возможность неявного преобразования. Неявные преобразования вообще могут бед наделать, лучше отключать такую возможность для конструкторов сложных классов.
Установка указателя на предка в конструкторе - это стандартная практика для Qt фреймворка. Указатель на parent присутствует в конструкторах всех классов, которые наследованы от базового класса QObject.
QGraphicsItem - является одним из тех немногих классов, которые не наследованы от QObject. Поэтому приходится применять множественное наследование, чтобы использовать систему сигналов и слотов, которая работает только с теми классами, которые были наследованы от QObject. Соответственно parent передаётся в базовый класс QObject.
https://www.slideshare.net/IgorShkulipa/c-stl-qt-03 исходя из 6 слайда: "Инструментарий спроктирован так, что для QObject и всех его потомков конструктор копирования и оператор присваивания недоступны- они объявлены в раделе private через макрос Q_DISABLE_COPY;
Только вот зачем оно нужно? (или не заморачиваться и все время писать explicit?)
Если делать неявное преобразование, как вы его показали, то да, работать не будет:
А вот если использовать новомодный синтаксис из более свежих стандартов:
И Q_DISABLE_COPY здесь не спасает на данный момент, он условно только "отключает копирование". А excplicit выключит вариант неявного преобразования с новым синтаксисом.
А вот в чем смысл этих всех запретов?
На мой взгляд, неявные преобразования могут носить достаточно непредсказуемый характер в зависимости от компиляторов.
На Qt/С++ я конечно давно не сталкивался с проблемами неявных преобразований, видимо сказывается наличие практики этих самых запретов и ограничений. Но когда занимался разработкой на Си/С++ под AVR и STM32, то там эти преобразования крови много попортили.
Вообще, в чём смысл всех запретов? - Контроль и единообразие, чтобы снизить хаос в проекте.
Я все-таки не понимаю с item! В этой функции то понятно- item передается как аргумент.
А вот тут: Как foreach понимает какой из указателей QGraphicsItem он сейчас будет перебирать?Вопрос снят.
foreach перебирает все указатели, которые находятся в контейнере foundItems
foreach абсолютно всё равно, какие в контейнере классы, главное, чтобы у них один общий базовый класс, в данном случае - это QGraphicsItem . То есть: QGraphicsItem можно скастовать в Apple или в Triangle , поскольку они наследованы от QGraphicsItem, просто если класс изначально задавался как Apple, его не получится скастовать из QGraphicsItem в Trianle. Это возможно, благодаря парадигме полиморфизма
А сам по себе foreach можете перебирать все объекты в контейнере QList, поскольку данный контейнер имеет итератор. Не было бы итератора, то и не смог бы перебирать.
P/S/ для вставки программного кода в комментарий используйте, пожалуйста, специальный диалог для вставки кода. Он вызывается кнопочкой с иконкой "{}" на тулбаре редактора комментариев. Также рядом есть кнопочка предварительного просмотра комментария.
И еще вопрос: А когда именно вызывается метод paint? Ну понятно что по идее- при столкновении, по таймеру при перерисовке. Я то сначала думал, что он вызывается после метода setRotation() или setPos(). Стал смотреть в отладчике, а нифига!
Метод update() вызывает принудительную перерисовку, также вызывается по некоторым методам, таким как setX(), setY() во внутренностях Qt. Некоторые методы, наподобие paint, вызываются в порядке внутренней очереди. Здесь уже нужно смотреть исходники Qt.
у меня такая проблема .делал все как на видео все понятно .но выдает ошибку ошибка: 'Apple' was not declared in this scope ошибка: 'apple' was not declared in this scope ошибка: expected type-specifier before 'Apple' : ошибка: expected ';' before 'Apple' ругается на одну строчку 4 раза вот эта строчка Apple *apple = new Apple(); // Создаём яблоко
http://imglink.ru/show-image.php?id=709c66221545c26eec66d6586f2b7ff1 вот скриншот
Когда у меня подобного рода "лабуда" я делаю следующией действия: 1. пересобираю проект 2. Удаляю папку debug и релиз, все make и qmake файлы 3. Перезапускаю qt Таких вот "чудес" раз по 5 на день. + теневую сборку отключаю
Скорее всего заголовочный файл не подключён в файле widget.cpp.
Спасибо .чет не заметил что не подключил
При столкновении объектов отсылается сигнал signalCheckItem, который содержит указатель на объект. В ядре игры проводится проверка. Если объект == apple, то выполняется удаление объекта.
Зачем нам нужен список с указателями на все яблоки? Почему бы в ядре не выполнять код: Еще вопрос. Зачем вызывать функцию removeItem, если вызывается оператор delete?Подумайте немного над этим кодом, что вы привели, откуда вы возьмёте объект apple в вашем варианте кода, кроме как не из списка с указателями на яблоки? С данной архитектурой программы необходимо наличие списка с указателями на яблоки.
Что касается удаления, ну примените вы delete. И получите протухший указатель в самой графической сцене. Всё и везде чистить нужно.
А если применить приведение типов? Enemy01 *itemEnemy01 = dynamic_cast
(item);
Вот теперь, это будет правильнее. А теперь ответьте сами себе на вопрос. Много ли начинающих программистов, которые прочитали эту статью разбираются в приведении типов и множестве других нюансов? Вообще, можно было сделать удаление этих объектов и внутри графической сцены. И при этом стоит воспользоваться соответствующим кастом из Qt. То есть qgraphicsitem_cast и т.д. Или вообще создать специальный базовый класс, который будет иметь общий базовый метод, который будет возвращать тип объекта и уже по типу объекта принимать решение об удалении и т.д. Поскольку, если программа имеет слишком много кастов, то это тоже не очень хорошо и говорит о непродуманной архитектуре классов.