Auto-generación de envolturas de colisión de alto rendimiento para personajes humanoides

Uno de los problemas más recurrentes dentro del desarrollo de videojuegos, es la generación de un sistema de colisiones para cada uno de los personajes con los que vamos a poder interactuar en ese aspecto (con colisiones), ya sea para detectar impactos tipo batalla entre personajes u obstaculizar con estas colisiones el movimiento de otros actores.

En este artículo voy a explicar de forma teórica una solución discreta, que he implementado y estoy utilizando en un videojuego que aun no he publicado, a la configuración manual de los colisionadores de cada personaje humanoide, que pueden ser muchos según la talla del proyecto y que limitan la actualización continua de los contenidos del juego por el alto consumo de recursos para incluir, en este caso concreto, nuevos personajes.

Sobre la mesa están también las soluciones más refinadas ya existentes para este problema (como los mesh colliders basados en la geometría del modelo), pero incido en el nivel de discreción, que es configurable, de este algoritmo, por tanto flexibilidad, su bajo coste (en tiempo de CPU); así como el ahorro en mantenimiento y actualización y por ende su utilidad en videojuegos para sistemas con recursos limitados o por lo menos sin garantía de homogeneidad en cuanto a rendimiento de los diferentes dispositivos sobre los que se va a distribuir el producto (que además puede querer tener una actualización continua de contenidos dentro de su estrategia de engagement). Hablamos de Android y no hace falta recordar la cantidad de desarrolladores con los que cuenta esta comunidad.

Vamos a plantear el problema con más claridad, la solución  y comentamos las ventajas y desventajas en el contexto del estado actual de la industria.

Problema: Configurar las colisiones de TODOS los personajes manualmente

Me topé con el problema que vengo a solucionar cuando, trabajando en el juego que antes he mencionado, llegó el momento de preparar los colisionadores de las extremidades y el cuerpo de cada uno de los personajes. En mi caso eran todos humanoides y voy a explicarlo dando por hecho que todos los modelos que mencione lo son, pero extrapolar la solución a otro tipo de esqueletos no tendría que ser complicado si a alguien le parece interesante seguir por ahí.

Rechazando el atajo de crear las colisiones una a una

Lo más sencillo hubiese sido asumir esta tarea como un trabajo repetitivo y configurar una a una las partes de cada personaje, también uno a uno y en mi opinión sujeto a tener un error manual y crear un bug difícil de localizar.

Empeñado entonces en convertir esta pesada tarea en un problema sujeto a ser automatizado ¿qué necesitamos para crear de forma dinámica un sistema de colisiones proceduralmente que funcione para todos los humanoides? Es decir, uno que nos de la holgura de tratar personajes de diferentes complexiones y con huesos adicionales a la estructura requisito de la solución.

Un esqueleto de animación humanoide
1. Un esqueleto de animación humanoide

Solución: obtener información sobre el esqueleto del modelo

Voy a explicar los motivos por los que me ha parecido una buena idea depositar la responsabilidad de dirigir las colisisones de un personaje sobre la información que nos da su esqueleto de animación (podemos ver un ejemplo de esqueleto utilizado para animar personajes (3D o imágenes deformables) en la imagen 1).

2. Colisionadores sobre una malla 3D

Qué necesitamos para dimensionar y posicionar los colisionadores

Resolviendo el problema con geometría, necesitamos:
      1. El centro de cada tramo del esqueleto con colisionador propio (El centro del antebrazo, del torso, de los muslos, etc)
      2. Los tamaños de los colisionadores.
Dependiendo del nivel de precisión que requiramos para nuestro caso, podemos decidir simplificar aun más la disección de personaje y crear un único colisionador por extremidad que vaya, en el caso de los brazos, desde el hombro hasta la mano y con las piernas desde la ingle hasta el talón. Además los colisionadores podrían ser todos cuadrados en lugar de cápsulas. Es una exposición teórica.

Cómo implementar la solución

