Advertisement
  1. Game Development
  2. Game Development
Gamedevelopment

Introducción Actualizada para Crear Mundos Isométricos, Parte 2

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Primer for Creating Isometric Worlds.
An Updated Primer for Creating Isometric Worlds, Part 1

Spanish (Español) translation by Javier Salesi (you can also view the original English article)

Final product image
What You'll Be Creating

En ésta parte final de la serie de tutoriales, construiremos sobre el primer tutorial y aprenderemos sobre implementar pickups, desencadenantes, cambio de nivel, buscar ruta, seguir ruta, scrolling de nivel, altura isométrica y proyectiles isométricos.

1. Pickups

Pickups son elementos que puede ser levantados dentro del nivel, normalmente al simplemente caminar sobre ellos-por ejemplo, monedas, joyas, efectivo, municiones, etc.

Datos de pickup pueden ser acomodados en nuestros datos de nivel como se muestra abajo:

En éstos datos de nivel, usamos 8 para denotar un pickup en un azulejo de césped (1 y 0 representan las paredes y los azulejos por los que se puede caminar respectivamente, como antes). Ésta podría ser una imagen de azulejo con un azulejo de césped superpuesta con la imagen del pickup. Siguiendo ésta lógica, necesitaremos dos diferentes estados de azulejos para cada azulejo que tenga un pickup, es decir uno con pickup y uno sin pickup para ser mostrado después de que el pickup sea levantado.

El típico arte isométrico tendrá múltiples azulejos transitables-supongamos que tenemos 30. El planteamiento de arriba significa que si tenemos N pickups, necesitaremos N x 30 azulejos además de los 30 azulejos originales, pues cada azulejo necesitará tener una versión con pickups y una sin pickups. Ësto no es muy eficiente; mejor, deberíamos intentar crear dinámicamente éstas combinaciones.

Para resolver ésto, podríamos usar el mismo método que utilizamos para colocar el héroe del primer tutorial. Cuando nos topemos con un azulejo pickup, colocaremos primero un azulejo de césped y luego colocamos el pickup arriba del azulejo de césped. De ésta manera, solo necesitamos N azulejos pickup además de los 30 azulejos transitables, pero necesitaríamos valores numéricos para representar cada combinación en los datos de nivel. Para resolver la necesidad de los valores de representación N x 30, podemos mantener un separado pickupArray para guardar exclusivamente los datos pickup aparte del levelData. El nivel completado con el pickup es mostrado abajo:

Isometric level with coin pickup

Para nuestro ejemplo, estoy manteniendo las cosas simples y no estoy usando un arreglo adicional para pickups.

Levantando Pickups

Detectar pickups se hace de la misma manera que detectando azulejos de colisión, pero después de mover el personaje.

En la función onPickupTile(), verificamos si el valor del array levelData en la coordinada heroMapTile es un azulejo pickup o no. El número en el arreglo levelData en la coordinada de azulejo denota el tipo de pickup. Verificamos colisiones antes de mover el personaje pero necesitamos verificar después pickups, porque en el caso de las colisiones el personaje no debería ocupar el lugar si ya está ocupado por el azulejo de colisión, pero en caso de pickups el personaje es libre de moverse sobre él.

Otra cosa a notar es que los datos de la colisión generalmente nunca cambian, pero los datos de pickup cambian cuando recogemos un elemento. (Ésto generalmente implica cambiar el valor en el arreglo levelData de, digamos 8 a 0.)

Ésto conduce a un problema: ¿qué ocure cuando necesitamos reiniciar el nivel, y así restablecer todo los pickups a sus posiciones originales? No tenemos la información para hacer ésto, pues el arreglo levelData ha sido cambiado debido a que el jugador levantó elementos. La solución es utilizar un arreglo duplicado para el nivel mientras se juega y para mantener el arreglo original levelData intacto. Por ejemplo, usamos levelData y levelDataLive[], clona el último del primero al inicio del nivel, y sólamente cambia levelDataLive[] durante el juego.

Para el ejemplo, estoy generando un pickup aleatorio en un azulejo de césped vacante después de cada pickup e incrementando el pickupCount. La función pickupItem se ve así.

Deberías notar que verificamos pickups cuando el personaje está en ese azulejo. Ésto puede ocurrir múltiples ocasiones dentro de un segundo (verificamos únicamente cuando el usuario se mueve, pero podríamos estar dando vueltas y vueltas dentro de un azulejo), pero la lógica de arriba no fallará; ya que establecemos los datos del arreglo levelData en 0, la primera vez que detectamos un pickup, todas las verificaciones subsecuentes onPickupTile() retornarán false para ese azulejo. Consulta el ejemplo interactivo de abajo:

2. Azulejos Trigger (Desencadenantes)

