- 1. Анимация Мухи по шагам
- 2. triangle.h
- 3. triangle.cpp
- 4. widget.cpp
- 5. Итог
- 6. Видеоурок
Во втором уроке подойдём к следующему аспекту написания игры - Анимация героя игры. В прошлом уроке было рассмотрено, как управлять объектом на графической сцене, и главным героем выступал красный треугольник, но это не очень интересно. Поэтому превратим треугольник в одушевленный объект, а именно в Муху , которая будет ползать под управлением клавиатуры и двигать ножками при движении.
В данном уроке корректировка программного кода производилась в главным образом в файлах triangle.h, triangle.cpp и самую малость в файле widget.cpp.
Анимация Мухи по шагам
Опишем алгоритм, по которому будет производиться отрисовка Мухи:
- Инициализируем в конструкторе класса переменную, которая будет отвечать за номер положения ножек. Всего будет три положения ножек мухи.
- Инициализируем в конструкторе класса переменную, которая будет считать полезные тики игрового таймера, то те тики, во время которых Муха передвигалась. Это необходимо, чтобы отсеять те тики, в которых не было движения Мухи , иначе Муха будет постоянно перебирать ножками, даже если мы не будем управлять Мухой .
- Отрисовываем Муху в методе paint, выбирая, какое из положений ножек мухи будет отрисовываться.
- В слоте, который обрабатывает событие отсчета игрового таймера производит накопление и обнуление счетчика полезных тиков игрового таймера и в зависимости от его величины устанавлвиваем положение ножек Мухи , а именно полностью перерисовываем Муху с нужным положением ножек.
triangle.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 в тот момент времени, когда это понадобится.