Как создаются изометрические миры. Растровая визуализация в изометрической проекции

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

Как нарисовать изометрию

Сегодняшние инструменты

Шаг 1.

Запускаем редактор inkscape, настройки нового документа не изменяем. Создадим изометрическую сетку для работы. Для этого отправляемся в Расширения-Отрисовка-Сетки-Изометрическая сетка и немного меняем настройки, увеличиваем число делений по Х и У до 10, делений между основными штрихами 3 и уменьшаем число разбиения делений до 1

Нажимаем применить, размещаем сетку ровно на документе

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

Ну вот, можно приступать.

Шаг 2.

Берем инструмент Рисовать кривые Безье и, кликая в узлы сетки, создаем первую стену

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

Очередь пола, тем же инструментом создаем пол

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

Шаг 3.

Добавим стол. Начнем с крышки стола. Уже привычным способом создаем прямоугольник по узлам сетки и размещаем у окна.

Обводку для стола делаем светлой, заливка любая темная похожая на цвет стола.

Теперь ножки- ножки делаем и двух элементов темного и светлого для создания объема. Линию создаем кривыми

Для придания нужной формы сначала оконтуриваем обводку (Контур-оконтурить обводку ) и, двигая узлы, придаем форму. Затем дублируем и меняем цвет на более темный, в результате имеем ножку стола. Группируем Ctrl+G

Дублируем 3 раза и размещаем под столом (Page Down ).

Шаг 4.

Следующий элемент полки на стене и кровать. Создаем кривыми три прямоугольника темного цвета и размещаем на стене

Цвет каждой полки должен отличаться друг от друга иначе полки сольются.

Кровать создаем из трех прямоугольников разного цвета

Добавляем белую подушку из трех прямоугольников

Можете повозиться для придания ей нужной «подушечной» формы.

Шаг 5.

Теперь шкаф. Очередные три прямоугольника для шкафа и два светлых для дверей

Цвет подбираем на свое усмотрение.

В заключении этого шага добавляем книги на стол и полки, и фиолетовое покрывало на кровать. Рисуем окно светло-голубого цвета и уменьшаем у него прозрачность.

Напомню, все делаем Кривыми Безье по изометрической сетке.

Шаг 6

Вешаем на стену картину желтого цвета с изображением цветка. Картину и цветок создаем кривыми. Перед кроватью коврик.

Для усиления эффекта объема добавляем тень под зеленый пол из размытого прямоугольника, тень под стол, и «землю» на которой мы «прикупили комнатку»

Ну и последнее. Для земли применяем пуантилизм (Фильтры-Рассеивание-пуантилизм), а для стен добавим толщину, пройдя кривой с обводкой в 2 пх толщиной белого цвета.

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

В Adobe illustrator есть 3 популярных способа работать с изометрией:

  1. Используя изометрическую сетку
  2. Методом SSR
  3. С помощью функции 3D

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

А. Изометрическая сетка

Изометрическая сетка служит исключительно вспомогательным инструментом.

Как создать:

I. С помощью инструмента Line Segment Tool строим линию под углом 30° c длиной не менее 2000 px

II. Теперь нам нужно создать для нее много копий. Открываем эффект Effect - Distort&Transformt- Transform , во вкладке Move параметры Horizontal и Vertical отвечают за расстояние между линиями, параметр Copies за количество копий (вот это поворот!)

III. Разбираем оформление Object - Expand Appereance

IV. Дублируем группу с нашими линиями и делаем их зеркальную копию Object - Transform - Reflect

V. Превращаем получившиеся линии в направляющие View - Guides - Make Guides

Примечание: в отличии от Фотошопа, функционал Иллюстратора позволяет создавать направляющие расположенные под углом

Сетка готова. Теперь с помощью инструмента Перо, можно рисовать по нашим направляющим фигуры.

Минусы: можно рисовать только простейшие фигуры прямоугольных форм. Не получится нарисовать фигуры эллипсоидной формы и фигуры сложной формы.

Однако изометрическая сетка нам пригодится при работе со следующим методом SSR, чтобы выравнивать по ней стороны фигуры.

Б. Scale Skew Rotate (SSR)

Преимущество SSR заключается в том, что можно рисовать объект в анфас, а потом переводить в изометрию.

Для этого нам нужны 3 инструмента: Масштаб (Scale Tool ), Наклон (Shear Tool ), Поворот (Rotate Tool ).

Значение параметра Scale по вертикали всегда остается неизменным 86,062% , а значения параметров Shear и Rotate выбираются в зависимости от того, какую грань изображаемого предмета мы переводим в изометрию (верхняя, левая или правая).

