|
DELPHI X TUTORIAL 2
Косвенный перевод ознакомительного курса, написанного Dominique Louis.
e-mail: Dominique@SavageSoftware.com.au
Выполнил: Прядкин Дмитрий.
e-mail: polox@chel.ru
В этой части данного руководства речь пойдет об изометрических проекциях и о различных способах их изображения, применительно к играм.
Если вы решились прочитать сей документ до конца, то, вероятно, обнаружите такую вот структуру:
1. Вступление
2. Терминология
3. Концепции (объяснения основных моментов)
4. Практический пример
Изометрические проекции также называют аксонометрическими. Следующие два скриншота из игр Age of Wonders и Miniverse помогут вам лучше понять, что же представляет из себя изометрическая проекция:

[Age of Wonders (написана на Delphi3)]

[Miniverse (Delphi+DelphiX)]
Если вы не знакомы с “клеточной графикой”, то прочтите следующий абзац.
Итак, в играх с изометрическим типом проекции используется клеточная графика. Вообще, грубо говоря, клетка в таких играх чуть-чуть напоминает клетку, которой выложена ванная комната во многих квартирах.
В идеале, одна клетка – ничто, но если клетки соединить воедино, то они уже будут представлять собой какое-то собрание клеток. Так например, если взять клетку, которая содержит изображение кирпича и потом соединить ее с несколькими такими же, то мы получим стену из кирпичей, абсолютно одинаковых.
Трехмерные изометрические пространства – движки, использующие метод трехмерного проектирования, являются относительно новыми, и, как правило, базируются на полигонах (проект S.A.G.E. от Auran). Некоторые используют воксельные технологии (Tiberian Sun от WestWood, Hexplore). Такие движки позволяют вытворять с графическими данными очень многое – от масштабирования до вращения. Можно еще заметить, что качество изображения приближается к качеству, которое можно получить при использовании заранее прорисованных объектов (спрайтов); и это очень хорошо, ведь еще недавно, даже с трехмерным графическим акселератором, движки с трехмерной технологией не давали превосходного качества изображения.
Векторные изометрические пространства – такая технология использована в игре Miniverse. Эта штука основывается на векторах, соответствующим образом прикрепленных к полностью заранее прорисованному графическому объекту (см. картинки).

(Векторный редактор MiniVerse)

(Векторный регион)
Изометрические движки с квадратными клетками – игры Hatfiller&MacCoy и Baldur’s Gates используют как раз такую технологию. Что тут говорить – когда используются квадратные клетки, то значительно упрощается их компоновка и прорисовка на карте. Кстати, помимо простоты отрисовки клеток, чисто визуальные качества карты, составленной по этой технологии, очень впечатляет глаз.
Изометрические движки на шестигранных клетках – такие технологии очень подходят для игр типа RPG, таких, как Age of Wonders, Hexplore:

[Редактор карт для Age of Wonders]
Примечание: изометрические технологии на шестигранных клетках по праву относят к “традиционным” технологиям, ибо используются уже давно во многих играх, начиная от стратегии The Tanks (1993г.).
Визуально, каждая клетка в подобных движках выглядит как шестигранник (отсюда и название) и имеет 6 сторон. (см. след. рисунок)

(стандартная шестигранная клетка)
Давайте зададим размеры этой клетки: ширина=50 пикселей, высота=32 пикселя. Тогда алгоритм для обработки подобных клеток будет примерно таким…
Для первого ряда карты
Первая клетка будет отрисована на карте в позиции (0,0)
Вторая – в позиции (70,0). Почему в данном случае координата х=70???, - если вы зададите этот вопрос, то ответ на него кроется в структуре клетки, ведь это шестигранник… Кроме того, это нужно, чтобы карта у нас рисовалась правильно, без пробелов.
Для второго ряда карты
Первая клетка отрисовывается на карте в позиции (-35,16). Такие координаты клетки опять же объясняются структурой нашей карты, т.е. чтобы не допустить пробелов. Попробуйте сами начертить на листке бумаги примерную карту из 3х рядов. У вас должно получиться что-то напоминающее структуру пшеничного колоса, но очень отдаленно :)
А вот что получилось у меня (я создал карту из семи рядов). При этом ядовито-розовым цветом я обозначил невидимые (transparent) части карты, которые не должны быть отрисованы на экране в процессе игры.

