Juego 'Sokoban' isométrico y hexagonal basado en mosaico 2D de Unity
Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)



En este tutorial, convertiremos un juego convencional de Sokoban en mosaico 2D en vistas isométricas y hexagonales. Si eres nuevo en juegos isométricos o hexagonales, al principio puede ser abrumador intentar seguirlos a los dos al mismo tiempo. En ese caso, recomiendo elegir primero isométrico y luego regresar en una etapa posterior para la versión hexagonal.
Vamos a construir sobre el anterior tutorial de Unity: Unity 2D Tile-Based Sokoban Game. Primero, repase el
tutorial, ya que la mayoría del código permanece inalterado y todos los
conceptos básicos siguen siendo los mismos. También vincularé a otros tutoriales que explican algunos de los conceptos subyacentes.
El aspecto más importante en la creación de versiones isométricas o hexagonales a partir de una versión 2D es averiguar el posicionamiento de los elementos. Usaremos métodos de conversión basados en ecuaciones para convertir entre los diversos sistemas de coordenadas.
Este tutorial tiene dos secciones, una para la versión isométrica y otra para la versión hexagonal.
1. Juego Sokoban Isométrico
Vamos a sumergirnos en la versión isométrica una vez que hayas pasado por el tutorial original. La siguiente imagen muestra cómo se vería la versión isométrica, siempre que usemos la misma información de nivel utilizada en el tutorial original.



Vista Isométrica
La teoría isométrica, la ecuación de conversión y la implementación se explican en varios tutoriales en Envato Tuts +. Una vieja explicación basada en Flash se puede encontrar en este detallado tutorial. Recomendaría este tutorial basado en Phaser, ya que es más reciente y a prueba de futuro.
Aunque los lenguajes de scripting utilizados en esos tutoriales son ActionScript 3 y JavaScript, respectivamente, la teoría es aplicable en todas partes, independientemente de los lenguajes de programación. Básicamente se reduce a estas ecuaciones de conversión que se deben usar para convertir coordenadas cartesianas 2D a coordenadas isométricas o viceversa.
1 |
//Cartesian to isometric:
|
2 |
|
3 |
isoX = cartX - cartY; |
4 |
isoY = (cartX + cartY) / 2; |
5 |
|
6 |
//Isometric to Cartesian:
|
7 |
|
8 |
cartX = (2 * isoY + isoX) / 2; |
9 |
cartY = (2 * isoY - isoX) / 2; |
Utilizaremos la siguiente función de Unity para la conversión a coordenadas isométricas.
1 |
Vector2 CartesianToIsometric(Vector2 cartPt){ |
2 |
Vector2 tempPt=new Vector2(); |
3 |
tempPt.x=cartPt.x-cartPt.y; |
4 |
tempPt.y=(cartPt.x+cartPt.y)/2; |
5 |
return (tempPt); |
6 |
}
|
Cambios en el arte
Utilizaremos la misma información de nivel para crear nuestra matriz 2D, levelData, que impulsará la representación isométrica. La mayor parte del código también seguirá siendo el mismo, aparte de lo específico de la vista isométrica.
El arte, sin embargo, necesita tener algunos cambios con respecto a los puntos de pivote. Por favor, consulte la imagen a continuación y la explicación que sigue.