С помощью метода SSR можно одновременно перевести в изометрию большую группу маленьких объектов, например, клавиши на клавиатуре ноутбука.

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

Сделать в изометрии фигуру эллипсоидной формы:

Или фигуру сложной формы, которую невозможно сходу рисовать по сетке:

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

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

Минусы: методом SSR неудобно рисовать объекты, у которого скошенные (или скругленные) края и объекты сложной формы, которые выгоднее делать через функцию 3D.

В. 3D

В этом случае нам поможет функция 3D, которая, к счастью, имеется в функционале Иллюстратора.

Порядок действий:

I. Рисуем переднюю часть объекта в анфас

II. Применяем эффект Effect - 3D - Extrude&Bevel

III. В параметре Position выбираем значение Isometric Right или Left (Top и Bottom нам не нужны, т.к. в таком ракурсе можно спокойно нарисовать фигуру с помощью SSR).

IV. Разбираем оформление, удаляем все лишние и на выходе получаем готовую к использованию фигуру.

Кроме того, это самый быстрый способ нарисовать примитив в изометрии: всего пара кликов и фигура готова. Если бы мы делали это методом SSR - пришлось бы немного повозиться.

По сути 3D является полноценной замены метод SSR и проигрывает второму лишь в частных случаях (например, при попытке перевести в изометрию клавиатуру от ноутбука).

Выводы:

  1. Универсального способа рисовать изометрию - НЕТ.
  2. Эти способы не единственные, но они подходят под большинство практических задач

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

В следующем уроке я расскажу о композиции в изометрии, рассмотрюсамые распространенные косяки и покажу процесс построения сложного объекта.

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

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

Только помни: нельзя попробовать стать графическом дизайнером. Надо либо окунаться в это с головой, либо заниматься чем-то попроще. Если ты все для себя решил и хочешь максимально быстрого прогресса - 




Что мы будем создавать

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

В этом туториале мы подробнее рассмотрим сортировку по глубине для изометрических уровней, потому что добавим ещё и подвижные платформы. Этот туториал - не введение в теорию изометрии и не посвящён коду. В нём мы будем разбираться в логике и теории, а не анализировать код. В качестве инструмента используется Unity, поэтому сортировка по глубине сводится к изменению sortingOrder спрайтов. В других фреймворках она может являться изменением порядка по оси Z или последовательности отрисовки.

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

1. Уровни без движения

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

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

Рассмотрим следующий изометрический уровень со всего двумя строками и семью столбцами.


Числа на тайлах соответствуют их порядку сортировки (sortingOrder), или глубине, или порядку по Z, т.е. порядку, в котором их необходимо отрисовывать. В таком случае мы сначала отрисовываем все столбцы первой строки, начиная с первого столбца с sortingOrder = 1.

После отрисовки всех столбцов первой строки ближайший к камере столбец имеет sortingOrder = 7, и мы переходим к следующей строке. То есть каждый элемент во второй строке будет иметь более высокое значение sortingOrder , чем любой элемент в первой строке.

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

Что касается кода, то в нём просто выполняется циклический обход строк и столбцов массива уровня и последовательное назначение sortingOrder в увеличивающемся порядке. Результат не испортится, даже если мы поменяем строки и столбцы местами, как видно на рисунке ниже.


Здесь перед переходом к следующей строке мы сначала полностью отрисовываем столбец. Восприятие глубины остаётся тем же. То есть логика статичного уровня заключается в отрисовке или полной строки, или полного столбца, переходе к следующему и последовательному присвоению sortingOrder в увеличивающемся порядке.

Добавление высоты

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

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


Естественно, что любой тайл на более высоком этаже будет иметь бОльший sortingOrder , чем любой тайл на нижнем. Что касается кода, для добавления верхних этажей нам достаточно смещать значение y экранных координат для тайла в зависимости занимаемого им этажа.

Float floorHeight=tileSize/2.2f; float currentFloorHeight=floorHeight*floorLevel; // tmpPos=GetScreenPointFromLevelIndices(i,j); tmpPos.y+=currentFloorHeight; tile.transform.position=tmpPos;
Значение floorHeight указывает на воспринимаемую высоту изображения тайлового изометрического блока, а floorLevel определяет, какому этажу принадлежит тайл.

2. Движение тайлов по оси X

Сортировка по глубине в статичных изометрических уровнях не так сложна, правда? Давайте двигаться дальше - мы воспользуемся способом «сначала строки», то есть будем сначала назначать sortingOrder полностью первой строке, а затем переходить к следующей. Давайте рассмотрим первый движущийся тайл или платформу, которая движется по единственной оси X.

