Física e interacción
En este post comparto los retos y aprendizajes que he tenido al implementar la física y las interacciones en mi primer videojuego usando Unreal Engine 5. A través de varias pruebas, errores y ajustes, explico cómo solucioné los problemas de colisiones y cómo llegué a una solución provisional usando solamente blueprints y evitando, por ahora, usar C++ para lograr la jugabilidad arcade que busco.
Marc Camps
10/11/20249 min leer


Como ya sabéis, este es mi primer videojuego y, además, soy un novato en Unreal Engine. A estas alturas he aprendido bastante y pienso que es la mejor opción para mi juego, pero no todas las soluciones son tan evidentes como me gustaría. Antes de plantearme si todos los objetos interactuaban correctamente a nivel físico, creé un “character” y me puse a programar la jugabilidad de recolección de basura. Lo hice primero porque me parecía algo complejo de desarrollar, ya que implica el uso de “inverse kinematics”. Sin embargo, no fue así.
Ahora, visto en perspectiva, creo que debería haber empezado por analizar cómo interactúan los diferentes elementos del juego en cuanto a física. Cuando creé el primer NPC lo basé en un “Pawn” y fue entonces cuando me di cuenta de que no podía interactuar correctamente con el jugador (character) al chocar entre sí o con otros elementos flotantes. Aquí fue donde empezó mi confusión, ya que no había aprendido ciertos conceptos básicos sobre “characters”, “pawns” y sus interacciones físicas. Además, comencé a crear al jugador con un “character” de forma un poco arbitraria, lo cual aumentó mi desconcierto.
Esperaba que las físicas reaccionaran como se espera simplemente "porque los elementos están en un mundo físico". He pasado bastante tiempo atascado con este tema. He recurrido a foros, webs, videos de YouTube, cursos, experimentos, y experimentado severas pérdidas de cordura, hasta que finalmente lo entendí. Ahora, he encontrado una solución que, por el momento, me sirve para mi prototipo. Antes de entrar en detalles, pongámonos en contexto para entender el problema y, posteriormente, la solución aplicada.
Jugabilidad deseada
Este es un juego de naves en vista isométrica en el que el objetivo es limpiar la órbita terrestre de basura espacial mientras esquivas y te defiendes de ataques enemigos, meteoritos, y otros eventos.
Por lo tanto, es necesario que el jugador y los NPC puedan chocar entre ellos o contra cualquier objeto inerte que esté flotando en el espacio. El resultado de estos choques debe hacer que ambos objetos reaccionen empujándose mutuamente, cumpliendo con el principio de "acción-reacción".
No debe ser una simulación perfecta, ya que sería un juego demasiado difícil, así que he simplificado varias cosas. Por ejemplo, los movimientos están limitados al plano XY, y la física no actúa en las tres dimensiones. Además, busco un control sencillo que se aleje de la simulación realista.
Aún así, habrá muchas colisiones, por lo que la física es clave y debe funcionar bien.
Problemas encontrados con la física
Primero, veamos los diferentes elementos que ofrece Unreal para crear el juego.
Blueprints basados en “Character”
Estos blueprints ofrecen muchas funcionalidades y, aparentemente, podrían ser una buena opción por dos motivos:
Permiten empujar objetos físicos del escenario.
Configuran fácilmente el movimiento del jugador mediante el “Character Movement Component”.






Sin embargo, acabé descartando esta opción porque, aunque el “Character” tiene muchas funcionalidades, la mayoría no me son útiles para mi juego:
Requiere una “Skeletal mesh” y no todos los NPC la necesitan. Podría ocultarla, pero ¿para qué tener un objeto que no se usa?
Obliga a tener una colisión en forma de cápsula vertical, mientras que mis objetos suelen ser naves alargadas horizontalmente. Activar la colisión por polígonos no es una opción óptima debido al alto coste computacional.
Intenté usar subobjetos para conseguir una colisión más parecida a la forma de la nave, pero no funcionó bien, ya que se requiere que el objeto de colisión sea el “root” del blueprint. Además, el “Character Movement Component” detiene la nave según la colisión principal.
El “Character Movement Component” tiene demasiadas opciones y complejidad para lo que necesito.