A nivel de código los pasos que deberíamos seguir para hacer funcionar nuestro sistema de colisiones, independientemente de que se sustente sobre un motor físico (que será más caro computacionalmente hablando) o sobre un sistema más discreto de detección de colisiones, serían:

  1. Referenciar de forma estructurada, y respetando los patrones impuestos en la arquitectura, los huesos que componen el esqueleto de un personaje en el scope en el que manejamos la lógica del personaje (cuándo ataca, su ciclo de vida físico, etc). Damos por hecho que entonces tenemos un seguimiento de información geométrica de cada hueso en tiempo de ejecución.
  2. Dentro del ciclo de comportamiento del personaje, calculamos una vez por frame lógico las posiciones intermedias de las subdivisiones que hemos definido (de hombro a mano o de hombro a codo si tenemos que ser más precisos) y la longitud de cada uno de estos tramos, que en el plano geométrico vendría a ser la distancia euclídea entre pares de huesos que forman tramo con colisionador. En el caso de la cabeza y el torso la longitud del hueso ya es el tamaño de la figura de colisión.
  3. Junto al personaje se crean los colisionadores una única vez, es decir, la figura elegida para representar los colisionadores (cubo, cápsula) sobre la posición media correspondiente y con el tamaño que anteriormente hemos calculado. Estos valores se mapean continuamente para sincronizar las cajas con las posiciones de los huesos en cada momento.
3. Colisionadores generados con anchura por defecto

¿Y qué pasa con la anchura de los colisionadores?

Después de todo, nuestros colisionadores deberían quedar como vemos en la imagen 3. ¿Cómo solucionamos el tamaño la anchura de las figuras respetando lo anteriormente dicho de proveer una solución para personajes con complexiones dispares?

Parametrización por personajes

He intentado evitar la configuración individual de personajes, pero al final me ha servido con un par de parámetros.

Definiendo la anchura máxima de cada tramo de colisión del personaje con un valor por defecto que funcione para la mayoría de casos y prevalece en caso de ausencia de configuración específica de un personaje.

Ventajas y desventajas de esta solución

Para ser un poco más objetivo y tener una valoración real de la solución, voy a exponer las ventajas y desventajas que se me ocurren de este método de auto distribución.

Ventajas

  • Independencia de la malla y tiempo de cómputo reducido en comparación con soluciones de colisión por malla que además no distinguen en muchos casos entre diferentes partes de una extremidad.
  • Ahorro en tiempo de configuración de personajes y actualizaciones: reducido a los valores de anchura y si queremos añadir algún offset de longitud para corregir errores de precisión entre la posición de los huesos respecto a la malla.
  • Podemos activar y desactivar estas colisiones de forma independiente en cualquier momento del ciclo de vida del personaje ya que los creamos y guardamos referencia a ellos desde el scope de la lógica de dicho personaje.
  • Podemos obviar huesos del esqueleto que no nos aporten información relevante para las colisiones (decoraciones del personaje manejadas con huesos: sombreros, etc)

Desventajas

  • Requiere que los personajes sigan un estructura de huesos determinada para encajar dentro del script de cálculo de posiciones y tamaños (aunque esto es punto positivo en organización de los recursos del proyecto más que una desventaja)
  • Imprecisión entre los colisionadores y la malla del personaje directamente proporcional a la inexactitud de la colocación de los huesos en el proceso de modelado de animación del personaje (diseño).

Conclusiones

¿Cuántas más tareas podríamos automatizar si pudieramos garantizar fidelidad entre los números que nos ofrece la mecánica de un personaje y la malla a la que está asignada dicho mecano? Si hubiera una información adicional abstraída como partes del cuerpo relacionada con el equeleto de los modelos que a diario se utilizan en desarollo de videojuegos o proyectos de animación , que de nuevo recuerdo que pueden ser 2D o 3D.

Se me ocurren diferentes escenarios en los que podríamos aprovechar la garantía comentada si existiera un compromiso entre diseño e interacción más firme:

Ejemplo: Automatización de cámaras y dirección procedural de animaciones

Automatizar encuadres de diferentes planos de cámara para personajes concretos definiendo qué partes del cuerpo hay en escena en cada momento, qué planos queremos reproducir y parametrizando rotaciones de cámara y offsets de desencuadre, personajes no tangibles (entidades que son relación entre actores en una escena), etc.

En la práctica, tendríamos la cámara haciendo un primer plano a un personaje concreto definiendo los parámetros:
  • Función movimiento transición de cámara o corte limpio
  • Plano que queremos (primer plano en este caso)
  • Y el identificador de nuestro actor objetivo

Espero que a alguien le sirva esta aproximación para resolver un problema tan común como cansado que es el preparar los modelos para colisionar.

¿Se te ocurre alguna mejora?

Entradas populares de este blog

Snake 2000: Classic Nokia game (Android App)

Sliding Puzzles: Lost Blocks (Android App)