Movimiento del carácter hexagonal usando coordenadas axiales

Lo que vas a crear

En la primera parte de la serie, exploramos los diferentes sistemas de coordenadas para juegos basados ​​en mosaicos hexagonales con la ayuda de un juego hexagonal de Tetris. Una cosa que puede haber notado es que todavía estamos confiando en las coordenadas de desplazamiento para dibujar el nivel en la pantalla usando el levelData formación. 

También puede ser curioso saber cómo podríamos determinar las coordenadas axiales de un mosaico hexagonal a partir de las coordenadas de píxeles en la pantalla. El método utilizado en el tutorial del limpiador de minas hexagonal se basa en las coordenadas de desplazamiento y no es una solución simple. Una vez que resolvamos esto, procederemos a crear soluciones para el movimiento de caracteres hexagonales y la búsqueda de caminos..

1. Conversión de coordenadas entre píxel y axial

Esto implicará algunas matemáticas. Usaremos el diseño horizontal para todo el tutorial. Comencemos por encontrar una relación muy útil entre el ancho y la altura del hexágono regular. Por favor refiérase a la imagen de abajo..

Considere el hexágono regular azul a la izquierda de la imagen. Ya sabemos que todos los lados son de igual longitud. Todos los ángulos interiores son de 120 grados cada uno. Conectar cada esquina al centro del hexágono dará como resultado seis triángulos, uno de los cuales se muestra con líneas rojas. Este triángulo tiene todos los ángulos internos iguales a 60 grados.. 

A medida que la línea roja divide los dos ángulos de la esquina en el medio, obtenemos 120/2 = 60. El tercer angulo es 180- (60 + 60) = 60 como la suma de todos los ángulos dentro del triángulo debe ser 180 grados. Por lo tanto, esencialmente el triángulo es un triángulo equilátero, lo que significa que cada lado del triángulo tiene la misma longitud. Así que en el hexágono azul las dos líneas rojas, la línea verde y cada segmento de la línea azul son de la misma longitud. De la imagen, está claro que la línea verde es hexTileHeight / 2.

Continuando hacia el hexágono de la derecha, podemos ver que la longitud del lado es igual a hexTileHeight / 2, La altura de la porción triangular superior debe ser hexTileHeight / 4 y la altura de la parte triangular inferior debe ser hexTileHeight / 4, que totaliza a la altura total del hexágono, hexagonal altura

Ahora considere el pequeño triángulo rectángulo en la parte superior izquierda con un ángulo verde y otro azul. El ángulo azul es de 60 grados, ya que es la mitad del ángulo de la esquina, lo que a su vez significa que el ángulo verde es de 30 grados (180- (60 + 90)). Usando esta información, llegamos a una relación entre la altura y el ancho del hexágono regular.

tan 30 = lado opuesto / lado adyacente; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);

Conversión de coordenadas axiales a píxeles

Antes de acercarnos a la conversión, revisemos la imagen del diseño hexagonal horizontal en el que hemos resaltado la fila y la columna en la que una de las coordenadas permanece igual..

Teniendo en cuenta el valor de y de la pantalla, podemos ver que cada fila tiene un desplazamiento de y 3 * hexTileHeight / 4, Mientras desciende en la línea verde, el único valor que cambia es yo. Por lo tanto, podemos concluir que el valor del píxel y solo depende de la axial yo coordinar.

y = (3 * hexTileHeight / 4) * i; y = 3/2 * s * i;

Dónde s es la longitud del lado, que se encontró que era hexTileHeight / 2.

El valor de la pantalla x es un poco más complicado que esto. Al considerar los mosaicos dentro de una sola fila, cada mosaico tiene un desplazamiento x de hexTileWidth, lo que claramente depende solo de lo axial j coordinar. Pero cada fila alternativa tiene un desplazamiento adicional de hexTileWidth / 2 dependiendo de lo axial yo coordinar.

Nuevamente considerando la línea verde, si imaginamos que era una cuadrícula cuadrada, entonces la línea habría sido vertical, satisfaciendo la ecuación x = j * hexTileWidth. Como la única coordenada que cambia a lo largo de la línea verde es yo, El desplazamiento dependerá de ello. Esto nos lleva a la siguiente ecuación..

x = j * hexTileWidth + (i * hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 + i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));

Aquí las tenemos: las ecuaciones para convertir las coordenadas axiales en coordenadas de pantalla. La función de conversión correspondiente es la siguiente.

