Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Platformer
Gamedevelopment

Física básica de plataformas en 2D, parte 2

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Basic 2D Platformer Physics .
Basic 2D Platformer Physics, Part 1
Basic 2D Platformer Physics, Part 3

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Geometría de nivel

Hay dos enfoques básicos para construir niveles de plataformas. Una de ellas es usar una cuadrícula y colocar las fichas apropiadas en las celdas, y la otra es de forma más libre, en la que puede colocar libremente la geometría de nivel sin importar donde desee.

Hay ventajas y desventajas para ambos enfoques. Usaremos la grilla, así que veamos qué tipo de ventajas tiene sobre el otro método:

  • Mejor detección de colisión de rendimiento contra la red es más barato que contra objetos colocados libremente en la mayoría de los casos.
  • Hace que sea mucho más fácil manejar buscatrazos.
  • Los mosaicos son más precisos y predecibles que los objetos colocados libremente, especialmente cuando se consideran terrenos destructibles.

Construyendo una clase de mapa

Comencemos creando una clase Map. Retendrá todos los datos específicos del mapa.

Ahora tenemos que definir todas las fichas que contiene el mapa, pero antes de hacerlo, necesitamos saber qué tipos de fichas existen en nuestro juego. Por ahora, estamos planeando solo tres: un mosaico vacío, un mosaico sólido y una plataforma de una sola vía.

En la demostración, los tipos de mosaicos corresponden directamente al tipo de colisión que nos gustaría tener con un mosaico, pero en un juego real eso no es necesariamente así. Como tiene más mosaicos visualmente diferentes, sería mejor agregar nuevos tipos como GrassBlock, GrassOneWay, etc., para permitir que la enumeración de TileType defina no solo el tipo de colisión sino también la apariencia del mosaico.

Ahora en la clase de mapa podemos agregar una matriz de mosaicos.

Por supuesto, un mapa de mosaico que no podemos ver no nos sirve de mucho, por lo que también necesitamos sprites para hacer una copia de seguridad de los datos del mosaico. Normalmente en Unity es extremadamente ineficiente tener cada tesela como un objeto separado, pero como solo estamos usando esto para probar nuestra física, está bien hacerlo de esta manera en la demostración.

El mapa también necesita una posición en el espacio mundial, de modo que si necesitamos tener más de uno solo, podemos separarlos.

Ancho y alto, en mosaicos.

Y el tamaño del mosaico: en la demostración, trabajaremos con un tamaño de mosaico bastante pequeño, que es de 16 por 16 píxeles.

Eso sería todo. Ahora necesitamos un par de funciones de ayuda para permitirnos acceder a los datos del mapa fácilmente. Comencemos haciendo una función que convertirá las coordenadas del mundo a las coordenadas del mosaico del mapa.

Como puede ver, esta función toma un Vector2 como parámetro y devuelve un Vector2i, que es básicamente un vector 2D que opera en enteros en lugar de flotantes.

La conversión de la posición mundial a la posición del mapa es muy sencilla: simplemente tenemos que cambiar el point por mPosición, de modo que devolvemos el mosaico relativo a la posición del mapa y luego dividimos el resultado por el tamaño del mosaico.

Tenga en cuenta que tuvimos que cambiar point  adicionalmente por cTileSize / 2.0f, porque el pivote de la pieza está en el centro. Hagamos también dos funciones adicionales que devolverán solo el componente X e Y de la posición en el espacio del mapa. Será útil más tarde.

También debemos crear una función complementaria que, dado un mosaico, devolverá su posición en el espacio mundial.

Además de traducir posiciones, también necesitamos tener un par de funciones para ver si un mosaico en cierta posición está vacío, si es un mosaico sólido o si es una plataforma de un solo sentido. Comencemos con una función GetTile muy genérica, que devolverá un tipo de mosaico específico.

Como puede ver, antes de devolver el tipo de mosaico, verificamos si la posición dada está fuera de límites. Si es así, entonces queremos tratarlo como un bloque sólido; de lo contrario, devolveremos un tipo verdadero.

El siguiente en cola es una función para verificar si un azulejo es un obstáculo.

De la misma manera que antes, comprobamos si el mosaico está fuera de límites, y si es así, volvemos a verdadero, por lo que cualquier loseta fuera de límites se trata como un obstáculo.

