Загрузка...
 
Печать

Тайлы (Tiles). Создание игровых террейнов

(Game terrain creating: Tile-based graphics)

Одним из ключевых элементов при разработке многих игр является создание террейн-движка (terrain engine). Принимая во внимание, что действие большинства игр (стратегия, RPG, экшн и т.д.) разворачивается на твёрдой поверхности, значение террейна (т.е. местности, земной поверхности, геологической локации) трудно переоценить. Существует множество методов прорисовки террейна, наиболее старым и классическим является рендеринг на основе плиток (tile-based rendering; далее -тайловый рендеринг).

Содержание



Основы создания террейн-движка на основе тайлов (плиток)

Сперва немного теории.

Что такое тайл (плитка)?


Image
Рис.1 Примеры террейновых тайлов


Тайл представляет собой строительный блок террейна.1 Строительные блоки, как и тайлы, сами по себе ничего не делают. Но, собрав несколько тайлов воедино, они уже будут иметь определённое значение. Для понятия принципа действия тайлов достаточно рассмотреть обычную мозаику вроде картонных пазлов. Каждый элемент мозаики сам по себе ничего не значит. Но все вместе они образуют законченную картину. Тайлы в тайловом террейн-движке работают по тому же принципу. Только холстом здесь выступает земная поверхность (пусть и виртуальная), а кистями (элементами мозаики) - террейные плитки (terrain tiles) (См. Рис.1).
На Рис.1 изображены тайлы леса, водной поверхности, скалистые горы, травянистая равнина и пустыня. Даже с таким ограниченным набором тайлов можно создать терейн, скажем, ограниченный со всех сторон горами. Бывают как квадратные (вид сверху), так и изометрические. Набирают популярность шестиугольные (hexadecimal) тайлы (Civilization VI).

Зачем нужны тайлы?

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

Экономия оперативной памяти

Возьмём, к примеру, карту типичной 2D RTS размером 100 тайлов в ширину и 100 в высоту. Казалось бы, ничего особенного: обычная карта размером 100х100 тайлов. В общей сложности для создания карты задействовано 10 000 тайлов. А теперь представь, что вместо тайлов ты решил применить для прорисовки тайлов один огромный битмап (= растровое изображение). Сперва рассчитаем объём памяти требуемый для тайловой карты. Мы просто перемножим количество тайлов в карте (10 000) на размер одного тайла:
100 тайлов в ширину х 100 тайлов в высоту = 10 000 тайлов. 64 пиксела в ширину х 64 пиксела в высоту = 4096 пикселей на каждый тайл.
10 000 тайлов х 4096 пикселей х 1 байт (8 бит) = 40 960 000 байт (при 256-цветной палитре). 10 000 тайлов х 4096 пикселей х 4 байта (32 бита) = 163 840 000 байт (при 32-битном цвете).
Получаем, что при отрисовке терейна (карты) с помощью одного битмапа размером 100 на 100 обычных тайлов потребуется 163 мегабайта памяти! Даже если снизить цветность до 256 цветов карта всё равно будет "весить" не меньше 41 мегабайта. Даже по сегодняшним меркам 163 мегабайта для карты из 2D стратегии - это слишком много.
А теперь рассчитаем объём памяти, требуемый для прорисовки терейна (карты) составленной при помощи 10 000 тайлов (100 х 100):
100 тайлов в ширину х 100 тайлов в высоту = 10 000 тайлов.
64 пиксела в ширину х 64 пиксела в высоту = 4096 пикселей на каждый тайл.
100 тайлов х 4096 пикселей (на каждый тайл) х 4 байта на каждый пиксел (32-битный цвет) = 1 638 400 байт. 10 000 тайлов х 1 байт на каждый тайл = 10 000 байт. 10 000 байт + 1 638 400 байт = 1 648 400 байт.
То есть, из набора тайлов (tileset), содержащего 100 различных тайлов, можно создать карту размером 100 х 100, затратив на это менее двух мегабайт памяти. Даже при наборе, содержащим 1000 различных тайлов, объём карты не превысит 20 мегабайт.
В итоге получаем, что главная причина применения тайлов в игрокодинге - экономия системных ресурсов и, в частности, системной памяти.

Повторное использование повторяющихся графических элементов