var rootThree = Math.sqrt (3); var sideLength = hexTileHeight / 2; function axialToScreen (axialPoint) var tileX = rootThree * sideLength * (axialPoint.y + (axialPoint.x / 2)); var tileY = 3 * sideLength / 2 * axialPoint.x; axialPoint.x = tileX; axialPoint.y = tileY; retorno axialPoint; 

El código revisado para dibujar la cuadrícula hexagonal es el siguiente.

para (var i = 0; i < levelData.length; i++)  for (var j = 0; j < levelData[0].length; j++)  axialPoint.x=i; axialPoint.y=j; axialPoint=offsetToAxial(axialPoint); screenPoint=axialToScreen(axialPoint); if(levelData[i][j]!=-1) hexTile= new HexTileNode(game, screenPoint.x, screenPoint.y, 'hex', false,i,j,levelData[i][j]); hexGrid.add(hexTile);   

Convertir píxel a coordenadas axiales

Revertir esas ecuaciones con la simple sustitución de una variable nos llevará a la pantalla a ecuaciones de conversión axial..

i = y / (3/2 * s); j = (x- (y / sqrt (3))) / s * sqrt (3);

Aunque las coordenadas axiales requeridas son enteros, las ecuaciones darán como resultado números de punto flotante. Así que tendremos que redondearlos y aplicar algunas correcciones, confiando en nuestra ecuación principal x + y + z = 0. La función de conversión es la siguiente.

función screenToAxial (screenPoint) var axialPoint = new Phaser.Point (); axialPoint.x = screenPoint.y / (1.5 * sideLength); axialPoint.y = (screenPoint.x- (screenPoint.y / rootThree)) / (rootThree * sideLength); var cubicZ = calculaCubicZ (axialPoint); var round_x = Math.round (axialPoint.x); var round_y = Math.round (axialPoint.y); var round_z = Math.round (cubicZ); if (round_x + round_y + round_z === 0) screenPoint.x = round_x; screenPoint.y = round_y;  else var delta_x = Math.abs (axialPoint.x-round_x); var delta_y = Math.abs (axialPoint.y-round_y); var delta_z = Math.abs (cubicZ-round_z); if (delta_x> delta_y && delta_x> delta_z) screenPoint.x = -round_y-round_z; screenPoint.y = round_y;  else if (delta_y> delta_x && delta_y> delta_z) screenPoint.x = round_x; screenPoint.y = -round_x-round_z;  else if (delta_z> delta_x && delta_z> delta_y) screenPoint.x = round_x screenPoint.y = round_y;  devolver screenPoint; 

Echa un vistazo al elemento interactivo, que utiliza estos métodos para mostrar mosaicos y detectar toques..

2. Movimiento del personaje

El concepto central del movimiento del personaje en cualquier cuadrícula es similar. Encuestamos la entrada del usuario, determinamos la dirección, encontramos la posición resultante, verificamos si la posición resultante cae dentro de un muro en la cuadrícula, de lo contrario, mueva el carácter a esa posición. Puede consultar mi tutorial de movimiento de caracteres isométricos para ver esto en acción con respecto a la conversión de coordenadas isométricas. 

Las únicas cosas que son diferentes aquí son la conversión de coordenadas y las direcciones de movimiento. Para una cuadrícula hexagonal alineada horizontalmente, hay seis direcciones disponibles para el movimiento. Podríamos usar las teclas del teclado. UNA, W, mi, re, X, y Z para controlar cada direccion La distribución de teclado predeterminada coincide con las instrucciones perfectamente, y las funciones relacionadas son las siguientes.

function moveLeft () movementVector.x = movementVector.y = 0; movementVector.x = -1 * velocidad; CheckCollisionAndMove ();  función moveRight () movementVector.x = movementVector.y = 0; movementVector.x = velocidad; CheckCollisionAndMove ();  function moveTopLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveTopRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveBottomRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveBottomLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove (); 

Las direcciones diagonales de movimiento forman un ángulo de 60 grados con la dirección horizontal. Entonces podemos calcular directamente la nueva posición usando trigonometría usando Cos 60 y Seno 60. De esto movimientovector, averiguamos la nueva posición resultante y verificamos si cae dentro de un muro en la cuadrícula como se muestra a continuación.

