Главная » Программирование игр под Android » ФРАГМЕНТЫ ТЕКСТУР, СПРАЙТЫ И ПАКЕТЫ: СКРЫВАЕМ OPENGL ES – РАЗРАБОТКА ИГР ДЛЯ ОС ANDROID

0

 

До настоящего времени наш код для примера с пушкой включал в себя шаблоны, без некоторых из них можно обойтись. В частности, это касается определения экземпляров класса Vertices. Утомительно постоянно писать семь строк кода только для того, чтобы определить один текстурированный прямоугольник. Еще одна область, в которой можно поработать, – ручное вычисление текстурных координат для изображений в атласах текстур. И наконец, очень много лишнего и повторяющегося кода употребляется для отображения наших 20-прямоугольников. Я также упоминал об удобном способе отображения нескольких объектов вместо выполнения отрисовки для каждого объекта в отдельности. Мы можем решить все эти проблемы, для чего применим несколько новых концепций.

Фрагменты текстур. Мы уже работали с фрагментами текстур в последнем примере. Фрагмент текстуры (texture region) – это прямоугольная область с одной текстурой (например, область в нашем атласе, содержащая пушку). Мы хотим, чтобы у нас был класс, инкапсулирующий все неудобные подсчеты преобразования пиксельных координат в текстурные.

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

Это четкое разделение между графикой и кодом модели нужно для красоты конструкции.

Сортировщики (бэтчеры) спрайтов. Сортировщик спрайтов отвечает за одновременное отображение нескольких спрайтов. Для этого бэтчер спрайтов должен знать позицию, размер и фрагмент текстуры для каждого спрайта. Именно при помощи бэтчера спрайтов мы волшебным образом избавимся от многочисленных вызовов отрисовки и матричных операций для одного объекта.

Эти концепции тесно взаимосвязаны; переходим к их обсуждению.

Класс TextureRegion

Поскольку мы уже работали с фрагментами текстур, будет несложно выяснить, что нам нужно. Мы знаем, как преобразовывать пиксельные координаты в текстурные. Мы хотим создать класс, где мы будем определять пиксельные координаты изображения в атласе текстур и который будет хранить текстурные координаты для фрагмента атласа для дальнейших операций (например, когда мы будем отображать спрайт). В листинге 8.16 показан класс TextureRegion.

Листинг 8.16. TextureRegion.Java: преобразование пиксельных координат в текстурные координаты

Класс TextureRegion хранит текстурные координаты верхнего левого угла (ul; vl) и нижнего правого угла (u2; v2) фрагмента в текстурных координатах. Конструктор принимает Texture и верхний левый угол, а также ширину и длину фрагмента в пиксельных координатах. Чтобы построить фрагмент текстуры для Cannon, нам надо сделать следующее:

Таким же образом мы можем построить фрагмент для Боба:

Ну и т. д. Мы можем использовать это в коде примера, который мы создали, и применить члены класса ul, vl, u2, v2 для определения текстурных координат вершин наших прямоугольников. Но мы не сделаем этого, пока не избавимся от всех громоздких определений. Вот для чего мы будем использовать бэтчеры спрайтов.

Класс SpriteBatcher

Как мы уже говорили, спрайт легко определяется его позицией, размером и фрагментом текстуры (а также опциональными параметрами: ориентацией и масштабом). Это просто графический прямоугольник в пространстве нашего мира. Для простоты будем придерживаться обозначений позиции как центра спрайта и прямоугольника, построенного вокруг этого центра. Теперь мы можем получить класс Sprite и применить его таким образом:

Код будет строить новый спрайт с центром в точке (20; 20) в мире с расстоянием 0,25 м от центра в каждую сторону, используя bobRegion TextureRegion. Но вместо этого мы можем сделать так:

Так уже намного лучше. Нам уже не нужно строить другой объект для представления графической стороны нашего объекта. Вместо этого мы изображаем копию Боба по требованию. Мы можем также использовать такой перегруженный метод:

Он будет рисовать пушку, вращающуюся под углом. Как же нам реализовать бэтчер спрайтов? И где представители класса Vertices?. Давайте подумаем, как могут работать бэтчеры.

