Galaxy2D Tutorials Галактика 2D
Изометрическое представление.

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

 

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

 

Библиотека для создания изометрии и пример программы находится в ISO_SRC.ZIP. В архиве также содержит несколько библиотек, чтобы Вы могли компилировать его или с BORLAND или с WATCOM. (См. 'library.txt' в LIBRARY.ZIP).

 

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

 

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

 

При изометрическом представлении вместо того, чтобы использовать прямоугольные клетки, используются ромбы. Когда Вы рисуете их, вместо координаты x идущей слева направо и y идущей сверху вниз, x теперь пойдет вниз-вправо, а y пойдет вниз-лево. На карте по прежнему слева направо - x, сверху вниз - y. Смотрите: (x и y - координаты карты)

 

 

 

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

 

 

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

 

Хороший угол для просмотра будет при соотношении 2:1. Это означает для каждых двух горизонтальных выведенных пикселей, выводиться один вертикальный пиксель. Мы будем фактически использовать 2.1:1. Ширина нашей клетки будет 32, тогда высота 32/2.1 = 15.23. Поэтому размер клетки - 32x15.

 

Это - 'основной' размер клетки с высотой 1. Вспомните, что наши клетки могут иметь разные высоты. Потому что стенка может быть 32x90. Высота может меняться, но ширина должна остаться равной 32.

 

Давайте посмотрим на форму клетки (в пикселях):

 

 

                      1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3

    1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2

   ----------------------------------------------------------------

 1|                             O O O O

 2|                         O O O O O O O O

 3|                     O O O O O O O O O O O O

 4|                 O O O O O O O O O O O O O O O O

 5|             O O O O O O O O O O O O O O O O O O O O

 6|         O O O O O O O O O O O O O O O O O O O O O O O O

 7|     O O O O O O O O O O O O O O O O O O O O O O O O O O O O

 8| O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O

 9|     O O O O O O O O O O O O O O O O O O O O O O O O O O O O

10|         O O O O O O O O O O O O O O O O O O O O O O O O

11|             O O O O O O O O O O O O O O O O O O O O

12|                 O O O O O O O O O O O O O O O O

13|                     O O O O O O O O O O O O

14|                         O O O O O O O O

15|                             O O O O

 

Если Вы немного поработаете с этой формой, то вы обратите внимание на то, что ее просто состыковывать вместе. Рисуйте первую клетку, сдвиньтесь вправо на 16 пикселей, вниз на 8, и рисуйте другую клетку. Но как узнать какую клетку, где рисовать? Посмотрите на экран снова, увеличим его и добавим координаты x и y  (верхнее число - x, нижнее - y):

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

 

Отлично, но есть небольшое отличие. Когда мы рисуем слева направо, наши клетки сдвинуты на 32 пикселя друг от друга. Как только мы смещаемся сверху вниз, мы смещаемся только на 8 пикселей за раз. В начале всех других строк, мы сдвигаем на 16 пикселей влево для того чтобы подогнать ромбические клетки из разных строк.

 

Поэтому мы будем рисовать экран примерно вот так (числа задают порядок, в котором клетки должны рисоваться):

Поскольку некоторые из клеток надо 'обрезать' по краям экрана, мы отсекаем их. Обратите внимание на каждую нечетную строчку, мы рисуем там, на одну клетку больше. Это потому что клетки сдвинуты влево, и мы должны компенсировать этот сдвиг.

 

Поняли это? Рисовать все это довольно просто, но мы, конечно, не можем обновить координаты карты этим способом (мы не можем указать какие координаты  x/y карты будут соответствовать данным клеткам).  Как это сделать? Хорошо, еще раз взглянем на рисунок с координатами карты. Понаблюдаем за x и y координатами, при перемещении их вправо. Вы видите, что x увеличивается на один, и y уменьшается на один при сдвиге на каждую клетку. При проходе сверху вниз по вертикали это немного изменится, поскольку мы переходим на другую строку.

 