Когда я говорю, что движение происходит по оси X, то имею в виду декартову, а не изометрическую систему координат. Давайте рассмотрим уровень только с нижним этажом, состоящий из трёх строк и семи столбцов. Будем считать, что во второй строке есть только один тайл, который движется. Уровень будет выглядеть так, как показано на изображении ниже.


Тёмный тайл - это наш подвижный тайл, а его sortingOrder будет равен 8, потому что в первой строке 7 тайлов. Если тайл движется по декартовой оси X, то он будет двигаться по «колее» между двумя строками. По всем позициям, которые он может занимать на своём пути, тайлы в строке 1 будут иметь меньший sortingOrder .

Аналогично, все тайлы в строке 2 будут иметь бОльшее значение sortingOrder , вне зависимости от положения тёмного тайла на его пути. Так как для назначения sortingOrder мы выбрали способ «сначала строки», то для движения по оси X нам не нужно делать ничего лишнего. Этот случай довольно прост.

3. Движение тайлов по оси Y

Проблемы начинают возникать, когда мы принимаемся за ось Y. Давайте представим уровень, в котором наш тёмный тайл движется по прямоугольной колее, как показано на рисунке ниже. Можно увидеть ту же ситуацию в сцене Unity MovingSortingProblem в исходниках .


Используя наш подход «сначала строки», мы можем присвоить sortingOrder подвижному тайлу на основании строки, которую он занимает в текущий момент. Когда тайл находится между двумя строками, то ему назначается sortingOrder на основании строки, из которой он движется. В этом случае мы не можем следовать порядковому sortingOrder в строке, в которую он движется. Это разрушает наш алгоритм сортировки по глубине.

Сортировка по блокам

Чтобы решить эту проблему, нам нужно разделить уровень на различные блоки, одним среди которых является проблемный блок, разрушающий наш подход «сначала строки», а остальными - блоки, которые могут использовать подход «сначала строки» без нарушений. Чтобы лучше понять это, посмотрите на рисунок ниже.


Блок тайлов 2x2, обозначенный синей областью - это наш проблемный блок. Все остальные блоки могут использовать подход «сначала строки». Пусть рисунок вас не смущает - на нём показан уровень, который уже правильно отсортирован с помощью нашего блочного алгоритма. Синий блок состоит из двух столбцовых тайлов в строках, между которыми движется в текущий момент наш тёмный тайл, и из тайлов непосредственно слева от них.

Чтобы решить задачу глубины проблемного блока мы можем использовать только для этого блока подход «сначала столбцы». То есть для зелёного, розового и жёлтого блоков мы используем «сначала строки», а для синего - «сначала столбцы».

Следует учесть, что нам по-прежнему нужно последовательно назначать sortingOrder . Сначала зелёный блок, потом розовый блок слева, затем синий блок, потом розовый блок справа, и, наконец, жёлтый блок. При переходе к синему блоку, мы разбиваем порядок только для того, чтобы переключиться на способ «сначала столбцы».

В качестве альтернативного решения мы можем также рассмотреть блок 2x2 справа от столбца подвижного тайла. (Интересно то, что нам даже не нужно менять подходы, потому что разбиение на блоки в этом случае само решает нашу проблему.) Решение в действии показано в сцене BlockSort .


Этот алгоритм реализуется в следующем коде.