Image
Рис.2 Пример простой карты


  • Очень важный аспект в игрокодинге, т.к. объём рабочего времени художников (как и разработчиков) обычно сильно ограничен.
На Рис.2 мы видим поле с деревьями. При дальнейшем рассмотрении можно заметить, что многие деревья схожи друг с другом. Теперь представь, что художник должен вручную отрисовывать каждый камень на карте. Это займёт кучу времени. Большинство гейм-дизайнеров не скрывают, что чаще им приходится многократно применять ранее наработанные зарисовки, нежели с нуля создавать игровой контент. Ведь кучу времени можно потратить и на более интересные занятия.
По сути карта на Рис.2 состоит всего из нескольких тайлов с травой и деревьями. Используя тайлы, геймдизайнер лишь "разбросал" эти готовые паттерны по поверхности терейна. Это в разы упростило задачу художника, т.к. надо было всего лишь разместить тайлы деревьев в разных участках карты.

Возможность создания динамического контента (например рандомно генерируемых карт)

Динамический контент необходим для многих игр. Возьмём, к примеру, генератор случайных карт (random map generator). Одной из первых игр, впервые применившей генератор случайных карт была небезызвестная Civilization. Здесь игрок уазывал тип желаемой карты и игра генерирует её буквально "на лету". Используя тайлы, создатели Civilization просто применили ряд алгоритмов для более или менее адекватного распределния тайлов на игровой карте. Если бы карта представляла собой один большой битмап, то его рандомная генерация превратилаяь бы в сущий кошмар как для программиста так и для компьютера. Предаставь, каково это, прорисовывать каждый отдельный пиксел программно. В случае с набром тайлов, содержащим траву и камни, программист задаёт генератор случайных чисел (random number generator) для случайного размещения камней на игровой карте (терейне). Именно тайлы позволяют создавать динамический контент быстро и просто.

Как создают тайлы?

Создание тайлов - очень трудоёмкая и кропотливая работа. Конечно, многие творческие личности очень любят это знаятие. Выбери размеры тайла (tile dimensions)
Сперва вооружись хорошим графическим редактором. В идеале - Adobe Photoshop Подойдёт и триальная версия программы, т.к. редактирование тайлов - относительно нехитрая операция с точки зрения графического дизайна. Многое можно сделать даже в обычном MS Paint.
Следующий шаг - выбери желаемый размер тайла. Для неизометрических наборов тайлов обычно применяют размеры, кратные числу 2: 32х32, 64х64, 128х128 и т.д. Выбор размера тайла зависит от ситуации. Каки-то определёных стандартов не существует. Но лучше исходить из следующих соображений:
  • Сколько тайлов потребуется игре?
  • Сколько памяти будет выделено для игровой графики?
  • Сколько тайлов одновременно будет видно во вьюпорте игры?
Два первых вопроса сильно взаимосвязаны друг с другом, т.к. число тайлов напрямую зависит от объёма памяти, необходимого для их хранения. Если игра требует набор в 1000 тайлов, то больших размеров лучше избегать. Если тайлов не больше сотни, то их можно сделать побольше.
Последний вопрос связан со способностью движка показывать в одном кадре тайлы без тормозов. Часть экранного пространства будет отведена под пользовательский интерфейс (User Interface, UI), т.е. под контрольную панель со статистикой, выбором построек и др. данными.
Оставшуюся часть занимает тайловая карта (tile map) размером 10х10 тайлов. Для того, чтобы подсчитать макс. размер тайла, необходимо минимальное игровое разрешение разделить на различные значения тайла. Максимальный размер тайла вычисляется по след. формулам:
Ширина экрана (Screen Width) / Требуемое число тайлов Высота экрана (Screen Height) / Требуемое число тайлов
При разрешении экрана 800х600 пикселей макс. размер тайла составит (800/10) = 80 пикселей в ширину. Если подогнать это к правилу кратности двум, то макс. размер тайла лучше выбрать 64х64 пикселей.
Если такой рзмер покажется тебе слишком маленьким, то прикинь, сколько тайлов одновременно будет видно в кадре в каждый момент времени. На самом деле это зависит от того, сколько юнитов одновременно ты планируешь выводить на экран (макс. количество). Если в игре планируется контроль игроком многочисленной армии юнитов, то лучше выбирать размер плитки поменьше. Если наоборот, юнитов будет совсем немного (Warcraft III), то лучше выбирать размер плитки побольше.

