libGDX: Часть 4. Спрайты, Текстуры, Регионы, Атлас

Часть 3. Пример.

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

Немного теории

Для начала, думаю, стоит определиться с терминологией.

Текстура – обычное двумерное изображение хранящееся в памяти компьютера или графического акселератора в одном из пиксельных форматов. В случае хранения в сжатом виде на дисках компьютера текстура может представлять собой обычный бит-мап, который мы привыкли видеть в форматах bmp, jpg, gif и т.д. Перед использованием текстура разворачивается в памяти и может занимать объем в десятки раз больший первоначального размера. Перед выводом на экран необходимо знать пару вещей о текстуре: размеры, координаты, куда выводить и т.д. Вообще или я чего-то не понимаю, или большинство людей терминологию не понимают. На самом деле, изображение на диске — это не текстура. Вот после того, как вы загрузите его в GPU оно станет текстурой.

Спрайт — растровое изображение, свободно перемещающееся по экрану. По сути, спрайт — это проекция объекта на плоскости. В 2D графике то проблем с этим нет. Но при работе с 3D графикой, при изменении угла обзора, происходит разрушение иллюзии. Представьте себе стену в какой-нибудь игре, когда вы смотрите прямо на неё, то всё вроде порядке, но если смотреть сбоку на спрайт этот, то увидите лишь линию.

SpriteBatch

В LibGDX есть класс SpriteBatch, который берет на себя все трудности вывода текстур на экран, позволяющий без проблем выводить изображения. Так как это функция движка, то вам не надо задумываться о его оптимизации, об этом уж подумали за вас. Он работает с координатам экрана, а не мира. Вспомните урок 2 об архитектуре игры. У нас мир 8на5 клеток. Но реальный экран телефона в пикселях. Для этого и нужны коэффициенты ppuX и ppuY. При разработке логики, мы работаем с нашим координатами, но при выводе надо их преобразовать в пиксели.

Ниже представлен пример создания текстуры и отрисовки оной:

//SpriteBatch, который используется для отображения
SpriteBatch spriteBatch = new SpriteBatch();  
/*Создается текстурка по изображению (загружается файл в память видеоадаптера и готово к использованию в OpenGL). Поддерживается .jpg, .png и .bmp форматы точно, остальные не знаю (: Тут стоит сразу отметить важную штуку одну – ширина и высота изображения должна равняться степени двойки. Вообще, когда я тестил с изображениями, размеры которых не были степенями двойки, то на Android 4.0 они работали нормально, а вот на 2.3 нет. Так что, лучше, чтобы степенью 2 было. Стороны не обязательно должны быть одинаковыми, это может быть изображение, например, размером 16×32 пикселя.*/
Texture texture = new Texture(Gdx.files.internal("images/my-sprite.png")); 


spriteBatch.begin();
spriteBatch.draw(texture, 100, 100);
spriteBatch.end(); 

Стоит отдельно остановиться на отрисовке, а именно на двух командах: spriteBatch.begin() и spriteBatch.end(). Они в коде должны быть всего в одном месте. То есть, при рендеринге их нельзя два раза вызывать. Отрисовка всех спрайтов должна быть между вызовами этих методов.

Важно: начало координат в нижнем левом углу при отрисовке. Но, когда вы получаете координаты при клике на экране монитора, то там отсчёт по оси Y идёт сверху.

Перед тем. как модифицировать нашу игру, надо ещё рассказать про атласы. Создавать отдельную текстуру для каждого объекта не совсем правильно, вернее сказать, не оптимально. При рендеринге в этом случае GPU будет переключаться между текстурами, что снизит производительность. Кто писал под Win API знает, что означает переключение контекста. Я сам столкнулся с этим, когда распараллеливал кое-какие задачи, выходило, что уменьшение времени работы не покрывало время на переключение контекста. Так же и тут при работе с текстурами: слишком много времени тратится на переключение между ними. В 2D играх это не существенно, сказал бы я, но так как мы пишем игру на телефон, то стоит подумать об оптимизации. В данном случае, необходимо использовать так называемый атлас.

Текстурный атлас — большое изображение или «атлас», который содержит много изображений меньшего размера, каждое из которых является текстурой для какой-то части объекта.Подтекстуры можно накладывать путём указания текстурных координат на атласе, то есть выбирая только определённую часть изображения. В приложениях, в которых часто используются мелкие текстуры, более эффективно хранить текстуры в текстурном атласе, так как:

  1. Позволяет сократить количество смен состояний (о чём писал выше) до одного для всего атласа.
  2. Уменьшает количество занятых текстурных слотов до одного для всего атласа.
  3. Минимизируется фрагментация видеопамяти.