Como el nombre sugiere, los azulejos desencadenantes pueden causar que ocurra algo cuando el jugador pisa en ellos o presiona una tecla cuando está en ellos. Podrían teletransportar al jugador a una locación diferente, abrir una puerta, o generar un enemigo, por dar unos cuantos ejemplos. En un sentido, pickups son solo una forma especial de azulejos desencadenantes: cuando el jugador pisa en un azulejo que contiene una moneda, la moneda desaparece y su contador de monedas se incrementa.

Veamos cómo podríamos implementar una puerta que lleva al jugador a un nivel diferente. El azulejo junto a la puerta será un azulejo desencadenante; cuando el jugador presiona la tecla x, procederán al siguiente nivel.

Isometric level with doors trigger tiles

Para cambiar niveles, todo lo que necesitamos hacer es cambiar el actual arreglo levelData con el del nuevo nivel, y establecer la nueva posición y dirección de heroMapTile y para el personaje héroe. Supongamos que hay dos niveles con puertas para permitir pasar entre ellas. Ya que el azulejo del piso junto a la puerta será el azulejo desencadenante en ambos niveles, podemos usar éste como la nueva posición para el personaje cuando aparecen en el nivel.

La lógica de implementación aquí es la misma para pickups, y de nuevo usamos el arreglo levelData para almacenar valores desencadenantes. Para nuestro ejemplo, 2 denota un azulejo de puerta, y el valor junto a él es el desencadenante. He usado 101 y 102 con la convención básica que cualquier azulejo con un valor mayor a 100 es un azulejo desencadenante y el valor menor a 100 puede ser el nivel que conduce a:

El código para verificar un evento desencadenante es mostrado abajo:

La función triggerListener() verifica si los valores del arreglo de los datos desencadenntes en la coordenada dada es mayor a 100. Si es así, encontramos a qué nivel necesitamos cambiar al restar 100 del valor del azulejo. La función encuentra el azulejo desencadenante en el nuevo levelData, que será la posición generada para nuestro héroe. He hecho que el desencadenante sea activado cuando la x es liberada; si solo escuchamos la tecla siendo presionada entonces finalizamos en un bucle donde cambiamos entre niveles mientras la tecla es presionada, ya que el personaje siempre se genera en el nuevo nivel arriba de un azulejo desencadenante.

Aquí está un demo funcionando. Intenta recoger elementos al caminar sobre ellos y cambiar niveles al pararte junto a puertas y pulsando x.

3. Proyectiles

Un proyectil es algo que se mueve en una dirección particular con una velocidad particular, como una bala, un hechizo mágico, una pelota, etc. Todo sobre el proyectil es lo mismo que el personaje héroe, aparte de la altura: más bien que rodar en el piso, los proyectiles a menudo flotan arriba de él a una cierta altura. Una bala viajará arriba del nivel de la cintura del personaje, e incluso una pelota podría necesitar rebotar.

Una cosa interesante a notar es que la altura isométrica es la misma altura en una vista lateral 2D, aunque más pequeña en valor. No  hay versiones complicadas involucradas. Si una bola es de 10 pixeles arriba del piso en coordenadas Cartesianas, podría estar 10 o 6 pixeles arriba del piso en coordenadas isométricas. (En nuestro caso, el eje relevante es el eje y.)

Intentemos implementar una pelota rebotando en nuestra pradera amurallada. Como un toque de realismo, agregaremos una sombra para la pelota. Todo lo que necesitamos hacer es agregar el valor de la altura del rebote al valor Y isométrico de nuestra pelota. El valor de la altura del salto cambiará de cuadro a cuadro dependiendo de la gravedad, y una vez que la pelota impacta el piso voltearemos la velocidad actual a lo largo del eje y.

Antes de que abordemos el rebote en un sistema isométrico, veremos cómo podemos implementarlo en un sistema Cartesiano 2D. Representemos el poder del salto de la pelota con una variable zValue. Imagina eso, para empezar, la pelota tiene una fuerza de salto de 100, así que zValue = 100.

Usaremos dos variables más: incrementValue, que comienza en 0, y gravity, que tiene un valor de -1. Cada cuadro, que restamos incrementValue de zValue, y restamos gravity de incrementValue para crear un efecto de atenuación. Cuando zValue llega a 0, significa que la pelota ha llegado al piso, en éste punto, volteamos el signo de incrementValue al multiplicarlo por -1, volteándolo a un número positivo. Ésto significa que la pelota se moverá hacia arriba desde el siguiente cuadro, rebotando así.

Aquí está cómo se ve en código:

El código sigue siendo el mismo para la vista isométrica también, con la pequeña diferencia que puedes usar un valor menor para ZValue para comenzar. Ve abajo cómo zValue es añadido al valor y isométrico de la pelota mientras se renderiza.