Private void DepthSort(){ Vector2 movingTilePos=GetLevelIndicesFromScreenPoint(movingGO.transform.position); int blockColStart=(int)movingTilePos.y; int blockRowStart=(int)movingTilePos.x; int depth=1; //сортировка строк до блока for (int i = 0; i < blockRowStart; i++) { for (int j = 0; j < cols; j++) { depth=AssignDepth(i,j,depth); } } //сортировка столбцов в той же строке до блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = 0; j < blockColStart; j++) { depth=AssignDepth(i,j,depth); } } //сортировка блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = blockColStart; j < blockColStart+2; j++) { if(movingTilePos.x==i&&movingTilePos.y==j){ SpriteRenderer sr=movingGO.GetComponent(); sr.sortingOrder=depth;//assign new depth depth++;//increment depth }else{ depth=AssignDepth(i,j,depth); } } } //сортировка столбцов в той же строке после блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = blockColStart+2; j < cols; j++) { depth=AssignDepth(i,j,depth); } } //сортировка строк после блока for (int i = blockRowStart+2; i < rows; i++) { for (int j = 0; j < cols; j++) { depth=AssignDepth(i,j,depth); } } }

4. Движение тайлов по оси Z

Движение по оси Z - это имитируемое движение по изометрическому уровню. В сущности, это просто движение по экранной оси Y. На изометрическом уровне с одним этажом для добавления движения по оси Z больше не нужно ничего делать с порядком, если вы уже реализовали описанный выше метод сортировки по блокам. Эту ситуацию в действии можно увидеть в сцене Unity SingleLayerWave , где к боковому движению по «колее» я добавил дополнительное волновое движение по оси Z.

Движение по Z на уровнях с несколькими этажами

Добавление на уровень новых этажей - это, как говорилось выше, всего лишь вопрос смещения экранной координаты Y. Если тайл не движется по оси Z, то не нужно делать ничего лишнего с сортировкой по глубине. Мы можем отсортировать по блокам первый этаж с движением, а затем ко всем последующим этажам применять сортировку «сначала строки». В действии эту ситуатцию можно посмотреть в сцене Unity BlockSortWithHeight .


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

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

Float whichFloor=(tileZOffset/floorHeight); float lower=Mathf.Floor(whichFloor);
Это значит, что lower и lower+1 и являются этажами, требующими особого подхода. Хитрость заключается в том, чтобы назначать sortingOrder обоим этим этажам вместе, как показано в представленном ниже коде. Это исправляет порядок и решает проблему с сортировкой по глубине.

If(floor==lower){ // нам нужно отсортировать нижний этаж и этаж непосредственно над ним вместе, за один проход depth=(floor*(rows*cols))+1; int nextFloor=floor+1; if(nextFloor>=totalFloors)nextFloor=floor; //сортировка строк до блока for (int i = 0; i < blockRowStart; i++) { for (int j = 0; j < cols; j++) { depth=AssignDepth(i,j,depth,floor); depth=AssignDepth(i,j,depth,nextFloor); } } //сортировка столбцов в той же строке до блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = 0; j < blockColStart; j++) { depth=AssignDepth(i,j,depth,floor); depth=AssignDepth(i,j,depth,nextFloor); } } //сортировка блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = blockColStart; j < blockColStart+2; j++) { if(movingTilePos.x==i&&movingTilePos.y==j){ SpriteRenderer sr=movingGO.GetComponent(); sr.sortingOrder=depth;//assign new depth depth++;//increment depth }else{ depth=AssignDepth(i,j,depth,floor); depth=AssignDepth(i,j,depth,nextFloor); } } } //сортировка столбцов в той же строке после блока for (int i = blockRowStart; i < blockRowStart+2; i++) { for (int j = blockColStart+2; j < cols; j++) { depth=AssignDepth(i,j,depth,floor); depth=AssignDepth(i,j,depth,nextFloor); } } //сортировка строк после блока for (int i = blockRowStart+2; i < rows; i++) { for (int j = 0; j < cols; j++) { depth=AssignDepth(i,j,depth,floor); depth=AssignDepth(i,j,depth,nextFloor); } } }
В сущности, мы рассматриваем два этажа как один и выполняем сортировку по блокам для этого единственного этажа. Посмотрите код в действии в сцене BlockSortWithHeightMovement . Благодаря этому подходу наш тайл теперь может свободно перемещаться по любой из двух осей, не разрушая глубину в сцене, как показано ниже.

Все мы играли в потрясающие изометрические игры , будь то первые Diablo, Age of Empires или Commandos. При первой встрече с изометрической игрой можно задаться вопросом: двухмерная она, трёхмерная или нечто совершенно другое. Сам мир изометрических игр обладает волшебной притягательностью для разработчиков. Давайте попробуем раскрыть тайну изометрической проекции и создадим простой изометрический уровень.

Для этого я решил использовать Phaser с кодом на JS. В результате у нас получится интерактивное приложение HTML5.

Учтите, что это не туториал по разработке на Phaser, мы просто используем его для удобного ознакомления с базовыми концепциями создания изометрической сцены. Кроме того, в Phaser есть простые способы создания изометрического контента, например, Phaser Isometric Plugin .

Для упрощения при создании сцены мы будем использовать тайлы.

1. Игры на основе тайлов

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

Чаще всего в тайловых играх используется вид сверху или сбоку . Давайте представим стандартный двухмерный вид сверху с двумя тайлами - тайлом травы и тайлом стены , показанными на рисунке:

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

[ , , , , , ]

Здесь 0 обозначает тайл травы, а 1 - тайл стены. Расположив тайлы согласно данным уровня, мы создадим огороженную лужайку, показанную на рисунке:


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

[ , , , , , ]

См. изображение ниже, где я пометил тайлы числами, соответствующими их значениям в данных уровня:


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

Если использовать показанные выше изображения тайлов с равной шириной и высотой, то у нас получатся одинаковые размерности уровня. Если ширина и высота тайла в нашем примере равны 50 пикселям, то общий размер уровня составит 300 на 300 пикселей.

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

2. Изометрическая проекция

Лучшее техническое объяснение изометрической проекции , как мне кажется, дано в этой статье Клинта Белленджера :

Мы наклоняем камеру по двум осям (поворачиваем камеру на 45 градусов вбок, потом на 30 градусов вниз). При этом создаётся ромбическая сетка, в которой ширина ячеек в два раза больше высоты. Такой стиль стал популярным благодаря стратегическим играм и экшн-RPG. Если посмотреть в этом виде на куб, то мы видим три его стороны (верхнюю и две боковые).

Хотя это звучит немного сложно, реализация такого вида довольно проста. Нам нужно разобраться в том, как соотносятся двухмерное и изометрическое пространства, то есть понять связь между данными уровня и видом. Нам нужно преобразовать декартовы координаты вида сверху в изометрические координаты. На рисунке ниже показано графическое преобразование:


Размещение изометрических тайлов

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

Исходная функция просто отрисовывает изображения тайлов в переданных ей x и y , а для изометрического вида нам нужно вычислить соответствующие изометрические координаты. Уравнения для этого представлены ниже. isoX и isoY обозначают изометрические координаты X и Y, а cartX и cartY - декартовы координаты X и Y:

//Преобразование из декартовых в изометрические координаты: isoX = cartX - cartY; isoY = (cartX + cartY) / 2; //Преобразование из изометрических в декартовы координаты: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2;

Да, вот и всё. Эти простые уравнения творят магию изометрического проецирования. Вот вспомогательные функции Phaser, которые можно использовать для преобразования из одной системы в другую с помощью очень удобного класса Point:

Function cartesianToIsometric(cartPt){ var tempPt=new Phaser.Point(); tempPt.x=cartPt.x-cartPt.y; tempPt.y=(cartPt.x+cartPt.y)/2; return (tempPt); } function isometricToCartesian(isoPt){ var tempPt=new Phaser.Point(); tempPt.x=(2*isoPt.y+isoPt.x)/2; tempPt.y=(2*isoPt.y-isoPt.x)/2; return (tempPt); }

Итак, мы можем использовать вспомогательный метод cartesianToIsometric для преобразования входных 2D-координат в изометрические внутри метода placeTile . За исключением этого, код отображения остаётся тем же, но нам нужно создать новые картинки тайлов. Мы не можем использовать старые квадратные тайлы из вида сверху. На рисунке ниже показаны новые изометрические тайлы травы и стен вместе с готовым изометрическим уровнем:


Невероятно, правда? Давайте посмотрим, как обычное двухмерное положение преобразуется в изометрическое:

2D point = ; // изометрическая точка вычисляется следующим образом isoX = 100 - 100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso point == ;

То есть входные данные преобразуются в , а - в .

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

Function getTileCoordinates(cartPt, tileHeight){ var tempPt=new Phaser.Point(); tempPt.x=Math.floor(cartPt.x/tileHeight); tempPt.y=Math.floor(cartPt.y/tileHeight); return(tempPt); }

(Здесь мы предполагаем, что высота и ширина тайла одинаковы, как и бывает в большинстве случаев.)

То есть, зная пару экранных (изометрических) координат, мы можем найти координаты тайла вызовом функции:

GetTileCoordinates(isometricToCartesian(screen point), tile height);

Эта точка на экране может быть, скажем, положением курсора мыши или подбираемого предмета.

Точки регистрации

Во Flash можно выбирать произвольные точки графики в качестве базовой точки или . Аналогом этого в Phaser является Pivot . Когда мы располагаем графику, скажем, в точке , то эта точка Pivot соответствует . По умолчанию или Pivot считается левая верхняя точка. Если вы попробуете создать приведённый выше уровень с помощью этого кода, то вы не получите нужного результата. Вместо этого у вас получится плоская земля без стен, как показано ниже:


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


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

  1. Сделать размеры изображений всех тайлов одинаковыми, и правильно расположить в изображении графику. При этом в каждом изображении тайла создаётся множество пустых областей.
  2. Вручную устанавливать базовую точку для каждого тайла, чтобы они располагались правильно.
  3. Отрисовывать тайлы с определённым смещением.

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

3. Движение в изометрических координатах

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

Y = y + speed; placetile(cartesianToIsometric(new Phaser.Point(x, y)))

Давайте подведём итог всем новым понятиям, которые мы изучили, и попробуем реализовать рабочий пример объекта, двигающегося в изометрическом мире. Можно использовать необходимые графические ресурсы из папки assets в репозитории исходного кода на git.

Сортировка по глубине

Если вы пробовали перемещать изображение мяча в огороженном саду, то заметили проблемы с сортировкой по глубине . Если в изометрическом мире есть подвижные элементы, то кроме обычного расположения, нам нужно позаботиться и о сортировке по глубине . Правильная сортировка гарантирует, что объекты, находящиеся ближе к экрану, будут отрисовываться поверх более далёких объектов. Как упомянуто в этой статье , простейший способ сортировки - использование декартовой координаты Y: чем выше объект на экране, тем раньше его следует отрисовывать. Это может неплохо работать для простых изометрических сцен, но лучше будет перерисовывать всю изометрическую сцену в процессе движения в соответствии с координатами тайла в массиве. Давайте я подробно объясню этот подход на примере псевдокода отрисовки уровня:

For (i, loop through rows) for (j, loop through columns) x = j * tile width y = i * tile height tileType = levelData[i][j] placetile(tileType, x, y)

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


Если мы будем выполнять цикл отрисовки в соответствии с псевдокодом, то сначала будет отрисована средняя угловая стена, затем цикл продолжит отрисовывать все стены в верхней правой части, пока не достигнет правого угла. В следующем цикле он отрисует стену слева от персонажа, а затем тайл травы, на котором стоит персонаж. Поскольку мы определили, что это тайл, который занимает персонаж, то отрисовываем персонажа после тайла травы. Таким образом, если бы на свободных трёх тайлах травы рядом с тайлом персонажа находились стены, то эти стены перекрывали персонажа, обеспечивая правильную сортировку по глубине.

4. Создание графики

Изометрическая графика может, но не обязана быть пиксель-артом. При работе с изометрическим пиксель-артом полезно изучить руководство RhysD , в котором содержится всё необходимое. Теорию можно изучить в Википедии .

При создании изометрической графики нужно соблюдать следующие правила:

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

Изометрические тайлы больше единичного размера тайла создают проблемы при сортировке по глубине. Такие проблемы рассмотрены в следующих статьях:

Посты по теме

5. Изометрические персонажи

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


Обратите внимание, что в игре с видом сверху при нажатии на клавишу вверх персонаж будет двигаться вертикально вверх, но в изометрической игре он переместится под углом в 45 градусов в сторону верхнего правого угла.

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

Для простоты понимания направления обычно обозначают как «север», «северо-запад», «запад» и так далее. В кадрах персонажа на рисунке показаны кадры неподвижного положения, начиная с юго-востока и по часовой стрелке:

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

Мы назначим две переменные, dX и dY , значение которых зависит от нажатых клавиш управления. По умолчанию эти переменные равны 0 , а значения им присваиваются согласно таблице внизу, где В, Н, П и Л означают, соответственно верхнюю , нижнюю , правую и левую клавиши направления. Значение 1 под клавишей означает, что клавиша нажата, 0 - что она не нажата.

Клавиша Положение В Н П Л dX dY ================ 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 -1 0 0 1 0 1 0 0 0 0 1 -1 0 1 0 1 0 1 1 1 0 0 1 -1 1 0 1 1 0 1 -1 0 1 0 1 -1 -1

Теперь с помощью значений dX и dY мы можем обновлять декартовы координаты следующим образом:

NewX = currentX + (dX * speed); newY = currentY + (dY * speed);

Итак, dX и dY представляют собой изменение положения персонажа по X и Y в зависимости от нажатых клавиш. Как сказано выше, мы легко можем вычислить новые изометрические координаты:

Iso = cartesianToIsometric(new Phaser.Point(newX, newY))

Получив новое изометрическое положение, мы должны переместить персонажа в это положение. На основании значений dX и dY мы можем понять, в каком направлении смотрит персонаж и использовать соответствующую анимацию. После перемещения персонажа не забывайте перерисовать уровень с соответствующей сортировкой по глубине, потому что тайловые координаты персонажа могут измениться.

Распознавание коллизий

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

Tile coordinate = getTileCoordinates(isometricToCartesian(current position), tile height); if (isWalkable(tile coordinate)) { moveCharacter(); } else { //ничего не делать; }

В функции isWalkable() мы проверяем, является ли значение массива данных уровня в заданной координате проходимым тайлом. Нам нужно также обновлять направление, в котором смотрит персонаж, даже если он не движется , на случай, если он столкнулся с непроходимым тайлом.

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

Сортировка по глубине с персонажами

Рассмотрим персонажа и тайл дерева в изометрическом мире, имеющие одинаковые размеры изображения , как бы неестественно это ни выглядело.

Чтобы хорошо разобраться в сортировке по глубине, нам нужно понять, что когда координаты X и Y персонажа меньше, чем у дерева, то дерево перекрывает персонажа. Когда координаты X и Y персонажа больше, чем у дерева, то персонаж перекрывает дерево. Когда их координаты X равны, то решение принимается только по координате Y: объект с большей координатой Y перекрывает другой. Если совпадают координаты Y, то решение принимается только по X: объект с большим X перекрывает другой.

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

6. Время для демо!

Играемая тенью роль очень важна, она добавляет реализма этой иллюзии. Кроме того, заметьте, что теперь мы используем две экранные координаты (x и y) для представления трёх измерений в изометрических координатах - ось Y в экранных координатах также является осью Z в изометрических координатах. Это может запутать.

10. Нахождение пути и движение по нему

Поиск пути и движение по нему - это довольно сложный процесс. Для нахождения пути между двумя точками существует множество разных решений с использованием различных алгоритмов, но поскольку levelData является двухмерным массивом, то всё гораздо проще, чем могло бы быть. У нас есть чётко заданные уникальные узлы, которые может занимать игрок, и мы можем легко проверить, можно ли по ним пройти.

Посты по теме

Подробный обзор алгоритмов поиска путей слишком велик для этой статьи, но я постараюсь объяснить наиболее распространённый способ: алгоритм кратчайшего пути, самыми известными реализациями которого являются A* и алгоритм Дейкстры.

Наша цель - найти узлы, соединяющие начальный узел с конечным. Из начального узла мы посещаем все восемь соседних узлов, и помечаем их как посещённые. Этот процесс рекурсивно повторяется для каждого нового посещённого узла.

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

Поиск пути

Глупо изобретать велосипед заново, если речь идёт о чётко описанных алгоритмах, поэтому для поиска пути мы будем использовать уже существующие решения. В Phaser нам потребуется решение на JavaScript, поэтому я выбрал EasyStarJS . Инициализация движка поиска пути выполняется следующим образом:

Easystar = new EasyStar.js(); easystar.setGrid(levelData); easystar.setAcceptableTiles(); easystar.enableDiagonals();// мы хотим, чтобы в пути были диагонали easystar.disableCornerCutting();// без диагональных путей при движении в углах стен

Поскольку в массиве levelData содержатся только 0 и 1 , мы можем сразу передать его в массив узлов. Значением 0 мы обозначили проходимый узел. Также мы включили возможность движения по диагонали, но отключили её, когда движение происходит рядом с углами непроходимых тайлов.

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

Мы будем распознавать нажатие мышью на любой свободный тайл в уровне и вычислять путь с помощью функции findPath . Callback-метод plotAndMove получает массив узлов созданного пути. Мы помечаем найденный путь на миникарте.

Game.input.activePointer.leftButton.onUp.add(findPath) function findPath(){ if(isFindingPath || isWalking)return; var pos=game.input.activePointer.position; var isoPt= new Phaser.Point(pos.x-borderOffset.x,pos.y-borderOffset.y); tapPos=isometricToCartesian(isoPt); tapPos.x-=tileWidth/2;//настройка для нахождения нужного тайла для ошибки из-за округления tapPos.y+=tileWidth/2; tapPos=getTileCoordinates(tapPos,tileWidth); if(tapPos.x>-1&&tapPos.y>-1&&tapPos.x<7&&tapPos.y<7){//нажатие мышью внутри сетки if(levelData!=1){//не тайл стены isFindingPath=true; //алгоритм делает своё дело easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove); easystar.calculate(); } } } function plotAndMove(newPath){ destination=heroMapTile; path=newPath; isFindingPath=false; repaintMinimap(); if (path === null) { console.log("No Path was found."); }else{ path.push(tapPos); path.reverse(); path.pop(); for (var i = 0; i < path.length; i++) { var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x); tmpSpr.tint=0x0000ff; //console.log("p "+path[i].x+":"+path[i].y); } } }


Движение по пути

Получив путь в виде массива узлов, мы должны заставить персонажа двигаться по нему.

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

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

Function aiWalk(){ if(path.length==0){//путь закончился if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){ dX=0; dY=0; isWalking=false; return; } } isWalking=true; if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){//достигли текущей точки назначения, задаём новую, меняем направление //перед поворотом ждём, пока не войдём на несколько шагов на тайл stepsTaken++; if(stepsTakendestination.x){ dX = -1; }else { dX=0; } if(heroMapTile.ydestination.y){ dY = -1; }else { dY=0; } if(heroMapTile.x==destination.x){ dX=0; }else if(heroMapTile.y==destination.y){ dY=0; } //...... } }

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

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

Можно посмотреть работающее демо .

11. Изометрический скроллинг

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


Видимую экранную область можно рассматривать как небольшой прямоугольник в большом прямогольнике всей области уровня. Скроллинг является простым перемещением внутреннего прямоугольника во внутреннем. Обычно в процессе скроллинга положение персонажа в экранном прямоугольнике остаётся постоянным - чаще всего он находится в центре экрана. Интересно, что для реализации скроллинга нам нужно всего лишь отслеживать угловую точку внутреннего прямоугольника.

Эта угловая точка, которую мы обозначаем в декартовых координатах, попадает в один из тайлов в данных уровня. Для скроллинга мы увеличиваем положение угловой точки по осям X и Y в декартовых координатах. Теперь мы можем преобразовать эту точку в изометрические координаты и использовать их для отрисовки экрана.

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

Или же мы можем отрисовывать на экране сетку изометрических тайлов размером X x Y , чтобы цикл отрисовки был эффективным для больших уровней.

Все эти шаги можно выразить следующим образом:

  • Обновление декартовых координат X и Y угловой точки.
  • Преобразование в изометрическое пространство.
  • Вычитание этого значения из изометрического положения отрисовки каждого тайла.
  • Отрисовка на экране только заданного количества тайлов, начиная с этого нового угла.
  • Дополнительно: отрисовка тайла, только если новое изометрическое положение отрисовки находится в пределах экрана.
var cornerMapPos=new Phaser.Point(0,0); var cornerMapTile=new Phaser.Point(0,0); var visibleTiles=new Phaser.Point(6,6); //... function update(){ //... if (isWalkable()) { heroMapPos.x += heroSpeed * dX; heroMapPos.y += heroSpeed * dY; //перемещаем угол в противоположном направлении cornerMapPos.x -= heroSpeed * dX; cornerMapPos.y -= heroSpeed * dY; cornerMapTile=getTileCoordinates(cornerMapPos,tileWidth); //получаем новый тайл персонажа heroMapTile=getTileCoordinates(heroMapPos,tileWidth); //сортировка по глубине и отрисовка новой сцены renderScene(); } } function renderScene(){ gameScene.clear();//удаляем предыдущий кадр, потом отрисовываем заново var tileType=0; //ограничиваем цикл видимой областью var startTileX=Math.max(0,0-cornerMapTile.x); var startTileY=Math.max(0,0-cornerMapTile.y); var endTileX=Math.min(levelData.length,startTileX+visibleTiles.x); var endTileY=Math.min(levelData.length,startTileY+visibleTiles.y); startTileX=Math.max(0,endTileX-visibleTiles.x); startTileY=Math.max(0,endTileY-visibleTiles.y); //проверяем граничное условие for (var i = startTileY; i < endTileY; i++) { for (var j = startTileX; j < endTileX; j++) { tileType=levelData[i][j]; drawTileIso(tileType,i,j); if(i==heroMapTile.y&&j==heroMapTile.x){ drawHeroIso(); } } } } function drawHeroIso(){ var isoPt= new Phaser.Point();//Не рекомендуется создавать точки в цикле обновления var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y); isoPt=cartesianToIsometric(heroCornerPt);//находим новое изометрическое положение персонажа из положения на 2D-карте gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//отрисовываем тень на текстуре рендера gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//отрисовываем персонажа на текстуре рендера } function drawTileIso(tileType,i,j){//располагаем изометрические тайлы уровня var isoPt= new Phaser.Point();//не рекомендуется создавать точку в цикле обновления var cartPt=new Phaser.Point();//Добавлено для лучшей читаемости кода. cartPt.x=j*tileWidth+cornerMapPos.x; cartPt.y=i*tileWidth+cornerMapPos.y; isoPt=cartesianToIsometric(cartPt); //Можно оптимизировать дальше и не отрисовывать ничего за пределами экрана. if(tileType==1){ gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false); }else{ gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false); } }

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

Пара примечаний:

  • При скроллинге нам может понадобиться отрисовка дополнительных тайлов на границах экрана, иначе по краям экрана тайлы будут появляться и исчезать.
  • Если в игре есть тайлы, занимающие несколько единичных размеров тайлов, то потребуется рисовать больше тайлов на границах. Например, если самый большой тайл из всего набора имеет размер X на Y, то потребуется отрисовывать на X больше тайлов слева и справа, и на Y больше тайлов сверху и снизу. Так мы гарантируем, что углы большого тайла будут видимы при скроллинге.
  • Нам по-прежнему нужно обеспечивать отсутствие пустых областей на экране, когда отрисовка выполняется рядом с границами уровня.
  • Уровень должен скроллиться только до тех пор, пока на соответствующем крае экрана не будет отрисован соответствующий крайний тайл. После этого персонаж должен продолжать двигаться в пространстве экрана без скроллинга уровня. Для этого нам нужно отслеживать все четыре угла внутреннего экранного прямоугольника и соответствующим образом управлять логикой скроллинга и перемещения персонажа. Готовы ли вы реализовать это самостоятельно?

Заключение

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

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