Что вообще означает понятие пакетная обработка, или бэтчинг? В мире графики под ним обычно понимают соединение нескольких вызовов отрисовки в один. Как уже говорилось выше, наш графический процессор только за. Вот как работает сортировщик спрайтов:

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

Каждый раз, когда мы вызываем метод SpriteBatcher drawSprite, мы добавляем четыре вершины в буфер, исходя из положения, размера, ориентации и фрагмента текстуры, которые задаются в качестве аргументов. Это также означает, что мы должны будем вручную менять и переводить координаты вершин без помощи OpenGL ES. Однако в этом нет ничего страшного, потому что тут нам на помощь придет код из нашего класса Vector2. Это ключ к устранению всех вызовов отрисовки.

Когда мы определили все спрайты, которые хотим отобразить, даем указание нашему бэтчеру спрайтов загрузить вершины всех прямоугольников спрайтов в графический процессор за один раз, а затем вызываем сам метод рисования OpenGL ES для изображения всех прямоугольников. Для этого мы преобразуем содержимое массива с переменными float в экземпляр класса Vertices и используем его для отображения прямоугольников.

ПРИМЕЧАНИЕ

Мы можем использовать только спрайты, находящиеся в одной и той же текстуре. Однако это не большая проблема, поскольку мы будем применять атласы текстур.

Обычная схема использования бэтчера спрайтов выглядит так:

Вызов Spri teBatcher. begi nBatch  позволяет сообщить сортировщику две вещи: он должен очистить буфер и использовать текстуру, которую мы ему передали. Для удобства свяжем текстуру с этим методом.

Далее отображаем столько спрайтов, относящихся к фрагментам в данной текстуре, сколько нам нужно. Это заполнит буфер, поскольку будут добавляться четыре вершины для каждого спрайта.

Вызов SpriteBatcher.endBatch сообщит сортировщику спрайтов, что мы уже закончили отображение группы спрайтов (пакета) и что он должен загрузить вершины в графический процессор собственно для отображения. Мы собираемся использовать индексированное отображение с экземпляром класса Verti ces, поэтому нам потребуется определить индексы в дополнение к вершинам в f1oat-массиве буфера. Однако, поскольку мы всегда отображаем прямоугольники, мы можем один раз заранее сгенерировать индексы в конструкторе SpriteBatcher. Для этого нам нужно знать, какое максимальное количество спрайтов сортировщик может нарисовать за один раз. Наложив строгие ограничения на количество спрайтов, которое можно отображать для одной группы, мы сможем избежать наращивания массивов в других буферах; мы можем просто один раз распределить эти массивы и буферы в конструкторе.

Базовый механизм достаточно прост. Метод SpriteBatcher.drawSpriteO может показаться странноватым, но на самом деле он довольно прост (пока мы не включаем вращение и масштабирование). Нам необходимо рассчитать координаты вершин и текстурные координаты в соответствии с параметрами. В предыдущих примерах мы уже делали это вручную, например, когда определяли прямоугольники для пушки, ядра и Боба. Мы выполним примерно то же самое в методе SpriteBatcher dravySprite, но только автоматически, исходя из параметров в методе. В листинге 8.17 показан код SpriteBatcher.

Листинг 8.17. Фрагмент из SpriteBatcher.Java без вращения и масштабирования

Сначала рассмотрим члены класса. Член класса verti cesBuffer является временным float-массивом, в котором мы храним вершины спрайтов текущего пакета. Член bufferlndex указывает, с какого места float-массива мы должны начинать записывать следующие вершины. Член verti ces – это экземпляр Vertices, который используется для отображения пакета. Он также хранит индексы, которые мы сейчас определим. Член numSpri tes содержит количество уже нарисованных спрайтов в текущем пакете.