Идентифицируй необходимые тайлы

Когда ты выяснил размеры тайла, можно сразу переходить к их созданию. С чего же начать? Начинай с тайла земной поверхности (ground tile), и на его основе рисуй остальные. Например в играх Age of Empires и Age of Wanders основные блоки террейна представляют собой траву (grass), грязь (dirt) и даже снег (snow).
Значит и ты начинай с базовых тайлов травы, грязи, снега и т.д., постепенно добавляя различные вариации. На сплошном футбольном газоне играть скучно. Если, например, в качестве базового тайла выбрана трава, то создай на её основе другие тайлы, добавив камней, деревьев, воды или даже грязевых луж (См. Рис.4).

Покажем ли тайлы?

Отображение тайлов в теории выглядит очень просто, но на деле это очень трудоёмкий процесс. Речь идёт о размещении тайлов на единой сетке (grid).
Существует 3 основных способа вывода отображения тайлов на экране:
  • 2D,
  • Изометрическая проекция (Isometric projection; т.н. 2,5D),
  • 3D.

Отображение в 2D-ceткe


Image
Рис.3 Расчёт положения выбранного тайла в массиве тайлов


  • Самый простой способ.
Здесь ты располагаешь тайлы по горизонтали (ширине экрана) и вертикали (высоте экрана). Первый тайл размещается в левом верхнем углу сетки (отсюда идёт начало отсчёта). Завершающий тайл располагается в нижнем правом углу сетки. Весь процесс отображения тайов сводится к простому тарверсингу (traversing, прокрутка) карты слева на право, сверху вниз. Рассмотрим следующий сниппет:
int x,y;

// Показываем сверху вниз
for(y=0; y<10; y++)
{
	// Показываем слева направо
	for(x=0; x<10; x++)
	{
		// Твоя функция показа здесь
		DisplayTile (x,y);
	}
}

В вышеприведённом коде есть 2 замкнутых цикла (loop). Первый инкрементирует (с каждым повторением увеличивает на единицу) вертикальную позицию тайла. Второй цикл - горизонтальную позицию тайла. Разместив цикл горизонтальной прорисовки внутри цикла вертикальной прорисовки, получаем отрисовку всей сетки (см. Рис.3). Обрати внимание, что в верхнем левом углу стоит номер ячейки 1, а в нижнем правом - 100.
Естественно, что функци. DisplayTile() ещё предстоит написать. Но до этого необходимо подумать о том, как будет хранится тайловая карта (tilemap, она же сетка, грид). Самый распространённый способ - представить тайлмэп в виде одного большого массива (array). Для демонстрации данного метода рассмотрим следующий код:
// Global tile map array
int g_iTileMap[100]; // 10*10 = 100 точек (ячеек) необходимо

// Прототип функции отображения тайла
void vDisplayTile(int x, int y);

void main()
{
	int x,y;
	
	// Сверху вниз
	for(y=0; y<10; y++)
	{
		// Слева направо
		for (x=0; x<10; x++)
		{
			// Показываем тайл
			vDisplayTile(x,y);
		}
	}
}

void vDisplayTile(int x, int y)
{
	int iTile;
	int tileWidth = 64;
	int tileHeight = 64;
	int mapWidth = 10;
	
	// Подсчитываем значение тайла
	// в данной ячейке с координатами x и y
	
	iTile = g_iTileMap[(x + (y * mapWidth))];
	// Показываем битмап на экране
	// Данная функция является псевдокодом!
	// Её необходимо заменить реальной функцией.
	DrawBitmap(iTile, (x * tileWidth), (y * tileHeight));
}

В функции main(), приведённой выше, программа выполняет цикл, итерируя тайлы один за другим. В конце вызывается функция vDisplayTile(). Внутри цикла функция vDisplayTile() идёт код вычисления местоположения тайла в массиве тайлов (tile array). К координате x (x-position) прибавляется координата y (y-position), умноженная на ширину экрана (тайловой сетки) (см. Рис.3).

Как рассчитать текущую позицию в массиве?