PS: Вы можете и не выделять ребра клеток. В моем примере это сделано просто так.
Традиционные изометрические движки – практически все, что можно сказать об этом виде движков, можно применить и к вышеописанному типу, с шестигранными клетками.

[Siege of Avalon]
Если более или менее серьезно продумать структуру такого вот движка, то получится, что он будет обладать качеством (очень хорошим), сравнимым с движками на квадратных клетках. Причем данная технология требует гораздо меньше памяти для хранения графических данных и наделяет такой важной возможностью, как генерация произвольных карт, как это сделано в Diablo.
Можно также, затратив сравнительно небольшое количество ресурсов, поворачивать карту наподобие того, как это делают программисты в 3D-играх.
Как уже было сказано, такая технология не требует большого количества памяти для хранения данных. Это обеспечивается тем, что практически каждая клетка может быть использована несколько раз при генерации карты: например, уже совсем не необходимо рисовать несколько раз дерево, выросшее на земле. Вместо этого достаточно просто нарисовать клетку с землей, а другую клетку – с деревом. Продолжая эту идею, можно “вырастить” целый лес деревьев, используя минимальное число клеток.
Клетки в традиционных изометрических движках имеют прямоугольную (не обязательно квадратную) форму. Размеры выбираются из соотношения 2:1, или 2.1:1, это значит, что длина клетки примерно в два раза больше ее высоты. В примере, прилагаемом к этой части, будет использоваться соотношение сторон 2.1:1, т.е. клетка будет иметь по размеры 32 пикселя по горизонтали и 15 по вертикали. Конечно, в своих примерах вы можете использовать сколь угодно бОльшие или мАлые по размерам клетки, но, поверьте, соотношение длин сторон для базовой клетки – важная вещь.
PS: Я использовал термин базовой клетки исключительно только для
того, чтобы вас не вводить в заблуждение, ибо базовыми клетками обычно пользуются при отрисовки земли, а уже объекты, которые должны на ней находиться (например то же дерево) могут и не иметь такой же высоты, как и базовая клетка; но при этом они должны иметь такую же длину.
Теперь об одной тонкости подготовки карты. Дело в том, что единственно возможным способом рисовать объекты является клетка, имеющая прямоугольную (в нашем случае) форму. Что же делать, если необходимо нарисовать…алмаз, расположенный, скажем, на траве. А вот что:
нарисовать эту траву с помощью базовых клеток
потом с помощью другой клетки нарисовать наш алмаз, который и будет лежать на траве.
Но, не все так просто. Ведь алмаз же не занимает целую клетку – он не прямоугольный. Следовательно, если оставить все как есть и нарисовать алмаз, то получится довольно неприятная картина: та часть фона, которая не занята алмазом, будет зиять на нашей карте, тем самым портить ее.
Очевидное решение заключается во введении областей с невидимым (transparent) цветом, который не будет отрисовываться. В нашем случае таким цветом является “ядовито-розовый” или Fucsia.
Теперь договоримся о форме и размере клетки. Конечно, этот абзац можно было бы и не включать, но вдруг кто-нибудь не поймет, что речь идет не про обычную плоскость, а про изометрическую проекцию.
Итак, раз уж мы выбрали изометрию (аксонометрию), то вполне понятно, что прямоугольник(а следовательно, и базовая клетка) в данной проекции будет выглядеть так:

Тогда при отрисовке карты нужно обеспечить корректную работу с таким видом клеток. На следующей картинке вы увидите, как не надо делать карту:

А на следующей картинке будет показано, как нужно делать:

Чтобы этого добиться, нам, опять же, как и в случае с шестигранными клетками, нужно придумать несложный алгоритм рисования:
Для нечетных рядов
Первая клетка рисуется в позиции х=0, вторая – в позиции х=32 и т.д. PS: 32 – это, если вы забыли, длина клетки, как мы условились ее считать.
Для четных рядов
Первая клетка рисуется в позиции х=-16, вторая – х=16
Позиция по у будет увеличиваться не на 15, а только на 8. Почему – разберитесь сами, ведь у вас перед глазами рисунок, да и начертить на бумаге вы можете и сами, что будет несомненно более удачно.
Все, на этом теоретическая часть закончена. Переходим к практической.
Итак, приступим.
Что же нам нужно? А вот что:
Run-time версии библиотеки DirectX (что-нибудь большее, чем версия 6.1)
Delphi 3, 4 или 5
Компоненты DelphiX, настроенные для соответствующей версии Delphi.
Начнем.
Давайте создадим новый проект в ИСР(IDE) Дельфи. Давайте кинем компонент TDXDraw на нашу форму и установим его свойства:
Align : alClient
AutoInitialize : True
Display : 640x480x8
Options : [doAllowReboot,doWaitVBlank,doCenter,doFlip]
Заметьте, что в поле Options мы не будем пока ставить FullScreen, ибо отлаживать программу, особенно игровую, лучше всего в оконном режиме. А уже только потом, когда все у нас заработает, мы, если захотим, перейдем в полноэкранный режим.
Теперь перебросим компонент TDXTimer на нашу форму и установим его свойства:
Enabled : False
Interval : 0
Раз уж мы используем таймер, то он нам зачем-то нужен. Давайте напишем пару процедур для обработки событий этого таймера:
Это для инициализации таймера
procedure TMainForm.DXDrawInitialize(Sender: TObject);
begin
DXTimer.Enabled := True;
end;
А это для высвобождения таймера, при выходе программы
procedure TMainForm.DXDrawFinalize(Sender: TObject);
begin
DXTimer.Enabled := False;
end;
Карты.
Вообще-то для того, чтобы нам что-нибудь отобразить, нужна структура карты, также нужно придумать функцию отрисовки карты.
Итак, давайте использовать массив 32х32 для нашей карты. Причем нам нужно сделать так, чтобы карта состояла из нескольких слоев клеток… давайте программировать…
1) Это тип клетки
type
TMapTile = record
Num : Byte; // количество клеток в данной позиции
Tile : array[ 0..9 ] of Byte; // тип клетки (0 = трава, 1 = дерево и т.д.)
Height : array[ 0..9 ] of Byte; // какой высоты достигает клетка
Layer : array[ 0..9 ] of Byte; // используется для уверенности в корректности отрисоки спрайтов
Walkable : boolean; // идентифицирует перемещаемость клетки
end;
1) А это массив для нашей карты
TMap = array[0..31, 0..31] of TMapTile;
Давайте использовать четыре различных типа объектов при построении нашей карты:
трава
стена
высокая стена
дерево
Теперь осталось определить, как будут выглядеть сами объекты. Ну, клетка с травой будет размером 16х15, стену сделаем 16х50. В силу нашей модели, определенной в TMapTile, высокая стена будет состоять из двух маленьких стен, нарисованных одна поверх другой.
Теперь осталось заполнить карту. Сделаем это.
А это массив для нашей карты
case tempbuffer[ y ][ x ] of
'0' : // травка
begin
Map[ y ][ x ].Num := 1;
Map[ y ][ x ].Tile[0] := 0;
Map[ y ][ x ].Height[ 0 ] := 0;
Map[ y ][ x ].Layer[ 0 ] := 0;
Map[ y ][ x ].Walkable := true;
end;
'1' : //стенка
begin
Map[ y ][ x ].Num := 2;
Map[ y ][ x ].Tile[ 0 ] := 1;
Map[ y ][ x ].Height[ 0 ] := 0;
Map[ y ][ x ].Layer[ 0 ] := 0;
Map[ y ][ x ].Walkable := false;
end;
'2' : //высокая стенка
begin
Map[ y ][ x ].Num := 2;
Map[ y ][ x ].Tile[ 0 ] := 2;
Map[ y ][ x ].Height[ 0 ] := 0;
Map[ y ][ x ].Layer[ 0 ] := 0;
Map[ y ][ x ].Walkable := false;
end;
'3' : //деревце
begin
Map[ y ][ x ].Tile[ 0 ] := 2;
Map[ y ][ x ].Height[ 0 ] :=0;
Map[ y ][ x ].Layer[ 0 ] := 0;
Map[ y ][ x ].Tile[ 1 ] := 15;
Map[ y ][ x ].Height[ 1 ] := 28;
Map[ y ][ x ].Layer[ 1 ] := 1;
Map[ y ][ x ].Walkable := false;
end;
end;
Общий наш цикл прорисовки будет проходить через каждый массив карты, будет использоваться переменная Num для установления количества клеток, которые будут рисоваться в заданной (х,у) позиции.
Заметьте, что каждая все клетки (как в нашем случае) могут и не иметь абсолютно одинаковых размеров, следовательно, нужно обеспечить обработку позиции. Итак, рассмотрим тип TMapTile:
type
TMapTile = record
Num : Byte; // количество клеток в данной позиции
Tile : array[ 0..9 ] of Byte; // тип клетки (0 = трава, 1 = дерево и т.д.)
Height : array[ 0..9 ] of Byte; // какой высоты достигает клетка
Layer : array[ 0..9 ] of Byte; // используется для уверенности в корректности отрисоки спрайтов
Walkable : boolean; // идентифицирует перемещаемость клетки
end;
Сейчас я объясню его значение, по крайней мере постараюсь, чтобы все это выглядело понятно:
Из переменных этого типа состоит вся наша карта. При этом задумывалось так, что каждая клетка на карте может иметь разный тип, разную длину и лежать в разных слоях…
Итак, когда карту мы начинаем рисовать, изображаем конструкцию цикла for, который будет проходить по всем без исключения элементам карты. Допустим, в данный момент времени обрабатывается данный элемент типа TMapTile. Первым делом нам нужно посмотреть на переменную Num и установить, сколько элементов содержит обрабатываемая в данный момент времени клетка. Потом нам нужно соответствующим образом нарисовать эту клетку, для этого нужны все остальные переменные, вернее, массивы переменных.
Рассмотрим частный случай. Пусть в данной клетке содержится три элемента. Тогда порядок будет следующим:
1. Идентифицируем тип первого элемента (Tile[0])
2. Смотрим, какой высоты достигает этот элемент (Height[0])
3. Смотрим, когда ее нужно рисовать (Layer[0])
А) Если значение Layer[0]=0, то значит просто рисуем этот элемент первым, т.к. он лежит в самом первом слое.
Б) Если значение Layer[0] не равно нулю, то следует нарисовать сначала тот элемент, у которого это значение равно нулю, в противном случае может получиться например так, что дерево растет под землей…;-/
PS: Лично я рекомендую для избавления от излишних проверок располагать все элементы клетки в порядке их рисования, т.е. элемент с землей будет иметь индекс 0, элемент с деревом – индекс 1 и так далее. Если так организовать, то необходимость в элементе Layer отпадет сама собой.
4. Идем к шагу 1, только уже смотрим не на первый элемент, а на второй (т.е. на Tile[1], Height[1], Layer[1])
Примечание: Все элементы сложных объектов, таких, как “Высокая стена”, должны находиться в одном слое в силу того, что они считаются за один объект.
Если вам это кажется трудным…то попробуйте своими силами прийти к этим выводам.
Вообщем теперь осталось привязать установить соответствие между координатами клетки на карте и на экране (сделайте это сами).
Если у вас возникли трудности – заглядывайте в исходный код, который прилагается.
|