Тем самым мы должны увеличить x и y при переходе с одной вертикальной линии клеток на другую. Это означает то, что если линия четная, то увеличивается x координата карты, а затем сдвигаемся вниз. Если линия нечетная, то увеличивается y координата карты, затем сдвигаемся вниз.

 

Теперь мы знаем, как рисовать экран и как проследить координаты карты для каждой выведенной клетки. Теперь посмотрим, как это будет работать в программе.

 

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

 

Мы будем использовать массив карты 10х10. При этом на каждую клетку можно будет наносить клетки, с различной высотой используя стек. Это даст нам возможность объединять графические клетки, чтобы создать новые.

 

Например, мы хотим стенку и стенку со свечой. Вместо создания двух разных клеток стенки, мы создаем одну стенку и одну свечку. Теперь Вы только рисуете стенку, затем рисуете свечу на верхней части ее.

 

Вот определение на Си массива для хранения различных клеток и высот:

 

struct MAP_STRUCTURE {

  char num_tiles;

  char tiles[10];  // максимум 10 клеток на одну точку карты

  char height[10]; // также максимум 10           

};

 

И массив нашей карты:

 

MAP_STRUCTURE map[10][10];

 

Мы хотим иметь три различных объекта на карте:

 

0) Трава

1) Стенка

2) Высокая стенка

 

Посмотрим на карту для нашего примера:

 

  0 1 2 3 4 5 6 7 8 9

0 O O O O O O O O O O

1 O . . . . . . . . O

2 O . . . . . . . . O           . = трава      (0)

3 O . . o o o o . . O           o = стена      (1)

4 O . . o . . o . . O           O = высокая стена (2)

5 O . . o . . o . . O

6 O . . o o o o . . O

7 O . . . . . . . . O

8 O . . . . . . . . O

9 O O O O O O O O O O

 

Теперь мы должны задать настройки для разных типов клеток. Отлично, трава - это клетка 32x15, выведенная с шаблоном травы. Стена - клетка 32x50. Так как мы собираемся использовать стек при рисовании, высокая стена будет состоять из двух стенок, выведенных одна над другой.

 

Теперь мы помещаем данные в наш массив карты (клетка 0 = трава, клетка 1 = стенка) вот так:

 

map[0][0].num = 2;

map[0][0].tile[0] = 1;

map[0][0].height[0] = 0;

map[0][0].tile[1] = 1;

map[0][0].height[1] = 50;

...

map[1][1].num = 1;

map[1][1].tile[0] = 0;

map[1][1].height[0] = 0;

...

 

Вы поняли идею? Наш цикл рисования будет проходить по каждой клетке карты, используя num для рисования клеток на ней.

 

Также, поскольку каждая клетка может иметь разный размер и по высоте и по ширине, то мы должны создать базисную точку, которая одинакова для всех клеток. Точка нижнего правого угла прекрасно бы для этого подошла. Надо только вычесть ширину и высоту экрана из x и у позиции перед рисованием клетки.

 

На Си, это выглядело примерно так:

 

( ПРИМЕЧАНИЕ: Это - не Изометрический метод рисования. Только пример

        для понимания метода рисования со стеком.)

 

for(i=0;i<10;i++) {

  for(j=0;j<10;j++) {

    for(k=0;k<map[i][j].num;k++) {

      tile_to_draw = map[i][j].tile[k];

      height_to_draw = map[i][j].tile[k];

      width = block_width(tile_to_draw);

      height = block_height(tile_to_draw);

      block_draw(tile_to_draw,x-width,y-height-height_to_draw);

    }

  }

}

 

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

 

Чтобы нарисовать Изометрическую карту, делайте так:

 

Начните с начальной позиции на экране x=8 y=16. Не забудьте вычесть ширину и высоту блока из этих координат прежде, чем Вы нарисуете клетку.

 