El script del juego IsometricSokoban usa sprites modificados como heroSprite, ballSprite y blockSprite. La imagen muestra los nuevos puntos de pivote utilizados para estos sprites. Este cambio proporciona el aspecto pseudo 3D que pretendemos con la vista isométrica. BlockSprite es un nuevo sprite que agregamos cuando encontramos un invalidTile.
Me ayudará a explicar el aspecto más importante de los juegos isométricos, la clasificación en profundidad. Aunque el sprite es solo un hexágono, lo estamos considerando como un cubo 3D donde el pivote está situado en el medio de la cara inferior del cubo.
Cambios en el código
Descargue el código compartido a través del repositorio git vinculado antes de seguir adelante. El
método CreateLevel tiene algunos cambios que tienen que ver con la
escala y el posicionamiento de las teselas y la adición de blockTile. La escala de
tileSprite, que es solo una imagen en forma de diamante que representa
nuestra loseta de tierra, necesita ser alterada de la siguiente manera.
1 |
tile.transform.localScale=new Vector2(tileSize-1,(tileSize-1)/2);//size is critical for isometric shape |
Esto refleja el hecho de que un mosaico isométrico tendrá una altura de la mitad de su ancho. El heroSprite y el ballSprite tienen un tamaño de tileSize/2.
1 |
hero.transform.localScale=Vector2.one*(tileSize/2);//we use half the tilesize for occupants |
Dondequiera que encontremos un invalidTile, agregamos un blockTile usando el siguiente código.
1 |
tile = new GameObject("block"+i.ToString()+"_"+j.ToString());//create new tile |
2 |
float rootThree=Mathf.Sqrt(3); |
3 |
float newDimension= 2*tileSize/rootThree; |
4 |
tile.transform.localScale=new Vector2(newDimension,tileSize);//we need to set some height |
5 |
sr = tile.AddComponent<SpriteRenderer>();//add a sprite renderer |
6 |
sr.sprite=blockSprite;//assign block sprite |
7 |
sr.sortingOrder=1;//this also need to have higher sorting order |
8 |
Color c= Color.gray; |
9 |
c.a=0.9f; |
10 |
sr.color=c; |
11 |
tile.transform.position=GetScreenPointFromLevelIndices(i,j);//place in scene based on level indices |
12 |
occupants.Add(tile, new Vector2(i,j));//store the level indices of block in dict |
El hexágono necesita una escala diferente para obtener el aspecto isométrico. Esto no será un problema cuando el arte sea manejado por artistas. Estamos
aplicando un valor alfa ligeramente inferior al blockSprite para que
podamos ver a través de él, lo que nos permite ver la clasificación de
profundidad correctamente. Tenga en cuenta
que también estamos agregando estos mosaicos al diccionario de
occupants, que se utilizarán más adelante para la clasificación en
profundidad.
El posicionamiento de las teselas se realiza mediante el método GetScreenPointFromLevelIndices, que a su vez utiliza el método de conversión CartesianToIsometric explicado anteriormente. El eje Y apunta en la dirección opuesta para Unity, que debe tenerse en cuenta al agregar el middleOffset para posicionar el nivel en el medio de la pantalla.
1 |
Vector2 GetScreenPointFromLevelIndices(int row,int col){ |
2 |
//converting indices to position values, col determines x & row determine y
|
3 |
Vector2 tempPt=CartesianToIsometric(new Vector2(col*tileSize/2,row*tileSize/2));//removed the '-' inthe y part as axis correction can happen after coversion |
4 |
tempPt.x-=middleOffset.x;//we apply the offset outside the coordinate conversion to align the level in screen middle |
5 |
tempPt.y*=-1;//unity y axis correction |
6 |
tempPt.y+=middleOffset.y;//we apply the offset outside the coordinate conversion to align the level in screen middle |
7 |
return tempPt; |
8 |
}
|
Al final del método CreateLevel, así como al final del método TryMoveHero, llamamos al método DepthSort. La clasificación por profundidad es el aspecto más importante de una implementación isométrica. Esencialmente, determinamos qué fichas van detrás o frente a otras fichas en el nivel. El método DepthSort es como se muestra a continuación.
1 |
private void DepthSort() |
2 |
{
|
3 |
int depth=1; |
4 |
SpriteRenderer sr; |
5 |
Vector2 pos=new Vector2(); |
6 |
for (int i = 0; i < rows; i++) { |
7 |
for (int j = 0; j < cols; j++) { |
8 |
int val=levelData[i,j]; |
9 |
if(val!=groundTile && val!=destinationTile){//a tile which needs depth sorting |
10 |
pos.x=i; |
11 |
pos.y=j; |
12 |
GameObject occupant=GetOccupantAtPosition(pos);//find the occupant at this position |
13 |
if(occupant==null)Debug.Log("no occupant"); |
14 |
sr=occupant.GetComponent<SpriteRenderer>(); |
15 |
sr.sortingOrder=depth;//assign new depth |
16 |
depth++;//increment depth |
17 |
}
|
18 |
}
|
19 |
}
|
20 |
}
|
La belleza de una implementación en 2D basada en matrices es que para la correcta ordenación de profundidad isométrica, solo tenemos que asignar una profundidad secuencialmente mayor mientras analizamos el nivel en orden, utilizando secuencial para bucles. Esto funciona para nuestro nivel simple con solo una capa de suelo. Si tuviéramos varios niveles de suelo a varias alturas, la clasificación en profundidad podría complicarse.
Todo lo demás permanece igual que la implementación 2D explicada en el tutorial anterior.
Nivel Completado
Puedes usar los mismos controles de teclado para jugar el juego. La única diferencia es que el héroe no se moverá vertical u horizontalmente sino isométricamente. El nivel final se vería como la imagen de abajo.



