У другому уроці підійдемо до наступного аспекту написання гри - Анімація героя игры. Головним героєм виступав червоний трикутник, але це дуже цікаво. Тому перетворимо трикутник на одушевлений об'єкт, а саме в Муху** , яка повзатиме під керуванням клавіатури і рухатиме ніжками під час руху.
У цьому уроці коригування програмного коду проводилося в основному у файлах triangle.h, triangle.cpp і зовсім небагато у файлі widget.cpp.
Анімація Мухи за кроками
Опишемо алгоритм, за яким проводитиметься малювання Мухи:
- Ініціалізуємо у конструкторі класу змінну, яка відповідатиме за номер положення ніжок. Усього буде три положення ніжок мухи.
- Ініціалізуємо в конструкторі класу змінну, яка вважатиме корисні тики ігрового таймера, то ті тики, під час яких Муха пересувалася. Це необхідно, щоб відсіяти тітики, в яких не було руху Мухи , інакше Муха постійно перебиратиме ніжками, навіть якщо ми не будемо керувати Мухою .
- Малюємо Муху у методі paint, вибираючи, яке з положень ніжок мухи малюватиметься.
- У слоті, який обробляє подію відліку ігрового таймера виробляє накопичення та обнулення лічильника корисних тиків ігрового таймера і залежно від його величини встановлюємо положення ніжок Мухи , а саме повністю перемальовуємо Муху з потрібним положенням ніжок.
трикутник.h
У цей файл додаємо лише дві цілі перемінні:
- steps - номер положення ніжок мухи;
- countForSteps - лічильник для відліку тиків таймера, при яких проводилося натискання клавіш клавіатури.
#ifndef TRIANGLE_H #define TRIANGLE_H #include <QObject> #include <QGraphicsItem> #include <QPainter> #include <QGraphicsScene> /* Подключаем библиотеку, отвечающую за использование WinAPI * Данная библиотека необходима для асинхронной проверки состояния клавиш * */ #include <windows.h> class Triangle : public QObject, public QGraphicsItem { Q_OBJECT public: explicit Triangle(QObject *parent = 0); ~Triangle(); signals: public slots: void slotGameTimer(); /// Слот, который отвечает за обработку перемещения треугольника protected: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); private: qreal angle; /// Угол поворота графического объекта int steps; // Номер положения ножек мухи int countForSteps; // Счётчик для отсчета тиков таймера, при которых мы нажимали на кнопки }; #endif // TRIANGLE_H
triangle.cpp
Загалом зміни торкнулися повністю файлу. У конструкторі класу надаються значення двом новим змінним. А в методі paint повністю відмальовується вся Муха з трьома можливими положеннями ніжок. Для малювання застосовуються такі об'єкти, як еліпси, лінії та об'єкти класу QPainterPath.
А для того, щоб Муха вийшла з адекватним зовнішнім виглядом, був зроблений нарис Мухи, який за координатами був перенесений у програмний код уроку.
З найцікавіше присутній у методі slotGameTimer(), у якому при відстеження стану кнопок ведеться відлік корисних тиків, і у випадку, якщо змінна countForSteps дорівнює 4, 8, 12 та 16, вибирається одне з положень ніжок Мухи та ініціюється перемальовка Мухи з цим положенням ніжок.
#include "triangle.h" Triangle::Triangle(QObject *parent) : QObject(parent), QGraphicsItem() { angle = 0; // Задаём угол поворота графического объекта steps = 1; // Задаём стартовое положение ножек мухи countForSteps = 0; // Счётчик для отсчета тиков таймера, при которых мы нажимали на кнопки setRotation(angle); // Устанавливаем угол поворота графического объекта } Triangle::~Triangle() { } QRectF Triangle::boundingRect() const { return QRectF(-40,-50,80,100); /// Ограничиваем область, в которой лежит треугольник } void Triangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // Рисуем ножки, без ножек же муха не сможет ползать painter->setPen(QPen(Qt::black, 2)); if(steps == 0){ // Первое положение ножек // Left 1 painter->drawLine(-24,-37,-22,-25); painter->drawLine(-22,-25,-17,-15); painter->drawLine(-17,-15,-10,-5); // Right 1 painter->drawLine(37,-28,28,-18); painter->drawLine(28,-18,24,-8); painter->drawLine(24,-8,10,-5); // Left 2 painter->drawLine(-35,-20,-25,-11); painter->drawLine(-25,-11,-14,-5); painter->drawLine(-14,-5,0,5); // Right 2 painter->drawLine(37,-12,32,-4); painter->drawLine(32,-4,24,2); painter->drawLine(24,2,0,5); // Left 3 painter->drawLine(-35,35,-26,24); painter->drawLine(-26,24,-16,16); painter->drawLine(-16,16,0,0); // Right 3 painter->drawLine(37,26,32,17); painter->drawLine(32,17,24,8); painter->drawLine(24,8,0,0); } else if (steps == 1){ // Второе положение ножек // Left 1 painter->drawLine(-32,-32,-25,-22); painter->drawLine(-25,-22,-20,-12); painter->drawLine(-20,-12,-10,-5); // Right 1 painter->drawLine(32,-32,25,-22); painter->drawLine(25,-22,20,-12); painter->drawLine(20,-12,10,-5); // Left 2 painter->drawLine(-39,-15,-30,-8); painter->drawLine(-30,-8,-18,-2); painter->drawLine(-18,-2,0,5); // Right 2 painter->drawLine(39,-15,30,-8); painter->drawLine(30,-8,18,-2); painter->drawLine(18,-2,0,5); // Left 3 painter->drawLine(-39,30,-30,20); painter->drawLine(-30,20,-20,12); painter->drawLine(-20,12,0,0); // Right 3 painter->drawLine(39,30,30,20); painter->drawLine(30,20,20,12); painter->drawLine(20,12,0,0); } else if (steps == 2){ // Третье положение ножек // Left 1 painter->drawLine(-37,-28,-28,-18); painter->drawLine(-28,-18,-24,-8); painter->drawLine(-24,-8,-10,-5); // Right 1 painter->drawLine(24,-37,22,-25); painter->drawLine(22,-25,17,-15); painter->drawLine(17,-15,10,-5); // Left 2 painter->drawLine(-37,-12,-32,-4); painter->drawLine(-32,-4,-24,2); painter->drawLine(-24,2,0,5); // Right 2 painter->drawLine(35,-20,25,-11); painter->drawLine(25,-11,14,-5); painter->drawLine(14,-5,0,5); // Left 3 painter->drawLine(-37,26,-32,17); painter->drawLine(-32,17,-24,8); painter->drawLine(-24,8,0,0); // Right 3 painter->drawLine(35,35,26,24); painter->drawLine(26,24,16,16); painter->drawLine(16,16,0,0); } // Усики QPainterPath path(QPointF(-5,-34)); path.cubicTo(-5,-34, 0,-36,0,-30); path.cubicTo(0,-30, 0,-36,5,-34); painter->setBrush(Qt::NoBrush); painter->drawPath(path); painter->setPen(QPen(Qt::black, 1)); // Тушка painter->setBrush(Qt::black); painter->drawEllipse(-15, -20, 30, 50); // Голова painter->drawEllipse(-15, -30, 30, 20); // Глазища painter->setBrush(Qt::green); painter->drawEllipse(-15, -27, 12, 15); painter->drawEllipse(3, -27, 12, 15); // Левое крылище QPainterPath path2(QPointF(-10, -10)); path2.cubicTo(-18, -10, -30, 10, -25, 35); path2.cubicTo(-25,35,-20,50,-15,40); path2.cubicTo(-15,40,0,20,-3,5 ); path2.cubicTo(-3,5, -8,8,-10,-10); painter->setBrush(Qt::white); painter->drawPath(path2); // Правое крылище QPainterPath path3(QPointF(10, -10)); path3.cubicTo(18, -10, 30, 10, 25, 35); path3.cubicTo(25,35,20,50,15,40); path3.cubicTo(15,40,0,20,3,5 ); path3.cubicTo(3,5, 8,8,10,-10); painter->setBrush(Qt::white); painter->drawPath(path3); Q_UNUSED(option); Q_UNUSED(widget); } void Triangle::slotGameTimer() { /* Проверяем, нажата ли была какая-либо из кнопок управления объектом. * Прежде чем считать шажки * */ if(GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState(VK_UP) || GetAsyncKeyState(VK_DOWN)) { /* Поочерёдно выполняем проверку на нажатие клавиш * с помощью функции асинхронного получения состояния клавиш, * которая предоставляется WinAPI * */ if(GetAsyncKeyState(VK_LEFT)){ angle -= 5; // Задаём поворот на 5 градусов влево setRotation(angle); // Поворачиваем объект } if(GetAsyncKeyState(VK_RIGHT)){ angle += 5; // Задаём поворот на 5 градусов вправо setRotation(angle); // Поворачиваем объект } if(GetAsyncKeyState(VK_UP)){ setPos(mapToParent(0, -2)); /* Продвигаем объект на 5 пискселей вперёд * перетранслировав их в координатную систему * графической сцены * */ } if(GetAsyncKeyState(VK_DOWN)){ setPos(mapToParent(0, 2)); /* Продвигаем объект на 5 пискселей назад * перетранслировав их в координатную систему * графической сцены * */ } // Двигаем ножками, Dance, dance, Baby !!! countForSteps++; if(countForSteps == 4){ steps = 2; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 8){ steps = 1; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 12){ steps = 0; update(QRectF(-40,-50,80,100)); } else if (countForSteps == 16) { steps = 1; update(QRectF(-40,-50,80,100)); countForSteps = 0; } } /* Проверка выхода за границы поля * Если объект выходит за заданные границы, то возвращаем его назад * */ 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); // снизу } }
widget.cpp
Для поліпшення плавності відмальовування було збільшено частоту спрацювання сигналу від таймера.
timer->start(1000 / 100);
Підсумок
В результаті у Вас має вийти Муха, яка при русі перебиратиме лапками. Також демонстрацію результату даного уроку Ви можете переглянути у відеоуроці за цією статтею.
Повний список статей цього циклу:
- Урок 1. Як написати гру на Qt. Управління об'єктом
- Урок 2. Як написати гру на Qt. Анімація героя ігри (2D)
- Урок 3. Як написати гру на Qt. Взаємодія з іншими об'єктами
- Урок 4. Як написати гру на Qt. Ворог - сенс у виживанні
- Урок 5. Як написати гру на Qt. Додаємо звук із QMediaPlayer
Здравствуйте,
Подскажите, почему муха может оставлять следы на игровой сцене?
Добрый день. Артефакты обычно остаются в том случае, если не обновляется полностью тот участок графической сцены, в котором находится муха. Скорее возможно boundingRect() написали не совсем правильно, если писали его самостоятельно для своего собственного варианта. Также как вариант можете запускать метод update() на графической сцене с указание того пространства, которое требуется перерисовать.
Здравствуйте, а можно, пожалуйста, ссылку на целые исходники, если есть?
Добрый день. В конце пятой статьи скачать можете.
Евгений, здравствуйте! Подскажите, а почему при нажатии одной клавиши переменная countForSteps увеличивается не на 1, на 4, ведь одно действие даёт увеличение этой переменной только на единицу? (стр.184 в файле triangle.cpp)
Добрый день. slotGameTimer срабатывает по таймеру и при каждой сработке countForSteps увеличивается на 1, это не зависит от нажатия клавиш, нажатая клавиша лишь определяет положение ножек, которое отрисовывается у игрового персонажа. В коде есть часть, которая отвечает за отрисовку и эта часть делает проверку на число, кратное 4, поэтому вам и показалось, что countForSteps увеличивается на 4, а на самом деле во время нажатя клавиши слот успевает вызываться 4 раза.
Евгений, благодарю! Всё равно не совсем понимаю :( Если муха двигает ножками только при нажатии клавиш перемещение, то что, собственно, делает код со строк 184-198 в triangle.cpp? В этих строчках код опеределяет "характер" движения ножек в зависимости от направления движения мухи. Но я не очень понимаю как эта часть кода связана с разными нажатиями кнопок игроком, которые, собственно, и задают "характер" движения ножек. Ведь, как я понял, таймер работает не только во время нажатия клавиш, так? Плюс, не понимаю, почему слот за одно движение срабатывает 4 раза... Прошу прощение, немного запутался :(
Код на строчка 184-198 вызывает перерисовку области на каждый 4-й такт счётчика. По той логике не нужно перерисовывать объект постоянно, достаточно реже, чем выполняется игровой слот. А слот выполняется постоянно по таймеру. Потому, что как вы правильно поняли, таймер работает и тогда, когда клавиши не нажимаются. А сработка 4 раза или даже чаще - это потому что нажатие обычно дольше, чем длительность отсчёта таймера. Слот срабатывает по таймеру, таймер срабатывает постоянно. Почему именно 4 раза? - Потому, что это эмпирически подобранная величина.
Евгений, благодарю!
Евгейни, скажите: а почему вы не использовали в этом проекте класс событий нажатия клавиш QKeyPressEvent? Это менее целесообразно в данном случае было?
Да, менее целесообразно. Не подходит, поскольку не даёт обрабатывать постоянное нажатие кнопки достаточно простым способом.
Во всяком случае это первая причина, которая мне вспоминается. Я уже почти ничего не помню с момента написания этого поста, касательно этого примера.
Евгений, я новичок в Qt и решил попрактиковать в создании какой-нибудь простенькой игры (змейка, тетрис, морской бой), где присутствует постоянное нажатие клавиш. На ваш опытный взгляд, можно ли сказать, что в таких и похожих ситуациях лучше не влезать в класс обработки событий клавиатуры из-за его насыщенности, а просто прибегнуть к подключению возможностей Win32 как вы сделали в этой игре?
События нажатий кнопок в Qt одноразовые. То есть пришло событие и вам нужно куда-то сохранить информацию об этом.
А лично на мой взгляд гораздо проще проверять нажата ли кнопка через Win Api в тот момент времени, когда это понадобится.