Pull to refresh

Изометрическая сортировка по глубине для подвижных платформ

Reading time8 min
Views6K
Original author: Juwal Bose

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

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

В этом туториале мы подробнее рассмотрим сортировку по глубине для изометрических уровней, потому что добавим ещё и подвижные платформы. Этот туториал — не введение в теорию изометрии и не посвящён коду. В нём мы будем разбираться в логике и теории, а не анализировать код. В качестве инструмента используется 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<SpriteRenderer>();
				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<SpriteRenderer>();
				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. Благодаря этому подходу наш тайл теперь может свободно перемещаться по любой из двух осей, не разрушая глубину в сцене, как показано ниже.


Заключение


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

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

Исходный код всех примеров в туториале выложен на Github.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 14: ↑14 and ↓0+14
Comments2

Articles