Переходя к конструктору, мы видим, что у нас есть две переменные: экземпляр класса GLGraphics, который нам нужен для создания экземпляра класса Vertices, и максимальное количество спрайтов, которые сортировщик может отображать в одном пакете. Первое, что мы делаем в конструкторе, – создаем массив переменных f1oat. У нас есть четыре вершины для каждого спрайта, каждая вершина занимает  (2 для х- и у-координаты и 2 для текстурных координат). Максимум у нас может быть количество спрайтов, равное maxSprites, поэтому для буфера нам понадобится 4 х 4 х maxSprites float. Далее создаем экземпляр класса Vertices. Он нужен нам для хранения не более maxSprites х 4 вершин и maxSprites х 6 индексов. Мы также сообщаем экземпляру класса Vertices, что у нас есть не только атрибуты положения, но и текстурные координаты для каждой вершины. Далее присваиваем членам bufferlndex и numSprites значение 0. Потом создаем индексы для нашего экземпляра класса Vertices. Нам нужно будет сделать это только один раз, потому что индексы не будут меняться. Первый спрайт в группе всегда будет иметь индексы 0,1, 2, 2,3,0; следующий – 4, 5,6, 6, 7,4 и т. д. Мы можем предварительно вычислить их и сохранить в экземпляре класса Vertices. Таким образом мы сможем установить их только один раз, вместо того, чтобы делать это для каждого спрайта.

Далее следует метод begi nBatch . Он привязывает текстуру и возвращает в исходное состояние члены numSpri tes и bufferlndex, так что вершины первого спрайта будут вставлены в начало массива float verti cesBuf fer .

Следующий метод – endBatch . Мы вызываем его, чтобы финализировать и отрисовать текущий пакет. Метод сначала переносит определенные для этого пакета вершины из массива f 1 oat в экземпляр класса Verti ces. Осталось привязать экземпляр класса Vertices, нарисовать numSprites х 2 треугольника и снова отвязать экземпляр класса Vertices. Поскольку мы используем индексированное отображение, мы определяем количество применяемых индексов, которое равно шести индексам для спрайта, умноженным на numSprites. Итак, остается само отображение.

Следующий метод можно назвать рабочей лошадкой класса SpriteBatcher. Он принимает х- и г/-координаты центра спрайта, его ширину и длину и TextureRegi on, к которому он относится. Метод добавляет четыре вершины в массив переменных f 1 oat, начиная с текущего buf ferlndex. Эти четыре вершины формируют текстурный прямоугольник. Мы подсчитываем позицию нижнего левого (xl; yl) и верхнего правого углов (х2; у2) и используем эти четыре переменные вместе с текстурными координатами из TextureRegi on для построения вершин. Вершины добавляются против часовой стрелки, начиная с нижней левой вершины. Когда они добавлены в массив float, увеличиваем счетчик mSprites и ждем добавления следующего спрайта или завершения пакета.

Это, пожалуй, все, что нужно сделать. Мы избегаем вызовов множества методов отрисовки, просто помещая предварительно преобразованные вершины в массив f1oat и отображая их за один раз. Так значительно ускорится реализация нашего отображения двухмерных спрайтов, по сравнению с методом, который мы использовали раньше. Меньшее количество изменений состояния OpenGL ES и меньше вызовов отрисовки упростят работу нашему графическому процессору.

Остается реализовать еще метод SpriteBatcher.drawSpriteO, который может рисовать вращающийся спрайт. Нужно построить четыре угловые вершины без добавления позиции, повернуть их вокруг начала координат, добавить позицию спрайта, чтобы вершины были расположены в пространстве игрового мира, а затем продолжить так, как в предыдущем методе отрисовки. Мы можем использовать для этого Vector2.rotate, но тогда возникнут некоторые функциональные издержки. Мы, таким образом, воспроизводим код из Vector2. rotate и оптимизируем его, где это возможно. Последний метод SpriteBatcher показан в листинге 8.18.

Листинг 8.18. Окончание метода SpriteBatcher.java: рисование вращающегося спрайта

Мы делаем то же самое, что и в более простом методе отрисовки. Кроме того, строим все четыре угловые вершины вместо только двух противоположных. Это требуется для вращения. Все остальное такое же, как и раньше.

А что насчет масштабирования? Нам явно не нужен отдельный метод, поскольку для масштабирования спрайта нужно отмасштабировать только его ширину и длину. Мы можем сделать это внутри двух методов отрисовки, поэтому нет нужды создавать еще несколько методов для этого.

Итак, это и есть тот самый большой секрет о том, как происходит молниеносное отображение спрайтов при помощи OpenGL ES.

Источник: Mario Zechner / Марио Цехнер, «Программирование игр под Android», пер. Егор Сидорович, Евгений Зазноба, Издательство «Питер»

По теме:

  • Комментарии