Для получения начальной позиции тайла в массиве сначала смотрим его x-координату. Допустим нам нужно найти позицию в массиве тайлов для ячейки грида с координатами 0,5. На Рис.3 мы начинаем отсчёт с тайла, расположенного в левом верхнем углу, и перемещаемся на 5 тайлов вправо. Так мы пришли в ячейку, помеченную "A".
Теперь необходимо к этой координате прибавить y-координату тайла, умноженную на ширину карты в текущей позиции. Размер игровой карты (сетки, грида) составляет 10 тайлов в ширину. Поэтому прибавляем 10*5 (y-координата) к текущей позиции. Следуй за стрелкой вправо, а затем вниз и окажешься в ячейке, помеченной "B". Таким образом можно вывести след. формулу:
X(A) + (Y*MapWidth) = array position (B)
На Рис.3 массив карты (map array) изображён в виде сетки (=грида), но в реальности он представляет собой вполне линейный кусок памяти. Сетка здесь применена для лучшей наглядности. Главным преимуществом массива карты является то, что он в точности соответствует тому, что пользователь видит на экране. Здесь даже нет сложных связных списков (linked lists) с необходимостью их последующей трассировки. Просто один простой массив.

Как подсчитать видимую позицию (visual position)?

Следующей рассмотрим функцию DrawBitmap() из приведённого выше псевдокода. Она представляет собой функцию, которая содержит в себе (resembles) все вызовы графических процедур (graphics calls), применяемые для отображения 2D-битмапа (растрового изображения).
В вызове этой воображаемой функции есть 3 параметра:
  • Графический тайл (tile graphic). Здесь назначается растровое изображение (битмап), выводимое на экран. В реальной реализации можно обращаться напрямую к графическому файлу, либо указать номер (индекс) из заранее заготовленного списка отображения (display list).
  • Экранная x-координата (on-screen x-position). Устанавливает x-координату ячейки, где будет отображаться данный тайл.
  • Экранная y-координата (on-screen y-position). Устанавливает y-координату ячейки, где будет отображаться данный тайл.
В случае с нашим примером x- и y- координаты для тайла 5,5 будут следующими: (5*64), (5*64) или 320, 320 (пикселей от начала отсчёта экранного пространства). В результате получаем формулу подсчёта x- и y- (пиксельных) координат тайла:
X-координата = X-позиция тайла в сетке * Ширина тайла (Tile width) Y-координата = Y-позиция тайла в сетке * Высота тайла (Tile height)

Отображение изометрических (isometric, т.н. 2.5D) тайлов


Image
Рис.4 Оси изометрического (2.5D) грида и порядок вывода тайлов