Vea cómo la clasificación de profundidad es claramente visible con nuestros nuevos blockTiles.
Eso no fue difícil, ¿verdad? Los invito a cambiar los datos de nivel en el archivo de texto para probar nuevos niveles. La siguiente es la versión hexagonal, que es un poco más complicada, y le aconsejo que se tome un descanso para jugar con la versión isométrica antes de continuar.
2. Juego Sokoban Hexagonal
La versión hexagonal del nivel Sokoban se vería como la imagen de abajo.



Vista hexagonal
Estamos utilizando la alineación horizontal para la cuadrícula hexagonal para este tutorial. La teoría detrás de la implementación hexagonal requiere mucha lectura adicional. Por favor, consulte esta serie de tutoriales para una comprensión básica. La teoría se implementa en la clase auxiliar HexHelperHorizontal, que se puede encontrar en la carpeta utils.
Conversión de coordenadas hexagonales
El script del
juego HexagonalSokoban utiliza métodos de conveniencia de la clase
auxiliar para conversiones de coordenadas y otras características
hexagonales. La clase auxiliar HexHelperHorizontal solo funcionará con una cuadrícula hexagonal alineada horizontalmente. Incluye métodos para convertir las coordenadas entre los sistemas offset, axial y cúbico.
La coordenada de desplazamiento es la misma coordenada cartesiana 2D. También
incluye un método getNeighbors, que toma una coordenada axial y
devuelve una List<Vector2> con los seis vecinos de esa celda de
coordenadas. El orden de la lista es en el sentido de las agujas del reloj, comenzando con la coordenada de la celda del vecino del noreste.
Cambios en los controles
Con una cuadrícula hexagonal, tenemos seis direcciones de movimiento en lugar de cuatro, ya que el hexágono tiene seis lados, mientras que un cuadrado tiene cuatro. Entonces, tenemos seis teclas para controlar el movimiento de nuestro héroe, como se muestra en la imagen a continuación.