Consulta el ejemplo interactivo abajo:

Si entiendes que el rol desempeñado por la sombra es uno muy importante que agrega al realismo de ésta ilusión. También, nota que ahora estamos usando dos coordenadas de pantalla (x y y) para representar tres dimensiones en coordenadas isométricas-el eje-y en coordenadas de la pantalla también es el eje-z en coordenadas isométricas. ¡Esto puede ser confuso!

4. Encontrando y Siguiendo una Ruta 

Encontrar ruta y seguir ruta son procesos complicados. Hay varios planteamientos usando diferentes algoritmos para encontrar el camino entre dos puntos, pero como nuestro levelData es un arreglo 2D, las cosas son más fáciles de los que podrían ser de otra manera. Tenemos nodos bien definidos y únicos que el jugador puede ocupar, y podemos fácilmente verificar si son transitables.

Posts Relacionados

Una visión genera detallada de algoritmos de búsqueda de caminos está afuera del alcance de éste artículo, pero trataré de explicar la manera más común en que funciona: el algoritmo de ruta más corta, del cual A* y los algoritmos de Dijstra son implementaciones famosas.

Tenemos como objetivo encontrar nodos que conecten un nodo inicial y un nodo final. Desde el nodo inicial, visitamos los ocho nodos colindantes y los marcamos como visitados; el proceso central es repetido para cada nodo recientemente visitado, recursivamente.

Cada hilo de ejecución sigue los nodos visitados. Cuando salta a nodos colindantes, los nodos que ya han sido visitados son saltados (la recursión se detiene); de otra manera, el proceso continúa hasta que llegamos al nodo final, donde la recursión finaliza y la ruta completa seguida es retornada como un arreglo de nodo. A veces el nodo final nunca es alcanzado, en cuyo caso fracasa el encuentro de ruta. Generalmente finalizamos encontrando múltiples rutas entre dos nodos, en cuyo caso tomamos el que tiene el número más pequeño de nodos.

Encuentro de Ruta

No es inteligente reinventar la rueda cuando se trata de algoritmos bien definidos, así que usaríamos las soluciones existentes para nuestros propósitos de encontrar rutas. Para usar Phaser, necesitamos una solución JavaScrilpt, y la que he elegido es EasyStarJS. Inicializamos el motor de búsqueda de rutas como se ve abajo.

Como nuestro levelData tiene únicamente 0 y 1, podemos directamente pasarlo como el arreglo de nodo. Establecemos el valor de 0 como el nodo transitable. Habilitamos la capacidad de caminar diagonal pero deshabilitamos ésta cuando caminamos cerca de las esquinas de azulejos no transitables.

Ésto es porque, si lo habilitamos, el héroe puede cortar en azulejo no transitable mientras camina en diagonal. En tal caso, nuestra detección de colisión no permitirá al héroe pasar. También, por favor toma nota de que en el ejemplo he elimindo completamente la detección de colisión pues ya no es necesaria para un ejemplo de caminata basado en AI.

Detectaremos el toque en cualquier azulejo libre dentro del nivel y calculamos la ruta usando la función findPath. El método callback plotAndMove recibe el arreglo de nodo de la ruta resultante. Marcamos el minimap con la ruta recientemente encontrada.

Isometric level with the newly found path highlighted in minimap

Seguimiento de Ruta

Una vez que tenemos la ruta como un arreglo de nodo, necesitamos hacer que el personaje la siga.

Digamos que queremos hacer que el personaje camine hacia un azulejo en el que hacemos click. Primero necesitamos buscar una ruta entre el nodo que el personaje actualmente ocupa y el nodo donde hicimos click. Si una ruta exitosa es encontrada, entonces necesitamos mover el personaje al primer nodo en el arreglo de nodo al establecerlo como el destino. Una vez que obtenemos el nodo de destino, verificamos si hay más nodos en el arreglo de nodo, si es así, establece el siguiente nodo como el destino-y así sucesivamente hasta que lleguemos al nodo final.

También cambiaremos la dirección del jugador basándonos en el nodo actual y el nuevo nodo de destino cada vez que llaegamos a un nodo. Entre nodos, solo caminamos en la dirección requerida hasta que llegamos al nodo de destino. Éste es un muy simple AI, y en el ejemplo ésto se hace en el método aiWalk mostrado parcialmente abajo.

 Nosotors  si necesitamos filtrar puntos de click válidos al determinar si hemos hecho click dentro del área transitable, más bien que un azulejo de pared u otro azulejo no transitable.

