23 abril 2025

Cómo detectar obstáculos en Unity 2D

En los juegos es bastante habitual tener que averiguar si un punto del escenario está libre de obstáculos para poder ubicar a un personaje o a otro elemento del juego. Piensa por ejemplo en un RTS: a la hora de construir un edificio tienes que elegir una sección de terreno libre ¿pero cómo puede saber tu código si un emplazamiento tiene o no ya un edificio u otro tipo de objeto?

En un juego 3D, la solución más habitual suele ser proyectar un rayo desde el foco de visión de la cámara, pasando por el punto donde se sitúa el cursor del ratón, en el plano de la pantalla, hasta chocar con un collider. Si el collider es el del suelo, ese punto está libre, y si no es que hay un obstáculo.

Claro, si el objeto que queremos colocar es más grande que un punto, proyectar un simple rayo se nos queda corto. Imagina que queremos colocar un edificio rectangular y el punto donde iría su centro está libre, pero la zona de la esquina no. Afortunadamente, para esos casos, Unity nos permite proyectar formas complejas, más allá de un mero punto. Por ejemplo, los métodos SphereCast permiten desplazar una esfera invisible a lo largo de una línea, devolviendo el primer collider con el que tope, Otro de los métodos, BoxCast, permitiría resolver la problemática del edificio rectangular proyectando una caja de base rectangular a lo largo de una línea. No tendríamos más que hacer esa proyección a lo largo de una línea vertical a la posición del suelo que queremos revisar.

En 2D, también hay métodos de proyección, BoxCast y CircleCast, pero sólo sirven cuando la proyección tiene lugar en el plano XY (el de la pantalla). Es decir, equivalen a mover una caja o un círculo en línea recta, a lo largo de la pantalla, a ver si tocan un collider. Por supuesto, eso tiene su utilidad. Imagina que estás haciendo un juego cenital y quieres comprobar si el personaje será capaz de atravesar una abertura en un muro. En ese caso te bastará con hacer un CircleCast de un círculo, con un diámetro como el ancho de hombros de nuestro personaje, proyectando a través de la abertura para ver si el círculo llega a rozar los collider del muro. 

Un CircleCast, proyectando un círculo a lo largo de un vector.
Un CircleCast, proyectando un círculo a lo largo de un vector.

¿Pero qué pasa cuando tienes que proyectar en el eje Z en un juego 2D? Por ejemplo, para un caso en 2D equivalente al ejemplo que poníamos antes para 3D. Ahí ya no nos servirían ni BoxCast ni CircleCast porque esos métodos definen el vector de proyección mediante un parámetro Vector2, limitado al plano XY. En esos casos, se usa una familia diferente de métodos: los métodos "Overlap".

Los métodos Overlap colocan una forma geométrica en un punto determinado del espacio 2D y, en caso de que la forma se superponga con algún collider, lo devuelven. Al igual que las proyecciones, hay métodos especializados en diferentes formas geométricas: OverlapBox, OverlapCapsule u OverlapCircle entre otros.

Supongamos un caso como el de la figura siguiente. Queremos saber si una forma con el tamaño del círculo rojo tocaría algún obstáculo (en negro) en caso de ubicarse en el punto marcado en la figura. 

Ejemplo de uso de OverlapCircle.
Ejemplo de uso de OverlapCircle.


En ese caso, usaríamos OverlapCircle para "dibujar" un circulo invisible en ese punto (el círculo que se ve en la figura es un mero gizmo) y comprobaríamos si el método devuelve algún collider. De no ser así, significaría que el emplazamiento elegido estaría libre de obstáculos.

Un método que llamase a OverlapCircle podría ser tan sencillo como el siguiente:

Llamada a OverlapCircle
Llamada a OverlapCircle

El método de la figura devuelve verdadero si no hay ningún collider en un radio  (MinimumCleanRadius) de la posición candidateHidingPoint. En caso de que haya algún collider, el método devolverá false. Para eso, el método IsCleanHidingPoint se limita a llamar a OverlapCircle, pasándoles como parámetros:

  • candidateHidingPoint (línea 224): Un Vector2 con la posición del centro del círculo a trazar.
  • MinimumCleanRadius (línea 225): Un float con el radio del círculo.
  • NotEmpyGroundLayers (línea 226): Un LayerMask con las capas de los collider que queremos detectar. Sirve para filtrar los collider que no queramos detectar. OverlapCircle descartará un collider que no se encuentre en una de las capas que hayamos pasado en el LayerMask.

En caso de que el área esté limpia de colliders, OverlapCircle devolverá null. En caso de que soí los haya, devolverá el primer collider que encuentre. Si te interesase conseguir el listado de todos los collider que pudiera haber en el área podríamos usar la variante OverlapCircleAll, que devuelve una lista con todos ellos.

Podríamos finalizar aquí, pero no quiero hacerlo sin advertirte de un quebradero de cabeza que te encontrarás sin duda en 2D. Por suerte, puede solucionarse fácilmente si estás avisado. 

El problema puede darse si usas tilemaps. Estos son muy habituales para dar forma a los escenarios en 2D. La cuestión es que para conformar los collider de un tilemap, lo normal es usar un componente "Tilemap Collider 2D", y también es bastante habitual añadir un componente "Composite Collider 2D" para sumar todos los collider individuales de cada tile en uno sólo y mejorar así el rendimiento. El problema viene de que por defecto el componente "Composite Collider 2D" genera un collider hueco, sólo definido por su perfil. Supongo que lo hace por rendimiento.  Eso ocurre cuando el parámetro "Geometry Type" tiene el valor Outlines.

Valores posibles del parámetro Geometry Type.
Valores posibles del parámetro Geometry Type.

¿Por qué supone un problema que el collider sea hueco? porque en ese caso la llamada a OverlapCircle sólo detectará el collider si el círculo que dibuje llega a cruzarse con el borde del collider. Si por el contrario, el círculo encajase limpiamente dentro del collider, sin tocar ninguno de sus bordes, entonces OverlapCircle no devolvería collider alguno y supondríamos erróneamente que la zona estaría limpia.

La solución es sencilla, una vez que te lo han explicado. Hay que cambiar el valor por defecto de "Geometry Type" a Polygons. Este valor hace que el collider generado sea "macizo" por lo que OverlapCollider lo detectará aunque el círculo dibujado encaje dentro sin tocar sus bordes.

Parece una tontería, porque lo es, pero fue una tontería que me llevó un par de horas resolver hasta que conseguí dar con la tecla. Espero que este artículo sirva para que no te pase a ti también.