Здесь применяются более сложные методы, т.к. необходимо заранее подготовить специальные тайлы. Вообще, процесс создания 2D и 3D тайлов во многом схожи, т.к. и те и другие (чаще всего) имеют квадратную форму.
Изометрические тайлы можно встретить во множестве известных RTS-играх: Age of Empires, Civilization, Command&Conquer: Tiberium Sun и др. Главное преимущество изометрических тайлов состоит в том, что они выглядят как трёхмерные, не используя при этом настоящую 3D-графику. Конечно, сегодня изометрия не так популярна как раньше, ведь большинство игроманов имеют компьютеры, способные работать с настоящей 3D-графикой. Потому большинство современных стратегий полностью трёхмерным. Изометрические тайлы заметно уступают трёхмерным по уровню ограниченности применения.
В основе изометрического тайла лежит обычный 2D-тайл, повёрнутый по двум осям на 45 градусов (См. Рис.4). Такой поворот также видоизменяет сетку карты (грид), т.к. в этом случае тайлы оказываются повёрнуты и слегка наклонены относительно перспективы вьюера (viewer's perspective). Т.к. карта оказывается повёрнутой, её координаты X и Y уже не совпадают с соответствующими экранными координатами (где начало координат расположено в верхнем левом углу). Вместо этого ось X берёт своё начало в середине верхнего края экрана и идёт в правый нижний угол экрана. Ось Y также начинается в середине верхнего края экрана и направлена в левый нижний угол экрана. Таким образом тайл с координатой 0,0 оказывается расположен в середине верхнего края экрана. Определив расположение начала координат, можно легко составить алгоритм вывода тайлов на экран. Здесь применяется тот же принцип, что и при расчётах для 2Р-проекции. С единственной поправкой на то, что в этот раз координаты тайлов будут несколько иными (См. Рис.8). Из рисунка видно, что вывод тайлов на экран также происходит в гриде (сетке), с той лишь разницей, что в этот раз сетка повёрнута. Как результат, нам необходимо вычислять смещение (offset) для X- и Y-координаты каждого тайла, выводимого на экран.

Отображение 3D-тайлов

Да, наконец-то мы дошли до трёхмерной графики. Сегодня почти все новые RTS-игры полностью трёхмерны, что неудивительно. Ведь трёхмерная графика обладает массой преимуществ, включая:
  • Динамическое отображение (Dynamic display);
  • Возможность вращать карту по всем осям;
  • Наличие реальной глубины прорисовки.
Первое преимущество (динамическое отображение) очень полезно в 3D-графике, т.к. при этом можно менять текстуры объектов буквально "на лету" без перерисовки остального контента. Возьмём к примеру обычный холм, выполненный в 3D. Внешний вид холма легко меняется с травы (grass) на грязь (dirt) путём простого переключения (swapping) с одной текстуры на другую. В случае с 2D-проекцией, пришлось бы создавать два отдельных тайла и полностью перерисовывать тайл, что ресурсоёмко.
Следующее преимущество 3D-графики (возможность вращать карту по всем осям) говорит само за себя. Если ты хоть раз пытался повернуть 2D-битмап, чтобы посмотреть, что расположено на обратной стороне, то ты понимаешь о чём речь. При переходе в 3D ты можешь свободно (или почти свободно) вращать тайлы карты и разглядывать их со всех сторон. В зависимости от расположения вьюпорта (viewport, виртуальная камера, отображающая ровно то, что видит игрок на экране) игровая карта может выглядеть по-разному. Кроме того использование 3D сохраняет кучу дискового пространства, т.к. для изменения угла обзора достаточно создать фрейм анимации (frame of animation) вместо покадровой прорисовки каждого возможного угла вращения.
Третье преимущество - глубина (depth) - вообще больше относится к вращению карты в 3D-пространстве. Только в 3D-игрок может свободно приближать (zoom in) и отдалать (zoom out) камеру вьюпорта.
Так как же 3D-тайлы выводятся на экран? Для этого их транслируют в определённую позицию и дают команду на прорисовку примитива (draw primitive). Вообще 3D тайлы можно отображать точно также, как и их 2D-собратьев. Ты просто помещаешь их в сетку (грид) и рендеришь от начала до конца. 3D-проекция не означает значительное видоизменение RTS-игр. Она лишь добавляет элемент гибкости (flexibility) и открывает новые возможности для игрокодеров.

Многослойные тайлы (Multi-Layering Tiles)

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

Как добавить дополнительные подробности (details) в тайл?


Image
Рис.5 Тайловые карты с различными деталями на них


Первая причина использования многослойных тайлов - это возможность добавлять в тайл т.н. детали. Это реализуется путём простого добавления нескольких слоёв тайлов, один поверх другого. Пока звучит достаточно просто. Сложность состоит в том, как определить, какой из тайлов должен быть отображён на каждом из слоёв.
На Рис.5 представлены три тайловые карты. На первой карте нет деталей (трава не считается деталью). На второй на отдельном слое добавлены скалы (rocks). Такая карта уже считается двухслойной (two-layer tile map). Первый слой содержит тайлы земной поверхности (с травой), на втором - детали в виде скал. На третьей карте добавлен третий слой с тайлами деревьев (trees).
Кто-то может спросить, почему бы не разместить тайлы с землёй, скалами и деревьями на одном слое? Потому что это повлечёт необходимость создавать дополнительные тайлы (скалы с деревьями, скалы без деревьев и т.д.). Это просто нерационально. К тому же, когда потребуется создать карту с песком, она потянет за собой целый сонм новых тайлов. В современном игрокодинге часто применяют тайлы с альфа-каналом, по контуру которого отсекается всё ненужное и скала лежит на траве не в виде квадрата, а имеет неправильную форму как в реальном мире. Таким образом тайл скалы может без труда смешиваться с любой поверхностью, создавая вполне реалистичный вид. В конечном итоге такой подход избавит тебя от создания сотен дополнительных тайлов. Да и игра будет весить куда меньше.

Как добавить переходные тайлы (Transition Tiles)?


Image
Рис.6 Переходные тайлы

Image
Рис.7 Тайлы дороги


Помимо тайлов песка, травы и скал, на тайловых картах применяется и другой очень важный вид тайлов - переходные тайлы. Они необходимы для плавного перехода от одной тайловой текстуры к другой. Если просто взять тайлы травы, песка и воды и создать на их основе произвольную карту, регионы на ней будут иметь неестесственные очертания с прямыми углами. Конечно, для любительского инди-проекта такая карта "потянет", но мы расскажем как её сделать более реалистичной. И помогут нам в этом переходные тайлы, размещаемые на границе двух тайловых текстур (например травы и песка). См. Рис.6.
Первым делом понадобятся тайлы для плавного перехода от травы к песку с севера на юг. Достаточно просто начертить плавную кривую линию разграничения.
Но работа над переходными тайлами на этом не закончена. Ведь остаются ещё углы (corners). Да, нам нужны тайлы для плавного перехода от "северо-южных" тайлов к "восточно-западным". Четыре угловых тайла плавно стыкуют рассмотренные выше переходные тайлы. Угловые переходные тайлы часто формируют округлую форму региона на карте. Но и их бывает недостаточно, например когда террейн имеет неправильную форму. Видны прямые углы, портящие вид террейна. Здесь на помощь придут инвертированные угловые тайлы (Inverted Corner Tiles). Здесь представлен полный набор тайлов, который позволяет создать практически все возможные комбинации перехода с песка на траву.

Как добавить дорожные тайлы (road tiles)?

Технически дорожные тайлы во многом схожи с деталями (иногда в 3D-графике их называют "деколями") и переходными тайлами. Но с ними тоже не всё так просто.
Допустим игрок в процессе игры занят расширением собственной империи и хочет построить новую дорогу. Что произойдёт, когда он станет размещать тайлы дороги (road tiles) один за другим? Сможет ли игра проконтролировать, изменение каждого дорожного тайла, например при создании поворота? Сможет ли она проконтролировать необходимость изменения тайлов, прилегающих к дороге? Проблема здесь заключается в том, как узнать в какую сторону должен быть развёрнут новый тайл, для указания поворота в соответствующем направлении? Конечно, это должен быть угловой тайл (См. Рис.7).
Первым делом необходимо проанализировать все тайлы, расположенные вокруг нового тайла. Алгоритму достаточно оттрассировать (traverse) все соседние тайлы, выбрав и пометив из них дорожные. В нашем случае видим всего два дорожных тайла, прилегающих к новому с севера и с востока.
Теперь создаём таблицу просмотра (lookup table, индекс) для определения какой из тайлов выбрать и установить в данной ситуации. Создать её относительно нетрудно. Для этого каждому выбранному (=дорожному) тайлу присваивается число, с отсчётом от 0 по часовой стрелке (clockwise) и инкрементированием на 2 при переходе от одного тайла к другому. Таким образом первый тайл будет "равен" 0, второй - 2, третий - 4, четвёртый - 8 и так далее. После этого присвоенные тайлам значения (0 и 2) суммируются. В результате получаем 2. При обращении к таблице просмотра видим, что числу 2 соответствует угловой тайл.

Как добавить тайлы с 3D-деревьями?

У деревьев есть ветви и листья, через которые можно увидеть другие объекты, частично скрытые ими. Они раскачиваются (sway) на ветру и вообще должны выглядеть объёмными. Времена, когда 2D-спрайты заменяли 3D-модели давно остались в прошлом. Как видим, создание реалистичных 3D-деревьев -свосем непростое занятие. Проблема ещё и в том, что в большинстве современных RTS игровая карта представлена в изметрической проекции или под определённым углом. Когда в игре камера смотрит строго сверху вниз такой проблемы нет: достаточно просто нанести на карту тайлы с округлыми кустами. Но данный случай здесь не рассматривается.
Рассмотрим небольшой пальмовый лес. Обрати внимание, что пальмы создают эффект объёма и полностью трёхмерны. Они размещены перед зданием и за ним. А ведь на всю эту сцену потребовалось не более 300 полигонов. Здесь вместо 3D-моделей деревьев были применены т.н. 20-билбордыg (2D-billboards). По этой технологии деревья на самом деле являются плоскими 2D-спрайтами. Самое замечательное здесь то, что каждое дерево состоит всего из двух полигонов, образующих т.н. квад (quad), на который нанесена текстура пальмы. Для создания реалистично выглядещей 3D-пальмы потребовалось бы несколько сотен, а то и тысяч полигонов.

Как добавить анимированные тайлы?

Практически в каждой RTS на игровой карте есть тайлы, которые анимированы. Обычно в первую очередь анимируют водную поверхность. Без анимации водные тайлы выглядят очень неестесственно и схематично. Один из самых простых методов анимации тайлов состоит в подготовке серии тайлов и их последующего "перелистывания" (быстрой смены). Допустим, нам необходимо анимировать тайлы с 1 по 100, анимация каждого из которых будет состоять из 10 сменяющихся кадров. Для этого необходимо подготовить 10 полностью анимированных тайлов, по 10 кадров в каждом. При проходе (итерации) цикла рендеринга (render loop) через тайлы с номерами 0, 10, 20, 30, 40, 50, 60, 70, 80 или 90 к номеру тайла прибавляется номер текущего кадра анимации (animation frame), отображаемого в данный момент времени. Когда номер текущего кадра анимации достигает 10, он сбрасывается на 0 и анимация начинается сначала. Для примера рассмотрим следующий псевдокод:
Anim_Frame = 0;

Loop Start = 0;
Loop < #TilesToDisplay;
Loop++;

// Render animated tile
if(Current_Tile.type = TYPE_ANIMATION)
{
	// Render as normal
}
else
{
	RenderTile(Current_Tile.value);
}

// Increment animation counter
Anim_Frame++;
if(Anim_Frame == 10)
{
	Anim_Frame = 0;
	Loop Repeat;
}

В данном псевдокоде цикл рендеринга (rendering loop) рендерит текущий в гриде тайл + текущий кадр анимации. Здесь следующий кадр анимации рендерится с каждым новым проходом цикла анимации. Процесс во многом схож с анимированием обычных битмапов. Разница лишь в том, что ты заранее подготавливаешь набор (set) с различными вариантами водной ряби, из которых в каждый момент времени для каждого тайла выбирается один из них. Рекомендуем заранее выбирать достаточно большой участок тайлов на гриде, которые будут анимированы (например тайлы от 0 до 1000).

Редактирование и хранение тайлов

Существует много разных способов хранения и редактирования тайлов. Здесь мы рассмотрим лучшие и наиболее распространённые из них:
  • 2D-массив (2D array storage)
  • Многослойный массив тайлов (Multilayer tile array storage) А затем разработаем класс тайла.

2D-массив (2D array storage)


Image
Рис.8 Тайловая карта с назначенной областью рисования. Источник: https://talk.pokitto.com


  • Двумерный массив.
  • Самый простой метод.
Первое измерение содержит тайлы по горизонтали, второе - по вертикали. Взгляни на следующий фрагмент кода:
// Устанавливаем измерения (dimensions) игровой карты
#define TilesWide 10
#define TilesHigh 10

// Объявляем массив карты
int iTileMap[TilesWide][TilesHigh];

// Очищаем карту, применив "нулевой" тайл
memset(&iTileMap, 0, (TilesWide*TilesHigh)*sizeof(int));

Здесь объявляется двумерный массив. Т.к. размер карты составляет 10 тайлов по ширине и 10 по высоте, всего карта содержит 100 тайлов. Тайл, расположенный в левом верхнем углу будет иметь координаты 0,0, а в правом нижнем - 9,9. После этого карта "обнуляется" путём применения тайла 0, представляющего собой базовый тайл (base tile) для всей системы тайлового рендеринга. Обычно в качестве тайла 0 назначают тайл земной поверхности (ground tile) или даже тайл "без тайла", который представляет собой тайл с битмапом, на котором написано "unused" или что-то вроде этого. Такие тайлы позволяют наглядно видеть всю карту без каких-либо назначенных на ней тайлов.
Теперь, когда у нас есть двумерный массив тайлов, каким образом один тайл будет сменяться на другой? Допустим, необходимо изменить тайл, который расположен на втором тайле слева и третьем тайле сверху, от начала отсчёта, на тайл, под номером 15. Для этого достаточно написать что-то похожее (См. Рис.9):
// Top to bottom
// Сверху вниз
iTileMap[0] [0] = 15;
iTileMap[0] [1] = 15;
iTileMap[0] [2] = 15;
iTileMap[0] [3] = 15;
iTileMap[0] [4] = 15;

// Left to right
// Слева направо

iTileMap[1] [4] = 15;
iTileMap[2] [4] = 15;
iTileMap[3] [4] = 15;

// Bottom to top
// Снизу вверх
iTileMap[3] [3] = 15;
iTileMap[3] [2] = 15;
iTileMap[3] [1] = 15;
iTileMap[3] [0] = 15;

// Right to left
// Справа налево
iTileMap[2] [0] = 15;
iTileMap[1] [0] = 15;


Image
Рис.9 Тайловая карта с окрашенной тайлами областью в форме буквы О


Здесь мы устанавливаем в выбранной позиции массива желаемое значение. К примеру, мы назначаем выбранным тайлам значение 15. Код стартует с левой стороны буквы О, начиная с левого верхнего угла. Следующий фрагмент рисует нижнюю часть буквы О, слева направо. Затем код рисует правую сторону буквы О, начиная с нижней части, и двигаясь по направлению вверх. Наконец, код закрывает букву О, рисуя её верхнюю часть справа налево. Данный процесс во многом схож с нанесением пикселей (pixel plotting).

Многослойный массив тайлов (Multilayer tile array storage)

Однослойный 2D-массив подойдёт для игровых карт начального уровня. Поэтому двигаемся дальше и рассмотрим многослойные карты, для создания которых необходимы массивы с несколькими измерениями. Самый простой способ сохранить несколько слоёв тайлов - это добавить в исходный массив дополнительное измерение (dimension). Для этого внесём в вышеприведённый код некоторые изменения:
// Устанавливаем измерения (dimensions) игровой карты
#define TilesWide 10
#define TilesHigh 10
#define TileLayers 3

// Объявляем массив карты
int iTileMap[TilesWide][TilesHigh][TileLayers];

// Очищаем карту, применив "нулевой" тайл
memset(&iTileMap, 0, (TilesWide*TilesHigh*TileLayers)*sizeof(int));

Здесь первым делом добавлена ещё одна строка #define TileLayers 3, устанавливающая количество тайловых слоёв карты (произвольно выбрано число 3). Далее добавляем ещё одно измерение в сам массив. Достаточно одного, т.к. в нём дополнительно определено кол-во слоёв. В последней строке мы дописываем в формулу очищения карты наше измерение с тремя слоями. В итоге получили многомерный массив для поддержки многослойных тайловых карт. Допустим, первый слой карты содержит 2 тайла с травой (grass tile). Второй слой карты содержит тайл с камнями. Вся карта генерируется так:
// Устанавливаем базовые тайлы (с травой)

// По вертикали
for(int i=0; i<10; i++)
{
	// По горизонтали
	for(int j=0; j<10; j++)
	{
		// Произвольно устанавливаем базовый тайл
		iTileMap [i][j][0] = rand()%2;
	}
}

// Добавляем тайлы с деталями (2-й слой)
iTileMap[5][5][1] = 3; 
iTileMap[3][9][1] = 3;
iTileMap[1][7][1] = 3;
iTileMap[8][8][1] = 3;
iTileMap[6][3][1] = 3;
iTileMap[4][1][1] = 3;

В первом блоке кода стоят циклы, через которые проходит каждый тайл карты, произвольно заполняя карту базовыми тайлами (с травой). У нас их два. Для того, чтобы карта не выглядела чересчуп "паттерново". Во втором блоке мы вручную заполняем второй слой карты тайлами с камнями (rocks). Позиция тайла соответствует координатам x y z, где z - номер тайлового слоя (tile layer). Указывая в качестве z-координаты 1, мы размещаем тайл с камнями на втором слое карты.

Источники


1. Barron T. Strategy Game Programming with DirectX 9.0. - Wordware Publishing Inc, 2003


Последние изменения страницы Четверг 23 / Июнь, 2022 14:04:56 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++