Пример атласаВ случае, когда у вас объекты одинакового размера, то собственноручно создать такой атлас не составляет особой проблемы. Но, если изображения отличаются по размеры, или их много, то лучше воспользоваться специализированными средствами по упаковке. В libGDX для этого есть TexturePacker. О нём не буду останавливаться пока, ибо для наших задач мы можем сами атлас создать. Многим этот пакер может никогда и не понадобится (:

SpriteBatch включает в себя методы по обработке текстуры перед выводом. Сам метод draw имеет несколько перегрузок.

Сигнатура метода Описание
draw(Texture texture, float x, float y) Отображает текстуру, используя ширину и высоту текстуры.
draw(Texture texture, float x, float y, int srcWidth, int srcHeight) Отображает текстуру, используя заданную ширину и высоту. То есть, если сходное изображение, к примеру, 20×20, то вы можете задать при выводе 10×10, изображение ужмётся само.
draw(Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight) Рисует часть текстуры.
draw(Texture texture, float x, float y, float width, float height, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY) Рисует часть текстуры, растянутую до размеров width*height, и, если необходимо, отраженную.
draw(Texture texture, float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY) Этот монструозный метод рисует часть текстуры с возможностью сжатия (растяжения) до width*height, вращения вокруг точки и возможностью отражения.
draw(Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2) Рисует часть текстуры, растянутую до размеров width*height. Это более продвинутый метод. Координаты указываются не в пикселях, а в действительных числах.
draw(Texture texture, float[] spriteVertices, int offset, int length) Рисует часть текстуры, «натягивая» ее на фигуру, указанную в spriteVerticles.

TextureRegion и использование атласа

Приступим к модификации нашей игры. Сразу применим атлас и TextureRegion для получения части изображения из атласа. Класс TextureRegion описывает прямоугольник внутри текстуры и используется для рисования только части текстуры. Для начала в папку assets добавьте папку images. Здесь будут хранится наши изображения. Добавим в класс WorldRenderer переменные нужные:

//для отрисовки
private SpriteBatch spriteBatch;
//текстурка нашего атласа
Texture texture;
//массив регионов
public  Map<String, TextureRegion> textureRegions = new HashMap<String, TextureRegion>();

Добавим в класс WorldRenderer метод по созданию текстуры и получению нужных регионов:

private void loadTextures() {
  //создание текстуры
  texture  = new Texture(Gdx.files.internal("images/atlas.png"));
  /*
  Получение регионов. Атлас у нас состоит из 4 изображений одинакового размера. Так что выцепить отдельные регионы не составляет проблемы.
  */
  TextureRegion tmp[][] = TextureRegion.split(texture, texture.getWidth() / 4, texture.getHeight() / 4);
  //добавляем в массив регионов
  textureRegions.put("player", tmp[0][0]);
  textureRegions.put("brick1", tmp[0][1]);
  textureRegions.put("brick2", tmp[1][0]);
  textureRegions.put("brick3", tmp[1][1]);
}

Изменяем главный метод по отрисовке render() и методы по отрисовке блоков и игрока.

public void render() {	
  //начинаем рисовать			
  spriteBatch.begin();
  //рисуем блоки
  drawBricks();
  //рисуем игрока
  drawPlayer();
  //заканчиваем отрисовку
  spriteBatch.end();			 
}

//отрисовка блоков
private void drawBricks() {
  int i=0;
  for (Brick brick : world.getBricks()) {
    //ради интереса для отрисовки используем разные изображения (регионы)
    spriteBatch.draw(textureRegions.get("brick"+(i%3+1)), brick.getPosition().x* ppuX, brick.getPosition().y * ppuY,Brick.SIZE * ppuX, Brick.SIZE * ppuY);
  ++i;
  }
		

}

//рисуем игрока
private void drawPlayer() {
  spriteBatch.draw(textureRegions.get("player"), world.getPlayer().getPosition().x* ppuX, world.getPlayer().getPosition().y * ppuY,Player.SIZE * ppuX, Player.SIZE * ppuY);
}

Для работы со спрайтами в libGDX есть класс Sprite. Его использование может показаться удобным. Но класс Sprite смешивает информацию про модель (расположение и другие) с информацией про представление. Это делает неподходящим использование Sprite в архитектуре, где вы хотите строго отделить модель от представления. Я его поэтому и не рекомендую использовать. Использование класса Texture или TextureRegion имеет больше смысла.

Теперь игра при запуске будет выглядеть так:

Часть 4. Пример.

Собственно всё. Можете скачать исходники урока Libgdxtutorial-lesson4.rar.