Ahora vamos a verificar si el azulejo es una loseta de tierra. Podemos pararnos tanto en un bloque como en una plataforma unidireccional, por lo que debemos volver verdadero si el mosaico es uno de estos dos.

Finalmente, agreguemos las funciones IsOneWayPlatform e IsEmpty de la misma manera.

Eso es todo lo que necesitamos que haga nuestra clase de mapas. Ahora podemos avanzar e implementar la colisión de personajes contra ella.

Colisión en mapa de caracteres

Regresemos a la clase MovingObject. Necesitamos crear un par de funciones que detectarán si el personaje está colisionando con el mapa de mosaicos.

El método por el cual sabremos si el personaje colisiona con un mosaico o no es muy simple. Comprobaremos todas las fichas que existen justo fuera de la AABB del objeto en movimiento.


El cuadro amarillo representa el AABB del personaje, y vamos a verificar los mosaicos a lo largo de las líneas rojas. Si alguno de ellos se superpone con un mosaico, establecemos una variable de colisión correspondiente en verdadero (como mOnGround, mPushesLeftWall, mAtCeiling o mPushesRightWall).

Comencemos creando una función HasGround, que verificará si el personaje colisiona con una ficha de tierra.

Esta función devuelve verdadero si el carácter se superpone con cualquiera de los mosaicos inferiores. Toma la posición anterior, la posición actual y la velocidad actual como parámetros, y también devuelve la posición Y de la parte superior de la casilla con la que estamos colisionando y si la ficha colisionada es una plataforma unidireccional o no.

Lo primero que queremos hacer es calcular el centro de AABB.

Ahora que lo tenemos, para el control de colisión inferior necesitaremos calcular el principio y el final de la línea del sensor inferior. La línea del sensor está a solo un píxel por debajo del contorno inferior del AABB.

El bottomLeft y bottomRight representan los dos extremos del sensor. Ahora que tenemos esto, podemos calcular qué fichas debemos verificar. Comencemos creando un bucle en el que revisaremos las teselas de izquierda a derecha.

Tenga en cuenta que no hay ninguna condición para salir del bucle aquí; lo haremos al final del bucle.

Lo primero que debemos hacer en el ciclo es asegurarnos de que checkedTile.x no sea mayor que el extremo derecho del sensor. Este podría ser el caso porque movemos el punto marcado por múltiplos del tamaño del mosaico, por lo que, por ejemplo, si el personaje tiene 1.5 azulejos de ancho, necesitamos verificar el mosaico en el borde izquierdo del sensor, luego un mosaico a la derecha , y luego 1.5 fichas a la derecha en lugar de 2.

Ahora necesitamos obtener la coordenada del mosaico en el espacio del mapa para poder verificar el tipo de mosaico.

Primero, calculemos la posición superior de la loseta.

Ahora, si el mosaico actualmente marcado es un obstáculo, podemos devolverlo fácilmente.

Finalmente, verifiquemos si ya miramos a través de todos los mosaicos que se cruzan con el sensor. Si ese es el caso, entonces podemos salir del circuito de forma segura. Después de que salimos del bucle y no encontramos una ficha con la que colisionamos, necesitamos devolverlfalse para que la persona que llama sepa que no hay tierra debajo del objeto.

Esa es la versión más básica del cheque. Intentemos que funcione ahora. De vuelta en la función UpdatePhysics, nuestra verificación de tierra antigua se ve así.

Reemplácelo usando el método recién creado. Si el personaje se cae y hemos encontrado un obstáculo en nuestro camino, entonces tenemos que sacarlo de la colisión y establecer el mOnGround en verdadero. Comencemos con la condición.

Si la condición se cumple, tenemos que mover el personaje en la parte superior del azulejo con el que colisionamos.

Como puede ver, es muy simple porque la función devuelve el nivel del suelo al que debemos alinear el objeto. Después de esto, solo tenemos que establecer la velocidad vertical en cero y establecer mOnGround en verdadero.

Si nuestra velocidad vertical es mayor que cero o no estamos tocando ningún piso, debemos establecer el mOnGround en false.

Ahora veamos cómo funciona esto.

Como puede ver, ¡funciona bien! La detección de colisión para las paredes en ambos lados y en la parte superior del personaje todavía no está allí, pero el personaje se detiene cada vez que toca el suelo. Todavía tenemos que poner un poco más de trabajo en la función de comprobación de colisión para que sea robusto.