He intentado cosas muy raras con subobjetos tratando de conseguir una colisión personalizada pero solo he conseguido resultados bastante imprevisibles… es mejor no forzar el “character” y usarlo para lo que sirve: para crear, por ejemplo, un personaje humanoide vertical en un juego de acción.
Definitivamente a mi no me sirve.
Blueprints basados en “Pawn”
Los blueprints “Pawn” son mucho más simples que los “Character” y, por lo tanto, mucho más customizables. Como están prácticamente vacíos al crearlos, ofrecen mucho margen para configurar lo necesario. Además, igual que con el “Character”, se puede configurar el controlador que toma control del “Pawn”, ya sea una IA o un jugador.
Creé un “pawn”, le asigné una “skeletal mesh”, una colisión adecuada, el componente FloatingPawnMovement y la cámara.


Luego, dibujé algunas líneas en el Event Graph para que se moviera, pero cuando el pawn choca contra un objeto flotante…


Pum, es como si chocara contra un muro de hormigón armado.
El movimiento del “Pawn” funciona de manera similar al del “Character”, basándose en las colisiones del objeto “root” del blueprint. Sin embargo, no parece tener la capacidad de empujar mediante la física otros objetos, ya que el componente de movimiento es más simple.
Intenté programar manualmente el empuje en cada choque aplicando “AddImpulse” en el objeto flotante, pero me llevaba mucho tiempo, así que empecé a buscar una opción intermedia entre usar un “Pawn” casi vacío y un “Character” demasiado complejo.
Movimiento del “Pawn” basado en física
Si activo la opción “Simulate Physics” en el root del blueprint, se puede programar un sistema basado en físicas para mover tanto al jugador como a los NPC. En este caso, cuando chocan contra un objeto físico, lo empujan de manera normal.
El problema con este sistema es que genera una jugabilidad demasiado compleja, alejándose del estilo arcade que quiero para el juego. Además, el tiempo que me ahorraría programando las colisiones lo invertiría en desarrollar el movimiento del “Pawn”.
La solución correcta: controlador de movimiento propio para el “Pawn”
Según lo que he visto en foros y documentación, la mejor solución para mi caso parece ser modificar algún componente de movimiento existente para los “Pawn” y añadirle la funcionalidad física que tiene el “Character”. Esto implicaría usar C++ y profundizar en el código de Unreal Engine.
Creo que terminaré haciendo esto, pero antes quiero asegurarme de que el esfuerzo valga la pena. Mi prioridad ahora es comprobar si el proyecto es viable con un prototipo de la forma más sencilla posible, por lo que, de momento, he descartado esta opción.
También cabe destacar que es posible crear un componente de movimiento desde cero, lo que ofrece infinitas posibilidades, aunque eso también implicaría un mayor esfuerzo.
Mi solución extraña para el prototipo
La idea general para mi prototipo consiste en crear blueprints de tipo “Actor” que llamaremos “physics colliders” y que van a envolver a los blueprints de tipo “Pawn”, tanto para los NPC como para el jugador. El “Actor” se encargará de las colisiones con objetos físicos, mientras que el “Pawn” tendrá su propia colisión para evitar atravesar muros u otros “Pawns”.
Los “physics collision”
Como siempre intento aprovechar al máximo el código que desarrollo, uso herencia en todo lo que hago, y esta no es la excepción.
Primero, creo un blueprint de tipo “Actor” que llamo “BP_Base_PhysicsCollision”. Este es el que envolverá al “Pawn” y se encargará de las colisiones que impliquen empujar a otros objetos físicos, como la basura espacial.
Es importante recordar que la basura espacial serán actores bastante simples, con la opción “Simulate Physics” activada en su objeto “root”.
Para que esto funcione, tanto el pawn del jugador como los NPC deben ser capaces de generar y mover su “physics collider”. Para esto, he implementado en “BP_Base_PhysicsCollision” un evento público que asigna la transformación del “Pawn” (posición, rotación y escala), y como este blueprint es la base, estará presente en todos los blueprints que hereden de él.