Otro punto interesante para codificar el AI: no queremos que el personaje gire para avanzar al siguiente azulejo en el arreglo de nodo tan pronto como llegue al actual, pues un giro inmediato resulta en que nuestro personaje camine en los bordes de los azulejos. En cambio, deberíamos esperar hasta que el personaje esté unos cuantos pasos dentro del azulejo antes de que busquemos el siguiente destino. También es mejor colocar manualmente al héroe en el centro del azulejo actual justo antes de girar, para hacer que todo se sienta perfecto.

Consulta el demo que funciona abajo:

5. Scrolling Isométrico

Cuando el área de nivel es mucho más grande que el área disponible de la pantalla, necesitaremos hacerla scroll (desplazar).

Isometric level with 12x12 visible area

El área visible de la pantalla puede ser considerada como un rectángulo más pequeño dentro del rectángulo más grande del área completa de nivel. Scrolling es, esencialmente, solo mover el rectángulo interior dentro del más grande. Generalmente, cuando tal scrolling ocurre, la posición del héroe sigue siendo la misma con respeto al rectángulo de la pantalla, comúnmente en el centro de la pantalla. Interesantemente, todo lo que necesitamos para implementar el scrolling es dar seguimiento al punto de la esquina del rectángulo interior.

Éste punto de la esquina, que representamos en coordenadas Cartesianas, caerá dentro de un azulejo en los datos de nivel. Para scrolling, incrementamos la posción x y y del punto de la esquina en coordenadas Cartesianas. Ahora podemos convertir éste punto a coordenadas isométricas y las usamos para dibujar la pantalla.

Los valores recientemente convertidos, en el espacio isométrico, necesitan estar también en la esquina de nuestra pantalla, lo que significa que son los nuevos (0,0). Así, mientras se parsean y se dibujan los datos de nivel, restamos éste valor de la posición isométrica de cada azulejo, y podemos determnar si la nueva posición del azulejo cae dentro de la pantalla.

Alternativamente, podemos decidir que vamos a dibujar sólamente una grilla de azulejo isométrico X x Y en la pantalla para hacer el bucle de dibujo eficiente para niveles más grandes.

Podemos expresar ésto en pasos como:

  • Actualizar coordenadas Cartesianas x y y de los puntos de la esquina.
  • Convertir ésto a espacio isométrico.
  • Restar éste valor de la posición de dibujo isométrico de cada azulejo.
  • Dibujar únicamente un número limitado predefinido de azulejos en la pantalla iniciando dede ésta nueva esquina.
  • Opcional: Dibujar el azulejo sólamente si la nueva posición del dibujo isométrico cae dentro de la pantalla.

Por favor nota que el punto de la esquina es incrementado en la dirección opuesta a la posición actualizada del héroe mientras se mueve. Ésto asegura que el héroe permanezca donde está con respecto a la pantalla. Consulta éste ejemplo (usa flechas para scroll (desplazar), pulsar para incrementar la grilla visible).

Un par de notas:

  • Mientras te desplazas, podríamos necesitar dibujar azulejos adicionales en los bordes de la pantalla, o podríamos ver azulejos desapareciendo y apareciendo en los extremos de la pantalla.
  • Si tienes azulejos que ocupan má de un espacio, entonces necesitarás dibujar más azulejos en los bordes. Por ejemplo, si el azulejo más grande en todo el conjunto mide X por Y, entonces necesitarás dibujar X más azulejos a la izquierda y derecha y Y más azulejos a la parte superior e inferior. Ésto asegura que las esquinas del azulejo más grande será aún visible cuando se desplaza adentro o afuera de la pantalla.
  • Todavía necesitamos asegurar que no tengamos áreas en blanco en la pantalla mientras estamos dibujando cerca de los bordes del nivel.
  • El nivel debería únicamente desplazarse hasta que el azulejo más extremo sea dibujado en el correspondiente extremo de la pantalla-después de ésto, el personaje debería continuar moviéndose en el espacio de la pantalla sin el desplazamiento de nivel. Para ésto, necesitaremos dar seguimiento a las cuatro esquinas del rectángulo interior de la pantalla, y acelerar el desplazamiento y la lógica del movimiento del jugador consecuentamente. ¿Estás listo para el desafío de tratar de implementar eso por tí mismo?

Conclusión

Ésta serie particularmente está dirigida a principiantes que tratan de explorar los mundos del juego isométrico. Muchos de los conceptos explicados tienen planteamientos alternos que son un poco más complicados, y deliberadamente he elegido los más fáciles.

Podrían no cumplir la mayoría de los escenarios que pudieras encontrar, pero el conocimiento obtenido puede ser usado para construir a partir de éstos conceptos para crear soluciones más complicadas. Por ejemplo, el simple ordenamiento de profundidad implementado se romperá cuando tengamos niveles de múltiples pisos y azulejos de plataforma moviéndose de un piso a otro.

Pero ese es un tutorial para otro momento.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.