función CheckCollisionAndMove () var tempPos = new Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var corner = nuevo Phaser.Point (); // comprueba tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (corner)) return; // check tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (corner)) return; // check bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (corner)) return; // check br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (corner)) return; hero.x = tempPos.x; hero.y = tempPos.y;  función checkCorner (corner) corner = screenToAxial (corner); corner = axialToOffset (corner); if (checkForOccuppancy (corner.x, corner.y)) return true;  falso retorno; 

Añadimos el movimientovector al vector de posición del héroe para obtener la nueva posición para el centro del héroe sprite. Luego encontramos la posición de las cuatro esquinas del sprite héroe y verificamos si están chocando. Si no hay colisiones, establecemos la nueva posición en el sprite de héroe. Veamos eso en acción..

Generalmente, este tipo de movimiento de flujo libre no está permitido en un juego basado en cuadrícula. Normalmente, los caracteres se mueven de un mosaico a otro, es decir, centro de mosaico a centro de mosaico, según los comandos o el toque. Confío en que puedas encontrar la solución por ti mismo..

3. Pathfinding

Así que aquí estamos en el tema de la búsqueda de caminos, un tema muy aterrador para algunos. En mis tutoriales anteriores, nunca intenté crear nuevas soluciones de búsqueda de caminos, pero siempre preferí usar soluciones disponibles que están probadas en batalla.. 

Esta vez, estoy haciendo una excepción y reinventaré la rueda, principalmente porque hay varias mecánicas de juego posibles y ninguna solución única beneficiaría a todos. Así que es útil saber cómo se hace todo esto para producir sus propias soluciones personalizadas para su mecánico de juego.. 

El algoritmo más básico que se usa para el pathfinding en grillas es El algoritmo de Dijkstra. Comenzamos en el primer nodo y calculamos los costos involucrados en el traslado a todos los nodos vecinos posibles. Cerramos el primer nodo y lo movemos al nodo vecino con el menor costo involucrado. Esto se repite para todos los nodos no cerrados hasta que alcanzamos el destino. Una variante de esta es la Algoritmo A *, Donde también utilizamos una heurística además del costo.. 

Se utiliza una heurística para calcular la distancia aproximada desde el nodo actual al nodo de destino. Como no conocemos realmente el camino, este cálculo de distancia es siempre una aproximación. Así que una mejor heurística siempre dará un mejor camino. Ahora, dicho esto, no es necesario que la mejor solución sea la que proporcione el mejor camino, ya que también debemos considerar el uso de recursos y el rendimiento del algoritmo, cuando todos los cálculos deben realizarse en tiempo real o una vez por actualización. lazo. 

La heurística más fácil y sencilla es la Heurística de manhattan o Distancia de Manhattan. En una cuadrícula 2D, esta es en realidad la distancia entre el nodo de inicio y el nodo final a medida que el cuervo vuela, o la cantidad de bloques que necesitamos para caminar.

Variante hexagonal de manhattan

Para nuestra cuadrícula hexagonal, necesitamos encontrar una variante para la heurística de Manhattan que se aproxime a la distancia. Mientras caminamos sobre las baldosas hexagonales, la idea es encontrar la cantidad de baldosas que debemos recorrer para llegar al destino. Déjame mostrarte la solución primero. Mueva el mouse sobre el elemento interactivo a continuación para ver qué tan lejos están las otras fichas del mosaico debajo del mouse.

En el ejemplo anterior, encontramos el mosaico debajo del mouse y encontramos la distancia de todos los otros mosaicos desde él. La lógica es encontrar la diferencia de yo y j coordenadas axiales de las dos baldosas primero, digamos di y DJ. Encuentra los valores absolutos de estas diferencias., absi y absj, como las distancias son siempre positivas. 

Notamos que cuando ambos di y DJ son positivos y cuando ambos di y DJ son negativos, la distancia es absi + absj. Cuando di y DJ son de signos opuestos, la distancia es el valor más grande entre absi y absj. Esto conduce a la función de cálculo heurístico. obtenerHeuristic como a continuación.

getHeuristic = function (i, j) j = (j- (Math.floor (i / 2))); var di = i-this.originali; var dj = j-this.convertedj; var si = Math.sign (di); var sj = Math.sign (dj); var absi = di * si; var absj = dj * sj; if (si! = sj) this.heuristic = Math.max (absi, absj);  else this.heuristic = (absi + absj); 

Una cosa a tener en cuenta es que no estamos considerando si el camino es realmente transitable o no; solo asumimos que es transitable y establecemos el valor de la distancia. 

