Создание 2D тайловой карты на QML. Часть 1
Первая мысль, которая меня посетила: «а что, собственно, в этом сложного?». Ну, вроде, ничего: • создаешь массив текстур, • указываешь размер карты, • пробегаешься циклом по массиву, создавая объекты. Именно так я и поступил с самого начала…
Небольшое отступлениеВдаваться в подробности того что из себя представляют тайлы мне не хочется, да и статья немного не об этом. Предполагается, что читатель уже имеет некоторое представление о том, что такое изометрия в играх, что такое тайлы, что они из себя представляют и как рисуются. Напомню лишь о том, что элементарный изометрический тайл создается в соотношении 2 к 1, т. е. если ширина тайла составляет 2 единицы, то его высота должна составить 1 единицу. Хочу отметить, что в моем проекте будут использоваться псевдо-3D тайлы, у которых размеры составляют 1 к 1. Выглядят они так:
но использоваться будет только половина от этого «кубика» (выделена красным). Пока что применения отсеченной нижней части я не придумал, но скорее всего в будущем она будет задействована для гор, углублений или банальных обрывов карт. Тогда скорее всего придется задействовать z-индекс… но это уже другая история
п.с. в конце статьи имеется исходник проекта
Первые шагиТак выглядел код в самом начале моего пути:
Вот и всё. Приложив минимум усилий получилось создать вот такую симпатичную карту:
Расписывать содержимое Tile.qml, я не стану, потому что в дальнейшем этот компонент нам вообще не понадобится. А всё потому, что делать так совершенно не стоит! Поясню: рисуя карту с размерами 4х12 (mapcols * maprows) было создано 48 объектов. Но такое игровое поле очевидно является слишком маленьким. Если же нарисовать поле побольше, например, шириной в 20 тайлов, то его высота составит 60 тайлов, а это — 1200 визуальных объектов! Не сложно представить сколько памяти будет задействовано для хранения такого количества объектов. Одним словом — много.
РазмышленияДолго думать нам новым методом создания карты не пришлось. Первым делом были обозначены основные параметры карты, которые должны быть достигнуты в новом методе: 1. карта должна быть подвижной (игрок может скроллить карту в любом направлении); 2. объекты, расположенные за пределами окна не должны отрисовываться; 3. метод должен быть максимально прост в реализации %)
Первую хотелку очень легко реализовать при помощи элемента Flickable. А почему бы и нет? Не нужно будет заморачиваться со скролами, ловлей событий и… в общем заморачиваться не придется вообще, что вполне удовлетворяет третьему пункту :-) элемент будет назван map_area — область_карты.
Чтобы дать Flickable возможность двигать карту, необходимо создать во флике элемент, с размерами равными полному размеру карты в пикселях. Для этого подойдет обычный Item — этот элемент не визуальный, благодаря чему его размеры не влияют на количество потребленной памяти. Он и будет носить ключевое имя map — карта.
Для отрисовки текстур необходимо использовать дополнительный элемент, который должен располагаться внутри элемента map. При этом его размер должен соответствовать размерам map_area, а чтобы этот элемент всегда находился «на виду», его необходимо перемещать в сторону противоположную скроллу карты. Т.е. если пользователь двигает карту влево, этот элемент должен перемещаться вправо и перерисовываться. Для реализации этой идеи могла бы подойти связка Image с QQuickImageProvider, но их возможности довольно скудны, поэтому придется создать собственный компонент, прибегнув к темной стороне — C++. Будущий элемент будет наследником QQuickPaintedItem и ему будет присвоено имя MapProvider.
От простого к… простомуВ моем представлении это выглядело как-то так:
В коде это выглядит так:
Именно этот код будет скелетом для дальнейшей работы. Следующим шагом будет создание элемента MapProvider. Для этого в проекте создаем новый C++ класс:
Сразу же регистрируем этот элемент в QML, для этого правим main.cpp. Его содержимое должно быть примерно таким:
После сохранения изменений, этот элемент можно задействовать в QML.
Для этого в main.qml добавляем импорт модуля:
и заменяем строку
Для того, чтобы наглядно показать как будет работать метод, я создал 2 дополнительных элемента на форме: внутри окна обозначил специальную область game_area, в которую переместил элемент map_area. Размер игровой области я намеренно сделал меньше размера формы, а чтобы отобразить границы этой области создал обычный Rectangle:
Мокрые расчеты — раздел, в котором много водыМы почти приблизились к отрисовке карты, но имеются некоторые нюансы, на которые стоит обратить внимание. И первый кандидат к рассмотрению — края карты. У нас они получаются «зубастыми». Это можно было наблюдать в прошлом проекте, но в новом от этого нужно избавиться. Чтобы спрятать с глаз долой зубастость слева и сверху, достаточно сместить карту (Item: map) влево и вверх на половину ширины и высоты тайла:
Чтобы спрятать зубастость справа и снизу, нужно просто ограничить скроллинг путем изменения параметров contentWidth и contentHeight. Тут необходимо учесть тот факт, что саму карту мы уже сместили влево и вверх на полразмера, значит размер контента необходимо уменьшить на полный размер тайла:
Реализация перемещения элемента MapProvider при скроллинге выглядит так:
жутковато :) сейчас поясню что же тут происходит.
По сути, наша карта состоит из прямоугольных блоков, в которые вписаны ромбовидные тайлы. Благодаря этому отпадает необходимость в перерисовке видимой области карты при малейшем скролле, можно просто выделить «защитную зону» (не придумал подходящего названия) за пределами видимой области, которая тоже будет отрисовываться вместе со всей картой, а перерисовывать всю карту нужно будет только тогда, когда скроллинг превысит размер этой зоны. Благодаря этому, количество необходимых перерисовок карты уменьшится в сотни раз (в зависимости от размеров тайла). В данном коде эта «защитная зона» рассчитывается путём прибавления к ширине и высоте MapProvider удвоенного размера тайла. Таким образом мы расширим отрисовываемую область вправо и вниз ровно на 2 тайла. Чтобы половину этой области распространить вверх и влево, необходимо подправить размеры контента у map_area и размеры карты map:
Формула расчета X и Y элемента MapProvider обеспечивает ему скачкообразное перемещение только тогда, когда скроллинг выходит за пределы «защитной зоны». В дальнейшем к этим скачкам будет привязано событие перерисовки карты.
Ближе к телуИтак, с расчетами на стороне QML покончено, теперь необходимо определится с набором дополнительных параметров, которые будут необходимы для правильной отрисовки «тела» элемента MapProvider: 1. Фактическое положение контента в map_area — понадобится для расчета номеров колонок и строк, с которых начинается отрисовка карты (отрисовка начинается сверху слева, значит мы найдем индекс верхнего левого тайла). Этим параметрам я дал имена cx и cy. 2. Размеры тайлов — необходимы для отрисовки картинок. 3. Размеры карты — понадобится для расчета реального индекса тайла. 4. Собственно, само описание текстур карты. У меня это обычный одномерный массив с наименованием ресурсов.
п.с. здесь «0004» — это имя ресурса картинки без расширения.
Разумеется, все эти параметры необходимо объявить на стороне C++, все это делается при помощи макроса Q_PROPERTY:
Мощь QtCreator'a позволит без труда и без запинки создать все эти параметры парой кликов (для тех, кто не в курсе: вызываем контекстное меню на каждой строке Q_PROPERTY -> Refactor -> Generate Missing Q_PROPERTY Members. )
ФиналНаконец, мы добрались до реализации метода paint. На самом деле он не сильно отличается от функции createMap() из предыдущего проекта, за исключением того, что в него добавлено кеширование картинок:
Кеширование необходимо для того чтобы каждый раз не перерисовывать картинку, а перерисовывается она из-за того, что размеры исходной картинки намного больше размеров тайла (это сделано для реализации масштабирования в будущем). Перерисовка съедает много ресурсов, особенно из-за того что при изменении картинки используется сглаживание Qt::SmoothTransformation. К слову, теоретически масштабирование можно реализовать и сейчас, достаточно лишь добавить фактор увеличения для параметра root.tilesize
Переменная tileCache объявляется в классе MapProvider:
И последний штрих — это добавление события перерисовки карты путем создания пары коннектов:
РелизНу вот и все, теперь можно запустить проект и увидеть такую картинку:
которая не сильно-то и отличается от картинки в первом проекте, но является менее прожорливой.
Для того чтобы увидеть как рисуется карта в движении, нужно увеличить значение переменной root.mapcols, установив его, например, в значение 8 (это значение умноженное на root.maprows соответствует количеству элементов в переменной mapprovider.mapdata, для больших значений будет необходимо добавить элементы).
Для того чтобы спрятать «защитную зону» за кулисы, оставив видимой только полезную часть карты, достаточно изменить параметр gamearea.clip с false на true
В следующей части будет описан процесс создания редактора карты на основе текущего проекта. Редактор должен будет уметь сохранять карту и загружать ее.