El “Pawn” generará este actor en su misma posición al iniciar el juego (evento “Begin Play”) y luego, en cada “Tick”, actualizará su posición y rotación mediante “SetTransform”.
“BP_Base_PhysicsCollision” será el blueprint base para los distintos tipos de colisión que tendrá cada pawn. Por ejemplo, para el jugador, tendré “BP_Player_PhysicsCollision”, que hereda de la base “BP_Base_PhysicsCollision” y contiene los objetos de colisión necesarios.


Del mismo modo, cada NPC tendrá su propio “physics collision” basado en la misma estructura:
Jugador => BP_Player => BP_Player_PhysicsCollider
NPC => BP_NPC_Satellite => BP_NPC_Satellite_PhysicsCollider
NPC => BP_NPC_Enemy01 => BP_NPC_Enemy01_PhysicsCollider
Dentro de cada “physics collision”, colocaremos los objetos de colisión que mejor se adapten, ya sea una “static mesh” o una o más formas de colisión. Siempre hay que tener en cuenta que a mayor complejidad de colisión, mayor potencia de cálculo será necesaria, por lo que no conviene pasarse con colisiones excesivamente complejas.
Los “pawns”
Todos los “Pawns” deben gestionar su “physics collision”, lo que constituye un comportamiento común entre ellos, independientemente de si se trata del jugador o de un NPC. Por esta razón, he creado el blueprint base “BP_Base_PhysicsPawn”, del cual derivan tanto el pawn del jugador como los de los NPC.
Este blueprint genera su “physics collision” en la misma posición y rotación que el pawn al inicio del juego (evento “Begin Play”). Luego, en cada evento “Tick”, actualiza la posición y rotación del “physics collision” gracias a la lógica común implementada en el blueprint base.


Además, contiene el “Floating Pawn Movement Component” y una “static mesh” llamada “Pawn Collision”, que son comunes para todos los pawns.
Aquí, “PawnCollision” se encargará de evitar que el pawn atraviese muros u otros “pawns”, pero no se encargará de las colisiones con objetos flotantes, que son responsabilidad del “PhysicsCollision”.
Se establecen unas variables públicas de configuración, como la clase de “PhysicsCollision” que se usará para detectar colisiones con objetos físicos, y qué “static mesh” queremos usar para detectar colisiones con otros “pawns” y muros. La “static mesh” debe ser una versión simplificada de la mesh que determina el aspecto del pawn.
A partir de aquí, tengo el “BP_Player”, que es el jugador, y donde se implementa la lógica de movimiento a través de los controles. Este blueprint hereda de “BP_Base_PhysicsPawn” y es importante que se llamen los eventos “Begin Play” y “Tick” del blueprint base para ejecutar la lógica común.


En el apartado de “Class Defaults” se puede seleccionar la mesh de colisión del pawn y la clase que se usará para el “Physics Collision”.


Los canales de colisión
Llegados a este punto, pongo todo en marcha y… no funciona. El “BP_Player” debería moverse, pero no lo hace. Esto ocurre porque, al tener un objeto dentro de otro, entran en conflicto las colisiones.
Para solucionar esto, añadí en la configuración del proyecto un nuevo preset de colisiones llamado “ExceptPhysicsCollision”. Este preset bloquea todas las colisiones excepto las del tipo “Physic Body” y asigna el tipo “Pawn” al objeto donde se aplique. Por otro lado, a todas las colisiones de los actores “Physics Collision” les he aplicado el tipo “Physics Body” mediante el preset “Physics Actor”.
De esta forma, los pawns ignoran los “physics body” que forman el blueprint que los envuelve, permitiendo su movimiento.


Conclusión
Después de mucho experimentar, esta es la solución que he implementado para mi prototipo, que me ha permitido avanzar en el desarrollo sin complicarme en exceso. La idea es, más adelante, profundizar en un componente de movimiento propio en C++ para el juego definitivo. Por ahora, esta solución basada en blueprints es más que suficiente para validar la jugabilidad y las físicas que busco.