Uno de los problemas que debemos resolver es visible si la compensación del personaje de un cuadro al otro es demasiado grande para detectar la colisión correctamente. Esto se ilustra en la siguiente imagen.

Esta situación no ocurre ahora porque hemos bloqueado la máxima velocidad de caída a un valor razonable y actualizamos la física con una frecuencia de 60 FPS, por lo que las diferencias en las posiciones entre los fotogramas son bastante pequeñas. Veamos qué sucede si actualizamos la física solo 30 veces por segundo.

Como puede ver, en este escenario nuestra prueba de colisión de tierra nos falla. Para solucionar esto, no podemos simplemente verificar si el personaje tiene tierra debajo de él en la posición actual, sino que más bien necesitamos ver si hubo obstáculos en el camino desde la posición del cuadro anterior.

Volvamos a nuestra función HasGround. Aquí, además de calcular el centro, también queremos calcular el centro del cuadro anterior.

También necesitaremos obtener la posición del sensor del cuadro anterior.

Ahora tenemos que calcular en qué mosaico verticalmente vamos a comenzar a verificar si hay una colisión o no, y en el que nos detendremos.

Comenzamos la búsqueda desde el mosaico en la posición del sensor del cuadro anterior, y lo terminamos en la posición del sensor del fotograma actual. Eso es, por supuesto, porque cuando verificamos una colisión en el suelo suponemos que estamos cayendo, y eso significa que nos estamos moviendo de la posición más alta a la inferior.

Finalmente, necesitamos tener otro ciclo de iteración. Ahora, antes de llenar el código para este ciclo externo, consideremos el siguiente escenario.

Aquí puedes ver una flecha moviéndose rápidamente. Este ejemplo muestra que no solo necesitamos iterar a través de todas las teselas que necesitaríamos pasar verticalmente, sino también interpolar la posición del objeto para cada tesela por la que pasamos para aproximar la ruta desde la posición del marco anterior al actual. Si simplemente seguimos usando la posición del objeto actual, en el caso anterior se detectaría una colisión, aunque no debería ser así.

Cambiemos el nombre de bottomLeft y bottomRight como newBottomLeft y newBottomRight, por lo que sabemos que estas son las posiciones del sensor del nuevo fotograma.

Ahora, dentro de este nuevo bucle, vamos a interpolar las posiciones de los sensores, de modo que al comienzo del bucle asumimos que el sensor está en la posición del fotograma anterior, y al final va a estar en la posición del fotograma actual.

Tenga en cuenta que interpolamos los vectores en función de la diferencia en los mosaicos en el eje Y. Cuando las posiciones viejas y nuevas están dentro del mismo mosaico, la distancia vertical será cero, por lo que en ese caso no podríamos dividir por la distancia. Entonces, para resolver este problema, queremos que la distancia tenga un valor mínimo de 1, de modo que si ocurriera tal escenario (y va a suceder muy a menudo), simplemente usaremos la nueva posición para detección de colisión.

Finalmente, para cada iteración, necesitamos ejecutar el mismo código que ya hicimos para verificar la colisión de tierra a lo largo del ancho del objeto.

Eso es practicamente todo. Como se puede imaginar, si los objetos del juego se mueven realmente rápido, esta forma de verificar la colisión puede ser un poco más cara, pero también nos asegura que no habrá fallas extrañas con objetos que se muevan a través de paredes sólidas.

Resumen

Uff, eso fue más código de lo que pensamos que necesitaríamos, ¿no? Si detecta algún error o atajos posibles, ¡déjenmelo a todos en los comentarios! La prueba de colisión debe ser lo suficientemente robusta como para que no tengamos que preocuparnos por los eventos desafortunados de los objetos que se deslizan por los bloques del mosaico.

Gran parte del código fue escrito para asegurarnos de que no haya objetos que pasen a través de los mosaicos a gran velocidad, pero si eso no es un problema para un juego en particular, podríamos eliminar el código adicional de manera segura para aumentar el rendimiento. Incluso podría ser una buena idea tener una bandera para objetos específicos que se mueven rápidamente, de modo que solo aquellos usen las versiones más costosas de los chequeos.

Todavía tenemos muchas cosas por cubrir, pero logramos hacer un control de colisión confiable para el terreno, que puede reflejarse de manera bastante directa en las otras tres direcciones. Lo haremos en la siguiente parte.

Advertisement
Advertisement
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.