- 1. Враг мухи в структуре проекта
- 2. spider.h
- 3. spider.cpp
- 4. widget.h
- 5. widget.cpp
- 6. Итог
- 7. Видеоурок
Продолжаем тему того, как написать игру на Qt. После того, как в прошлых статьях была создана Муха, которая поедает яблоки, настало время создать ей врага. А Враг Мухи , как известно, Паук . Создание игровых персонажей, которые будут участвовать в непосредственной жизни Вашего главного героя - это не только отрисовка анимации действий и передвижения, а также логика реакций на воздействия игрока, но и искусственный интеллект, в соответствии с логикой которого будет определятся поведение игрового персонажа. Таким образом, мы добавляем в игру новый смысл, не только съесть как можно больше яблок, но и выжить любой ценой.
Определим поведение Паука в данной версии игры. Что же он должен делать? Да самое обычное из всех действий - охотиться на Муху , просто гоняться за ней по игровому полю.
Также добавим в игру кнопку для запуска игрового процесса, и паузу, и самое главное, что добавим - это Game Over.
Враг мухи в структуре проекта
Как и в случае с Мухой в структуру проекта добавляется дополнительный класс, который будет отвечать за объект, которым является Паук .
- spider.h - заголовочный файл класса, отвечающего за Паука
- spider.cpp - файл исходных кодов, отвечающий за Паука
spider.h
Отличие данного файла от заголовочного файла мухи заключается в том, что в нём объявлен игровой таймер, который отвечает за поведение Паука , то есть враг Мухи тактируется не от таймера в ядре игры, а от собственного внутреннего таймера. Также при инициализации паука, в него закладывается его цель, то есть Муха , за которой он неустанно следует. В данном случае искусственный интеллект примитивен до безобразия, но большего на данный момент и не требуется.
#ifndef SPIDER_H #define SPIDER_H #include <QObject> #include <QGraphicsItem> #include <QGraphicsScene> #include <QPainter> #include <QTimer> #include <QDebug> class Spider : public QObject, public QGraphicsItem { Q_OBJECT public: explicit Spider(QGraphicsItem * target, QObject *parent = 0); ~Spider(); void pause(); // Сигнал для инициализации паузы signals: void signalCheckGameOver(); // Сигнал на вызов состояния Game Over public slots: protected: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); private: qreal angle; // Угол поворота графического объекта int steps; // Номер положения ножек паука int countForSteps; // Счетчик для изменения полоэения ножек QTimer *timer; // Внутренний таймер паука, по которому инициализируется его движение QGraphicsItem * target; // Цель паука, данный объект приравнивается объекту Мухи private slots: void slotGameTimer(); // Слот игрового таймера паука }; #endif // SPIDER_H
spider.cpp
Исходный код Паука аналогичен исходному коду Мухи , с тем отличием, что в игровом Слоте Паука отслеживается положение Мухи на игровом поле, и Паук разворачивается в сторону Мухи и ползёт за ней. А как только враг мухи натыкается на какой-нибудь из объектов на графической сцене, то он производит проверку, является ли этот объект Мухой , если да, то инициализируется процедура Game Over .
#include "spider.h" #include "math.h" static const double Pi = 3.14159265358979323846264338327950288419717; static double TwoPi = 2.0 * Pi; static qreal normalizeAngle(qreal angle) { while (angle < 0) angle += TwoPi; while (angle > TwoPi) angle -= TwoPi; return angle; } Spider::Spider(QGraphicsItem *target, QObject *parent) : QObject(parent), QGraphicsItem() { angle = 0; // Задаём угол поворота графического объекта steps = 0; // Задаём стартовое положение ножек мухи countForSteps = 0; // Счётчик для отсчета тиков таймера, при которых мы нажимали на кнопки setRotation(angle); // Устанавливаем угол поворота графического объекта this->target = target; // Устанавливаем цель паука timer = new QTimer(); // Инициализируем игровой таймер паука // подключаем сигнал таймера к игровому слоту паука connect(timer, &QTimer::timeout, this, &Spider::slotGameTimer); timer->start(15); // Запускаем таймер } Spider::~Spider() { } QRectF Spider::boundingRect() const { return QRectF(-40,-50,80,100); } void Spider::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // Рисуем ножки, без ножек паук не догонит муху painter->setPen(QPen(Qt::black, 2)); if(steps == 0){ // Первое положение ножек // Left 1 painter->drawLine(-24,-45,-28,-35); painter->drawLine(-28,-35,-22,-10); painter->drawLine(-22,-10,0,0); // Right 1 painter->drawLine(24,-45,28,-35); painter->drawLine(28,-35,22,-10); painter->drawLine(22,-10,0,0); // Left 2 painter->drawLine(-35,-38,-30,-18); painter->drawLine(-30,-18,-25,-3); painter->drawLine(-25,-3,0,0); // Right 2 painter->drawLine(35,-38,30,-18); painter->drawLine(30,-18,25,-3); painter->drawLine(25,-3,0,0); // Left 3 painter->drawLine(-35,38,-30,18); painter->drawLine(-30,18,-25,3); painter->drawLine(-25,3,0,0); // Right 3 painter->drawLine(35,38,30,18); painter->drawLine(30,18,25,3); painter->drawLine(25,3,0,0); // Left 4 painter->drawLine(-24,45,-28,35); painter->drawLine(-28,35,-22,10); painter->drawLine(-22,10,0,0); // Right 4 painter->drawLine(24,45,28,35); painter->drawLine(28,35,22,10); painter->drawLine(22,10,0,0); } else if (steps == 1){ // Второе положение ножек // Left 1 painter->drawLine(-23,-40,-24,-30); painter->drawLine(-24,-30,-19,-9); painter->drawLine(-19,-9,0,0); // Right 1 painter->drawLine(20,-50,23,-40); painter->drawLine(23,-40,15,-12); painter->drawLine(15,-12,0,0); // Left 2 painter->drawLine(-30,-35,-27,-24); painter->drawLine(-27,-24,-23,-5); painter->drawLine(-23,-5,0,0); // Right 2 painter->drawLine(40,-27,35,-10); painter->drawLine(35,-10,28,-1); painter->drawLine(28,-1,0,0); // Left 3 painter->drawLine(-40,27,-35,10); painter->drawLine(-35,10,-28,1); painter->drawLine(-28,1,0,0); // Right 3 painter->drawLine(30,35,27,24); painter->drawLine(27,24,23,5); painter->drawLine(23,5,0,0); // Left 4 painter->drawLine(-20,50,-27,30); painter->drawLine(-27,30,-20,12); painter->drawLine(-20,12,0,0); // Right 4 painter->drawLine(23,40,24,30); painter->drawLine(24,30,19,9); painter->drawLine(19,9,0,0); } else if (steps == 2){ // Третье положение ножек // Left 1 painter->drawLine(-20,-50,-23,-40); painter->drawLine(-23,-40,-15,-12); painter->drawLine(-15,-12,0,0); // Right 1 painter->drawLine(23,-40,24,-30); painter->drawLine(24,-30,19,-9); painter->drawLine(19,-9,0,0); // Left 2 painter->drawLine(-40,-27,-35,-10); painter->drawLine(-35,-10,-28,-1); painter->drawLine(-28,-1,0,0); // Right 2 painter->drawLine(30,-35,27,-24); painter->drawLine(27,-24,23,-5); painter->drawLine(23,-5,0,0); // Left 3 painter->drawLine(-30,35,-27,24); painter->drawLine(-27,24,-23,5); painter->drawLine(-23,5,0,0); // Right 3 painter->drawLine(40,27,35,10); painter->drawLine(35,10,28,1); painter->drawLine(28,1,0,0); // Left 4 painter->drawLine(-23,40,-24,30); painter->drawLine(-24,30,-19,9); painter->drawLine(-19,9,0,0); // Right 4 painter->drawLine(20,50,27,30); painter->drawLine(27,30,20,12); painter->drawLine(20,12,0,0); } painter->setPen(QPen(Qt::black, 1)); // Левое Жвало QPainterPath path1(QPointF(0, -20)); path1.cubicTo(0, -20, -5, -25, -3, -35); path1.cubicTo(-3,-35,-15,-25,-8,-17); path1.cubicTo(-8,-17,-5,15,0,-20 ); painter->setBrush(Qt::black); painter->drawPath(path1); // Правое Жвало QPainterPath path2(QPointF(0, -20)); path2.cubicTo(0, -20, 5, -25, 3, -35); path2.cubicTo(3,-35,15,-25,8,-17); path2.cubicTo(8,-17,5,15,0,-20 ); painter->setBrush(Qt::black); painter->drawPath(path2); // Голова painter->setBrush(QColor(146, 115, 40, 255)); painter->drawEllipse(-10,-25,20,15); // Тушка painter->drawEllipse(-15, -15, 30, 30); // Жопка painter->drawEllipse(-20, 0, 40,50); painter->setPen(QPen(Qt::white,3)); painter->drawLine(-10,25,10,25); painter->drawLine(0,35,0,15); // Левое глазище painter->setPen(QPen(Qt::black,1)); painter->setBrush(Qt::red); painter->drawEllipse(-8,-23,6,8); // Правое глазище painter->setBrush(Qt::red); painter->drawEllipse(2,-23,6,8); Q_UNUSED(option); Q_UNUSED(widget); } void Spider::slotGameTimer() { // Определяем расстояние до Мухи QLineF lineToTarget(QPointF(0, 0), mapFromItem(target, 0, 0)); // Угол поворота в направлении к Мухе qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length()); if (lineToTarget.dy() < 0) angleToTarget = TwoPi - angleToTarget; angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2); /* В Зависимости от того, слева или справа находится Муха от Паука, * устанавливаем направление поворота паука в данном тике таймера * Скорость разворота зависит от угла, на который необходимо повернуться пауку * */ if (angleToTarget > 0 && angleToTarget < Pi) { // Rotate left if(angleToTarget > Pi / 5){ angle = -15; } else if(angleToTarget > Pi / 10){ angle = -5; } else { angle = -1; } } else if (angleToTarget <= TwoPi && angleToTarget > (TwoPi - Pi)) { // Rotate right if(angleToTarget < (TwoPi - Pi / 5)){ angle = 15; } else if(angleToTarget < (TwoPi - Pi / 10)){ angle = 5; } else { angle = 1; } } else if(angleToTarget == 0) { angle = 0; } setRotation(rotation() + angle); // Разворачиваемся // Бежим в сторону мухи if(lineToTarget.length() >= 40){ setPos(mapToParent(0, -(qrand() % ((4 + 1) - 1) + 1))); // Двигаем ножками, Dance, dance, Spidy !!! countForSteps++; if(countForSteps == 6){ steps = 1; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 12){ steps = 0; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 18){ steps = 2; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 24) { steps = 0; update(QRectF(-40,-50,80,100)); countForSteps = 0; } } /* Производим проверку на то, наткнулся ли паук на какой-нибудь * элемент на графической сцене. * Для этого определяем небольшую область перед пауком, * в которой будем искать элементы * */ QList<QGraphicsItem *> foundItems = scene()->items(QPolygonF() << mapToScene(0, 0) << mapToScene(-2, -2) << mapToScene(2, -2)); /* После чего проверяем все элементы. * Один из них будет сам Паук - с ним ничего не делаем. * А с остальными высылаем сигнал в ядро игры * */ foreach (QGraphicsItem *item, foundItems) { if (item == this) continue; if(item == target){ emit signalCheckGameOver(); } } /* Проверка выхода за границы поля * Если объект выходит за заданные границы, то возвращаем его назад * */ if(this->x() - 10 < -250){ this->setX(-240); // слева } if(this->x() + 10 > 250){ this->setX(240); // справа } if(this->y() - 10 < -250){ this->setY(-240); // сверху } if(this->y() + 10 > 250){ this->setY(240); // снизу } } /* Функция паузы, отвечает за включение и отключение паузы * */ void Spider::pause() { if(timer->isActive()){ timer->stop(); } else { timer->start(15); } }
widget.h
В заголовочном файле ядра приложения необходимо определить объект, который отвечает за Паука, переменную состояния игры и Горячую клавишу паузы. А также слоты для обработки запуска игры, паузы и процедуры Game Over .
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QGraphicsScene> #include <QGraphicsItem> #include <QShortcut> #include <QDebug> #include <QTimer> #include <QMessageBox> #include <triangle.h> #include <apple.h> #include <spider.h> #define GAME_STOPED 0 #define GAME_STARTED 1 namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private: Ui::Widget *ui; QGraphicsScene *scene; /// Объявляем графическую сцену Triangle *triangle; /// и треугольник QTimer *timer; /** Объявляем игровой таймер, благодаря которому * будет производиться изменения положения объекта на сцене * При воздействии на него клавишами клавиатуры * */ QTimer *timerCreateApple; /// Таймер для периодического создания яблок в игре QList<QGraphicsItem *> apples; /// Список со всеми яблоками, присутствующими в игре double count; /// Переменная, которая хранит счёт игре Spider *spider; // Объект Паука QShortcut *pauseKey; // Горячая клавиша, отвечающая за паузу в игре int gameState; /* Переменная, которая хранит состояние игры. * То есть, если игра запущена, то статус будет GAME_STARTED, * в противном случае GAME_STOPED * */ private slots: /// Слот для удаления яблок если Муха наткнулась на это яблоко void slotDeleteApple(QGraphicsItem * item); void slotCreateApple(); /// Слот для создания яблок, срабатывает по таймеру void on_pushButton_clicked(); // Слот для запуска игры void slotGameOver(); // Слот инициализации Game Over void slotPause(); // Слот для обработки паузы }; #endif // WIDGET_H
widget.cpp
По сравнению с программным кодом, который был в предыдущих уроках, с данным файлом в этом уроке необходимо основательно поработать. Поскольку игра, стартует по нажатию кнопки pushButton, то и программный код инициализации объектов и запуска таймеров необходимо вынести в слот, которому передаётся сигнал от кнопки. Также возникает необходимость реализовать функцию паузы в игре, по которой все игровые таймеры останавливаются и объекты не могут передвигаться или производить иных действий. А в слоте, который отвечает за Game Over необходимо правильно обработать удаление всех объектов с игровой сцены.
#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); /// Фиксируем размеры виджета scene = new QGraphicsScene(); /// Инициализируем графическую сцену 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); /// Устанавливаем область графической сцены timer = new QTimer(); timerCreateApple = new QTimer(); gameState = GAME_STOPED; pauseKey = new QShortcut(this); pauseKey->setKey(Qt::Key_Pause); connect(pauseKey, &QShortcut::activated, this, &Widget::slotPause); } Widget::~Widget() { delete ui; } void Widget::slotDeleteApple(QGraphicsItem *item) { /* Получив сигнал от Мухи * Перебираем весь список яблок и удаляем найденное яблоко * */ foreach (QGraphicsItem *apple, apples) { if(apple == item){ scene->removeItem(apple); // Удаляем со сцены apples.removeOne(apple); // Удаляем из списка 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); // Добавляем Муху в Список } void Widget::on_pushButton_clicked() { count = 0; ui->lcdNumber->display(count); triangle = new Triangle(); /// Инициализируем муху scene->addItem(triangle); /// Добавляем на сцену треугольник triangle->setPos(0,0); /// Устанавливаем треугольник в центр сцены spider = new Spider(triangle); // Инициализируем паука scene->addItem(spider); // Добавляем паука на сцену spider->setPos(180,180); // Устанавливаем позицию паука /* Подключаем сигнал от паука на проверку состояния GameOver * */ connect(spider, &Spider::signalCheckGameOver, this, &Widget::slotGameOver); /** Инициализируем таймер и вызываем слот обработки сигнала таймера * у Треугольника 100 раз в секунду. * Управляя скоростью отсчётов, соответственно управляем скоростью * изменения состояния графической сцены * */ connect(timer, &QTimer::timeout, triangle, &Triangle::slotGameTimer); timer->start(1000 / 100); /** Раз в секунду отсылаем сигнал на создание яблока в игре * */ connect(timerCreateApple, &QTimer::timeout, this, &Widget::slotCreateApple); timerCreateApple->start(1000); /** Подключаем сигнал от Мухи, в котором передаются Объекты, на которые * наткнулась Муха * */ connect(triangle, &Triangle::signalCheckItem, this, &Widget::slotDeleteApple); ui->pushButton->setEnabled(false); gameState = GAME_STARTED; } void Widget::slotGameOver() { /* Если игра окончена * Отключаем все таймеры * */ timer->stop(); timerCreateApple->stop(); QMessageBox::warning(this, "Game Over", "Мои соболезнования, но Вас только что слопали"); /* Отключаем все сигналы от слотов * */ disconnect(timerCreateApple, &QTimer::timeout, this, &Widget::slotCreateApple); disconnect(triangle, &Triangle::signalCheckItem, this, &Widget::slotDeleteApple); disconnect(spider, &Spider::signalCheckGameOver, this, &Widget::slotGameOver); /* И удаляем все объекты со сцены * */ spider->deleteLater(); triangle->deleteLater(); foreach (QGraphicsItem *apple, apples) { scene->removeItem(apple); // Удаляем со сцены apples.removeOne(apple); // Удаляем из списка delete apple; // Вообще удаляем } /* Активируем кнопку старта игры * */ ui->pushButton->setEnabled(true); gameState = GAME_STOPED; // Устанавливаем состояние игры в GAME_STOPED } void Widget::slotPause() { /* Выполняем проверку на состояние игры, * если игра не запущена, то игнорируем Паузу. * В противном случае или запускаем, или останавливаем все таймеры * */ if(gameState == GAME_STARTED){ if(timer->isActive()){ timer->stop(); timerCreateApple->stop(); spider->pause(); } else { timer->start(1000/100); timerCreateApple->start(1000); spider->pause(); } } }
Итог
По результатам проведенной работы игра перестанет быть бесконечной и ценность набираемых очков возрастёт в разы.
Также в видеоуроке по данной статье даны дополнительные комментарии и продемонстрирована работа игры.
Полный список статей данного цикла:
- Урок 1. Как написать игру на Qt. Управление объектом
- Урок 2. Как написать игру на Qt. Анимация героя игры (2D)
- Урок 3. Как написать игру на Qt. Взаимодействие с другими объектами
- Урок 4. Как написать игру на Qt. Враг - смысл в выживании
- Урок 5. Как написать игру на Qt. Добавляем звук с QMediaPlayer
Здравствуй у меня опять проблема как решить? In member function 'void Widget::on_pushButton_clicked()': ошибка: 'class Ui::Widget' has no member named 'pushButton' ui->pushButton->setEnabled(false); : In member function 'void Widget::slotGameOver()': ошибка: 'class Ui::Widget' has no member named 'pushButton' ui->pushButton->setEnabled(true); скрин http://priscree.ru/img/a9de07f8d2a5a0.jpg ^ ^
вставляю кнопку вообще 10 ошибок не определена ссылка на паука и тд
Добавьте через графический дизайнер кнопку в widget.ui и проследите, чтобы название кнопки было pushButton
добавил кнопку все равно много ошибок вот скрины http://priscree.ru/img/e8e0d04f9e232c.jpg http://priscree.ru/img/3fb040824e8665.jpg
ошибки в vtable... Это moc файлы попортились. Нужно удалить build сборку и пересобрать, и если не поможет, то создать новый проект и переписать в него код. Иначе никак.
хорошо попробую переписать все.как напишу отпишусь)
все заработало спасибо еще вопрос у нас препод просит чтобы мы сделали так.чтобы можно было сохранять результаты это возможно ?
Конечно, возможно. Можно сохранять в файл. Можно сохранять в базу данных. Можно даже в реестр писать с помощью QSettings . Мне больше всего нравится база данных для таких целей. По окончании игры можно добавлять результат в базу данных. Также можно добавить кнопку, которая бы открывала окно с результатами.
а как это сделать ? можете помочь если не сложно конечно и есть время
Значит так. Просто писать код за вас я не буду, но подсказать и направить на нужный путь - это без проблем.
Поэтому -> В предыдущем сообщении я дал ссылку на статью с базой данных. Изучите её. Изучите, как добавлять записи в базу данных. Там же показано, как отобразить записи в таблице. А потом попытайтесь по окончании игры сделать добавление строк в базу данных. В коде есть место, где по окончании игры вызывается диалог. Это происходит в слоте slotGameOver . Вот в нём можно сделать добавление записей в базу данных. Также можете добавить кнопку в окно игры, по нажатию которой можно будет вызвать диалог, в котором как раз и будете показывать рекорды. Если есть затруднения с пониманием сигналов и слотов, то изучите следующую статью .
Здравствуйте,
Подскажите, пжлст, как работает этот код :
Самое непонятное - это 5 -ая строчка, зачем добавлять 90 градусов?
не 90 на 45, ошибся
Там неправильный подсчёт был по направлению и полный бардак был с поведением, эти 45 градусов исправляли ситуацию. Точную причину уже совсем не помню.
А вообще все эти расчёты довольно тяжелы для процессора, я подобрал более лёгкий вариант со скалярными векторами. Но статью ещё пока не недописал к сожалению (она пока в разработке).
Здравствуйте! Почему может быть такая ошибка: враг висит в углу, колеблется в нём, но не двигается? По логике код такой же, но вместо орисовки моделей использую спрайты. Со спрайтом "мухи" всё хорошо, а вот "паук" отказывается вести себя нормально. Подозреваю, что ошибка может быть здесь (это код, который идёт вместо отрисовки):
Или же ошибка где-то в управлении "паука", но там код вообще неизменный. (в Spider.cpp, void Spider...)
Добрый день.
Дело тут явно не в методе paint, скорее всего всё-таки есть какая-то ошибка в другом месте. Обычно такое может быть в районе метода slotGamerTimer
Спасибо! На самом деле ошибка оказалась от невнимательности - просто пропустила подключение движения в сторону модели. Теперь всё работает.
Если вам не сложно, могли бы вы, пожалуйста, объяснить, за что отвечают параметры в скобках тут: setPos(mapToParent(0, -(qrand() % ((2 + 1) - 1) + 1)))?
Нашла только такое, не много о чём говорит: (qrand() % ((high + 1) - low) + low). Экспериментально кое-что выяснила, но хотелось бы знать точно)
А ещё вот тут, когда генерировали бананы: banana->setPos((qrand() % (251)) * ((qrand()%2 == 1)?1:-1),
(qrand() % (251)) * ((qrand()%2 == 1)?1:-1));
А так, благодарю за указку на slotGameTimer!
П.С.: я не очень умею пока читать документацию( поэтому прошу помощи у вас, чтобы разобраться с этими параметрами