Las teclas se
organizan en el mismo diseño que una cuadrícula hexagonal si se
considera la tecla S del teclado como la celda central, con todas las
teclas de control como vecinos hexagonales. Ayuda a reducir la confusión con el control del movimiento. Los cambios correspondientes al código de entrada son los siguientes.
1 |
private void ApplyUserInput() |
2 |
{//we have 6 directions of motion controlled by e,d,x,z,a,w in a cyclic sequence starting with NE to NW |
3 |
if(Input.GetKeyUp(userInputKeys[0])){ |
4 |
TryMoveHero(0);//north east |
5 |
}else if(Input.GetKeyUp(userInputKeys[1])){ |
6 |
TryMoveHero(1);//east |
7 |
}else if(Input.GetKeyUp(userInputKeys[2])){ |
8 |
TryMoveHero(2);//south east |
9 |
}else if(Input.GetKeyUp(userInputKeys[3])){ |
10 |
TryMoveHero(3);//south west |
11 |
}else if(Input.GetKeyUp(userInputKeys[4])){ |
12 |
TryMoveHero(4);//west |
13 |
}else if(Input.GetKeyUp(userInputKeys[5])){ |
14 |
TryMoveHero(5);//north west |
15 |
}
|
16 |
}
|
No hay cambios en el arte, y no hay cambios de pivote necesarios.
Otros cambios en el código
Explicaré los cambios de código con respecto al tutorial 2D Sokoban original y no a la versión isométrica anterior. Consulte el código fuente vinculado para este tutorial. El hecho más interesante es que casi todo el código sigue siendo el mismo. El método CreateLevel solo tiene un cambio, que es el cálculo de middleOffset.
1 |
middleOffset.x=cols*tileWidth+tileWidth*0.5f;//this is changed for hexagonal |
2 |
middleOffset.y=rows*tileSize*3/4+tileSize*0.75f;//this is changed for isometric |
3 |
Un cambio
importante es obviamente la forma en que se encuentran las coordenadas
de la pantalla en el método GetScreenPointFromLevelIndices.
1 |
Vector2 GetScreenPointFromLevelIndices(int row,int col){ |
2 |
//converting indices to position values, col determines x & row determine y
|
3 |
Vector2 tempPt=new Vector2(row,col); |
4 |
tempPt=HexHelperHorizontal.offsetToAxial(tempPt);//convert from offset to axial |
5 |
//convert axial point to screen point
|
6 |
tempPt=HexHelperHorizontal.axialToScreen(tempPt,sideLength); |
7 |
tempPt.x-=middleOffset.x-Screen.width/2;//add offsets for middle align |
8 |
tempPt.y*=-1;//unity y axis correction |
9 |
tempPt.y+=middleOffset.y-Screen.height/2; |
10 |
return tempPt; |
11 |
}
|
Aquí usamos la clase auxiliar para convertir primero la coordenada a axial y luego encontrar la coordenada de pantalla correspondiente. Tenga en cuenta el uso de la variable sideLength para la segunda conversión. Es el valor de
la longitud de un lado de la losa hexagonal, que de nuevo es igual a la
mitad de la distancia entre los dos extremos puntiagudos del hexágono. Por lo tanto:
1 |
sideLength=tileSize*0.5f; |
El único otro cambio es el método GetNextPositionAlong, que es utilizado por el método TryMoveHero para encontrar la siguiente celda en una dirección determinada. Este método ha sido completamente modificado para acomodar el diseño completamente nuevo de nuestra cuadrícula.
1 |
private Vector2 GetNextPositionAlong(Vector2 objPos, int direction) |
2 |
{//this method is completely changed to accommodate the different way neighbours are found in hexagonal logic |
3 |
objPos=HexHelperHorizontal.offsetToAxial(objPos);//convert from offset to axial |
4 |
List<Vector2> neighbours= HexHelperHorizontal.getNeighbors(objPos); |
5 |
objPos=neighbours[direction];//the neighbour list follows the same order sequence |
6 |
objPos=HexHelperHorizontal.axialToOffset(objPos);//convert back from axial to offset |
7 |
return objPos; |
8 |
}
|
Usando la clase de ayuda, podemos devolver fácilmente las coordenadas del vecino en la dirección dada.
Todo lo demás permanece igual que la implementación 2D original. Eso no fue difícil, ¿verdad? Dicho esto, entender cómo llegamos a las ecuaciones de conversión siguiendo el tutorial hexagonal, que es el quid de todo el proceso, no es fácil. Si juegas y completas el nivel, obtendrás el resultado de la siguiente manera.



Conclusión
El elemento principal en ambas conversiones fue la conversión de coordenadas. La versión isométrica implica cambios adicionales en la técnica con su punto de pivote así como la necesidad de una clasificación en profundidad.
Creo que ha descubierto lo fácil que es crear juegos basados en cuadrículas utilizando solo datos de nivel basados en matrices bidimensionales y un enfoque basado en mosaicos. Hay posibilidades ilimitadas y juegos que puedes crear con esta nueva comprensión.
Si ha entendido todos los conceptos que hemos discutido hasta ahora, lo invitaría a cambiar el método de control para tocar y agregar algún hallazgo de ruta. Buena suerte.