Координаты карты передаются функции рисования как левый верхний угол клетки, которая будет нарисована. Мы теперь начинаем цикл для каждой вертикальной линии клеток, которая будет нарисована. На экране 320x200 можно нарисовать 25 клеток вертикально. Мы фактически будем рисовать больше клеток, так как высокие блоки будут отсекаться, поэтому мы будем рисовать приблизительно 35 вертикальных клеток ( добавка идет снизу, если объекты высокие ).

 

Теперь инициализируем некоторые временные координаты для хранения координат карты. Теперь проверим, если строка вывода клеток нечетная в вертикальном цикле, то произведем сдвиг координаты x экрана на 16 пикселей влево. Теперь начните цикл горизонтальной прорисовки. Имеются 12 клеток (если без половинок то 10 ), которые надо вывести поперек (13 для каждой четной вертикальной строки). Нарисуйте клетку для прорисовки карты.

 

Рисуйте клетку / клетки, используя метод со структурой и стеком, который описан выше. Переместите вправо на 32 пикселей, добавьте 1 к временным координатам карты X , вычтите 1 из временных координат Y. Закончите горизонтальный цикл. Теперь переместите вниз 8 на пикселей. Если это - четная вертикальная строка, добавьте единицу к координате x карты, иначе добавлять единицу к y. Закончите вертикальный цикл. Отлично, экран теперь выведен.

 

После того, как Вы получили рабочий движок, обратите внимание, что, когда Вы двигаетесь в любом направлении, то прыгаете на 32 или 16 пикселей. Нет плавной прокрутки. Хорошо, это ОЧЕНЬ просто исправить. Мы все еще имеем карту 10x10:

 

    -X-

    0123456789

| 0 ..........

Y 1 ..........

| 2 ..........

  3 ..........

  4 ..........

  5 ..........

  6 ..........

  7 ..........

  8 ..........

  9 ..........

 

Но теперь, мы увеличим размер внутри координат карты на 16x16:

 

        X0                X1

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

Y ................ ................

0 ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

  ................ ................

 

Карта все еще остается 10x10, но мы можем перемещать внутри клеток. Это дает нам диапазон 160x160. Мы назовем это точной системой координат.

 

Одно замечание: мы не можем работать с нечетными числами, при использовании таких координат, поскольку это создаст дерганое перемещение. Пробуйте это позже, и вы это поймете.

 

Теперь мы сделаем следующие изменения в нашем движке:

- Возьмем позицию x и y, которые мы хотим вывести как левый - верхний угол на экране. Они могут располагаться от 0-160. Назовем их - vx и vy.

- Делим эти числа на 16, чтобы получить координаты карты, на которых мы стоим. Мы назовем их mx и my:

  mx = vx / 16

  my = vy / 16

- Необходима в инициализация некоторых переменных для предварительного сдвига наших клеток, которые мы будем рисовать. Чтобы обеспечить эффект гладкой прокрутки, которые мы назовем prestep_x и prestep_y. Мы также нуждаемся в некоторых временных переменных, которые мы назовем x_off и y_off. Способ вычисления их:

   - Булевое "и" для vx и vy на 15: x_off = vx & 15  y_off = vy & 15

   - Мы вычисляем предварительный сдвиг:
     prestep_x = x_off - y_off
     prestep_y = (x_off / 2) + (y_off / 2)
     (эти функции зависят от вашего размера клетки, в нашем случае 32x15)

 

Мы теперь имеем наши координаты предварительного сдвига. Когда мы готовы рисовать клетку,  выводим изображение из карты, используя mx и my, мы делаем влево шаг на prestep_x пикселей и вверх на prestep_y пикселей.

 

Теперь мы имеем гладкую прокрутку, и мы хотим добавить рисование спрайта. Спрайт рисуется так же как фон, но спрайт может выводиться в любом месте, а не только в линии клеток. Спрайт может быть записан поверх другой клетки.

 

