Спрайтовая анимация на QGraphicsScene Qt

Спрайтовая анимация на QGraphicsScene Qt

Несколько лет назад я работал в фирме, которая разрабатывала компьютерные игры на своем собственном игровом движке. Помимо прочего в движке была возможность проигрывания спрайтовой (покадровой) анимации. Уволившись с той конторы я написал свой собственный аналог. Ниже приведен пример изображения, содержащего такую анимацию. Программист указывает размер кадра и их количество (иногда больше информации), программа разделяет изображение на кадры и переключает их с нужной частотой:

Значительно позже я написал свою первую игрушку (описание разработки игры под Andoird), при этом я сначала использовал спрайтовую анимацию, но затем подумал, что предпочтительнее применять стандартные компоненты — я переписал код с использованием QMovie . По приведенной выше ссылки вы можете прочитать как я всем этим пользовался. Однако, после публикации игры на google market я получил несколько негативных отзывов — люди писали, что на большом планшете качество картинки отвратительное.

Виноват был QMovie , ну и я конечно — ведь я преобразовал анимации в формат .gif , при этом обязательно выполняется сжатие с потерями качества. Я, конечно, пробовал выбрать другой формат — QMovie поддерживал также формат .mng (насколько я понимаю, это как раз то, что нужно — набор кадров с таким же сжатием, как .png ). Однако проблемы была как с конвертацией картинок в этот формат, так и с проигрыванием анимации (на телефоне у меня показывался только первый кадр).

Есть и другие причины не использовать QMovie на графической сцене. Для его отображения нужно использовать QLabel , который является наследником класса QWidget , но при размещении виджетов на сцене значительно падает производительность.

В связи с этим, я быстро переписал игрушку с использованием .png-анимации. Полученное решение Вы можете увидеть в репозитории игры, однако, я долго не описывал этот вариант, т.к. он не был удобен для повторного использования. Сейчас я переписал его еще раз, и теперь мне кажется, что мои классы можно использовать в других проектах без особых изменений.

Кадры анимации надо быстро переключать. В игре у нас может для нескольких элементов одновременно проигрываться одна анимация — например если вы создадите 10 одинаковых солдат. Один солдат при этом может начинать шаг, а другой заканчивать его, т.е. они могут проигрывать одну и туже анимацию, но находиться на разном кадре. Было бы здорово загрузить анимацию ( .png файл) в единственном экземпляре так, чтобы все элементы игры могли ей пользоваться. При этом каждый элемент должен знать лишь какая анимация ему нужна и номер кадра. Чтобы удовлетворить это требование кадры анимации лучше всего поместить в обычный массив (у него самая низкая трудоемкость произвольного доступа, т.е. операции извлечения элемента по индексу).

В программе может быть множество анимаций, чтобы не запутаться в них можно применить шаблон проектирования Объектный пул. Пул — это объект, владеющий другими объектами и предоставляющий доступ к ним по идентификатору. В нашем случае внутри пула будут находиться анимации (массивы с кадрами), а идентификатором будет являться имя анимации. Я организовал пул как словарь ( QMap ) и использовал шаблоны чтобы разделить анимации разных типов — например, в вашей игре анимации персонажей могут находиться в одном пуле, а анимации деревьев — в другом:

Обратите внимание на использование шаблона проектирования Singleton, он используется тут потому, что доступ к пулу требуется в различных частях программы (нужна глобальная точка доступа), а также, пул должен (соответствующего типа шаблона) существовать в одном экземпляре.

Для отображения наших элементов на графической сцене нам нужно создать наследника класса QGraphicsItem , но нам ведь нужно отображать кадр анимации ( QPixmap ), а для этого есть стандартный класс QGraphicsPixmapItem — именно он будет базовым. Кроме того, чтобы использовать механизм сигналов и слотов наш класс наследует QObject :

Анимированный элемент графической сцены ( AnimatedGraphicsItem ) имеет два режима воспроизведения анимации — разовый и циклический. В разовом анимация проигрывается лишь один раз, затем элемент скрывается (вызов hide() , см. ниже) и вырабатывается сигнал animationFinished() . Воспроизводить кадры можно начиная с разного номера кадра, по умолчанию с нулевого, но можно задать случайный начальный кадр (это нужно, например если у вас в игре почти одновременно могут быть созданы несколько юнитов — очень некрасиво смотрится если такие юниты идут синхронно). Кроме того, в конструкторе класса можно задать частоту смены кадров.

Кадры меняются по событиям таймера (именно его настраиваем на определенную частоту), обработка которых выполняется в слоте on_timerTick() . Внутри класса хранится указатель на массив кадров — сами кадры создаются лишь один раз внутри AnimationPool .

Наследуя класс AnimatedGraphicsItem мы получаем возможность воспроизводить внутри наших элементов .png-анимацию. В качестве примера создадим элемент, управляемый кнопками клавиатуры — мы можем попросить его двигаться в любую из четырех сторон. Во время движения элемент плавно перемещается за счет использования QGraphicsItemAnimation и проигрывает разные наборы спрайтов (на которых видно, что существо бежит влево или вправо).

Чтобы сделать такой пример, я сначала создал перечисление со всеми типами анимации (в файле animations_enum.h ) для пула и макрос, сокращающий обращение к пулу:

Чтобы работать с этим набором анимации в любой точке программы, достаточно подключить этот файл и обратиться к ACTOR_ANIMATION_POOL , но перед этим в пул надо добавить кадры — делаем это в функции main() :

Класс View тут это наследник QGraphicsView , для которого перегружена функция обработки событий клавиатуры:

Класс View лишь получает события клавиатуры, он не должен их обрабатывать, т.к. это обязанность конкретных классов:

Класс GraphicsActor получает направление движения, соответствующее нажатой клавиши и обрабатывает его включая нужную спрайтовую анимацию и перемещаясь в указанном направлении:

В конструкторе задается начальное действия персонажа, а также параметры (я использую setScale() , т.к. анимация в .png файле у меня достаточно большая):

Метод setSprites() устанавливает проигрываемый набор кадров, при этом можно потребовать немедленной установки (нужно в конструкторе), без которой анимация будет изменяться только если новая анимация отличается от той, что уже проигрывается:

Анимация перемещения будет проигрываться AnimationPeriodMS миллисекунд, после чего в слоте onAnimationFinished() нужно будет запустить ее заново:

Основная работа выполняется в функции обработки нажатия клавиши:

Тут объекту устанавливается набор спрайтов и запускается анимация движения .

Полный исходный код программы вы можете взять в репозитории (на мой взгляд, он вполне пригоден для повторного использования).

Собрал приведенный пример на qt5.5. Анимация движения отыгрывает рывками. Ощущение, что персонаж осуществляет перемещение на величину SpeedPx значительно быстрее, чем положено по AnimationPeriodMS. Остальное время он стоит в финальной точке. Анимация фреймов отыгрывает корректно.

Ранее не было обнаружено таких проблем. Анимация перемещения — это тема предыдущей статьи: https://pro-prof.com/forums/topic/qt-qgraphicsitemanimation-example. Тут и там используется QGraphicsItemAnimation , но вот пару дней назад мне ребята подсказали, что этот класс в новой версии Qt помечен как deprecated. В ближайшее время я посмотрю в чем может быть дело и, вероятно, опишу новую статью без deprecated класса.

Взял код с репозиотрия, собрал с Qt 5.8 — перемещается плавно (описанных вами проблем нет). Уточните, вы создавали проект сами и вставляли в него код из статьи или взяли готовый проект из репозитория (ссылка в конце статьи)?

📎📎📎📎📎📎📎📎📎📎