- 1. Програмний код уроку
- 2. apple.h
- 3. apple.cpp
- 4. трикутник.h
- 5. triangle.cpp
- 6. віджет.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); }
трикутник.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); } /* Проверка выхода за границы поля * Если объект выходит за заданные границы, то возвращаем его назад * */ /* Программный код из прошлого урока */ }
віджет.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 и т.д. Или вообще создать специальный базовый класс, который будет иметь общий базовый метод, который будет возвращать тип объекта и уже по типу объекта принимать решение об удалении и т.д. Поскольку, если программа имеет слишком много кастов, то это тоже не очень хорошо и говорит о непродуманной архитектуре классов.