Это означает что спрайт, может двигаться внутри координат карты (16x16). Так на карте 10x10, спрайт имеет диапазон движения 160x160, а не 10x10.

 

Фактически диапазон перемещения 80x80, потому что спрайт должен перемещаться по четной границе. Пробуйте нарисовать его на нечетной, и движение становится судорожное, когда спрайт перемещается.

 

Поэтому скажем, мы имеем спрайт, который мы хотим нарисовать в 136,64. Сначала разделим эти числа на 16, чтобы получить истинные координаты карты. Затем получите смещение внутри координат выполнением операции & с 15:

 

map_x = sprite_x / 16;

map_y = sprite_y / 16;

off_x = sprite_x & 15;

off_y = sprite_y & 15;

 

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

 

В нашем примере - спрайт в 48,64 был бы выведен в координаты карты 8,4 со смещением x=8 и смещением y=0.  Поэтому, когда наша функция рисования достигает координаты карты 8,4, мы должны рисовать спрайт.

 

Когда мы рисуем спрайт, то мы будем рисовать его из правого нижнего угла. Но мы должны осуществить сдвиг влево на 32 пикселя и вверх на 16 пикселей. Это из-за смещения от верхнего левого угла. Далее добавьте смещение, и рисуйте (также не забудьте скорректировать координаты для изометрического просмотра, смотри предварительный сдвиг выше):

 

xo = sprite_x & 15;

yo = sprite_y & 15;

off_x   = xo - yo;

off_y   = (xo/2) + (yo/2);

screen_x = current_draw_x - 32 + off_x - width;

screen_y = current_draw_y - 16 + off_y - height;

block_draw(sprite,screen_x,screeny);

 

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

 

struct MAP_STRUCTURE {

  char num_tiles;

  char tiles[10];  // максимум 10 клеток на координату

  char height[10]; // и высот также 10

 

  char num_sprites;

  char *sprite[10];

  signed short int sprite_xoff[10];

  signed short int sprite_yoff[10];

};

 

Обратите внимание, что введено ограничение на 10 спрайтов, но Вы можете изменить его, если Вам надо.

 

Поэтому, когда Вы рисуете карту, сбросьте все массивы спрайтов, чтобы не вывести лишних спрайтов:

 

map_clear_sprites()

{

  signed short int i,j;

 

  for(i=0;i<map_width;i++) {

    for(j=0;j<map_height;j++)

     map[i][j].num_sprites = 0;

  }

}

 

Затем надо добавить спрайт к списку для прорисовки следующего кадра:

 

sprite_add_to_list(char *sprite,signed short int x,signed short int y,char height)

{

  signed short int mx,my,xo,yo,offx,offy,sprite_num;

 

  mx = x / 16;

  my = y / 16;

 

  if(map[my][mx].num_sprites9)

    return;

 

  xo = x & 15;

  yo = y & 15;

  off_x   = xo - yo;

  off_y   = (xo/2) + (yo/2);

  sprite_num = map[my][mx].num_sprites;

  map[my][mx].sprite[sprite_num]        = sprite;

  map[my][mx].sprite_xoff[sprite_num]   = off_x - 32;// - block_width(sprite);

  map[my][mx].sprite_yoff[sprite_num]   = off_y - 16 - height;// - block_width(sprite);

  map[my][mx].num_sprites++;

}

 

Итак, движок будет рисовать и клетки и спрайты. Только добавьте xoff и yoff перед рисованием.

 

Вы можете добавить слои, так как движок знает порядок рисования клеток и спрайтов внутри координат карты. Это будет необходимо, так как спрайт может быть закрыт другими объектами, и быть невидимым.

 

Copyright © 1996 by Jim Adams, All right reserved.
Graphics Illustrations by Lennart Steinke (Sep 1997)

PMG  11 марта 2004 (c)  Сергей Анисимов