Encontrando el camino hexagonal

Continuemos con la búsqueda de rutas para nuestra cuadrícula hexagonal con el nuevo método heurístico encontrado. Como usaremos la recursión, será más fácil de entender una vez que desglosemos la lógica central de nuestro enfoque. Cada mosaico hexagonal tendrá una distancia heurística y un valor de costo asociado..

  • Tenemos una función recursiva, digamos findPath (azulejo), que lleva en una baldosa hexagonal, que es la baldosa actual. Inicialmente esta será la ficha inicial..
  • Si el mosaico es igual al de final, la recursión finaliza y hemos encontrado el camino. Si no procedemos con el cálculo..
  • Encontramos a todos los vecinos transitables de la baldosa. Recorreremos todas las fichas vecinas y aplicaremos más lógica a cada una de ellas, a menos que estén cerrado.
  • Si un vecino no es previamente visitó y no cerrado, encontramos la distancia de la baldosa vecina a la baldosa final utilizando nuestra heurística. Colocamos las tejas vecinas. costo a coste de la baldosa actual + 10. Colocamos la baldosa vecina como visitó. Colocamos las tejas vecinas. azulejo anterior como el azulejo actual. También hacemos esto para un vecino visitado previamente si el costo de la baldosa actual + 10 es menor que el costo del vecino.
  • Calculamos el costo total como la suma del valor del costo de la baldosa vecina y el valor de la distancia heurística. Entre todos los vecinos, seleccionamos el vecino que da el costo total más bajo y llamamos a findPath en esa baldosa vecina.
  • Configuramos el mosaico actual en cerrado para que no se considere más..
  • En algunos casos, no encontraremos ninguna ficha que satisfaga las condiciones, y luego cerramos la ficha actual, abrimos la ficha anterior y rehacemos.

Hay una condición de falla obvia en la lógica cuando más de una ficha satisface las condiciones. Un mejor algoritmo buscará todos los diferentes caminos y seleccionará el que tenga la longitud más corta, pero no lo haremos aquí. Revisa el pathfinding en acción a continuación..

Para este ejemplo, estoy calculando vecinos de manera diferente que en el ejemplo de Tetris. Cuando se usan coordenadas axiales, los mosaicos adyacentes tienen coordenadas que son más altas o más bajas en un valor de 1.

función getNeighbors (i, j) // las coordenadas están en axial var tempArray = []; var axialPoint = nuevo Phaser.Point (i, j); var neighbourPoint = new Phaser.Point (); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // l neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // r neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); return tempArray; 

los findPath La función recursiva es la siguiente.

la función findPath (tile) // pasa en un hexTileNode if (Phaser.Point.equals (tile, endTile)) // success, el destino ha alcanzado console.log ('success'); // ahora pinta el camino. paintPath (azulejo);  else // encuentra todos los vecinos var vecinos = getNeighbors (tile.originali, tile.convertedj); var newPt = new Phaser.Point (); var hexTile; var totalCost = 0; var currentLowestCost = 100000; var nextTile; // buscar heurística y costo para todos los vecinos while (neights.length) newPt = neighbour.shift (); hexTile = hexGrid.getByName ("tile" + newPt.x + "_" + newPt.y); if (! hexTile.nodeClosed) // si el nodo no se calculó aún si ((hexTile.nodeVisited && (tile.cost + 10)

Puede requerir una lectura adicional y múltiple para comprender correctamente lo que está sucediendo, pero créanme, vale la pena el esfuerzo. Esta es solo una solución muy básica y podría mejorarse mucho. Para mover el carácter a lo largo de la ruta calculada, puede consultar mi ruta isométrica siguiendo el tutorial. 

La marcación de la ruta se realiza mediante otra función recursiva simple, PaintPath (azulejo), que se llama primero con la ficha final. Solo marcamos el nodo anterior de la baldosa si está presente.

función paintPath (tile) tile.markDirty (); if (tile.previousNode! == null) paintPath (tile.previousNode); 

Conclusión

Con la ayuda de los tres tutoriales hexagonales que he compartido, deberías poder comenzar con tu próximo juego impresionante basado en fichas hexagonales. 

Tenga en cuenta que también hay otros enfoques, y hay muchas más lecturas si está dispuesto a hacerlo. Por favor, avíseme a través de los comentarios si necesita algo más que explorar en relación con los juegos basados ​​en mosaicos hexagonales..