Mecánicas como servicios

En esta ocasión me gustaría cubrir un tema muy relacionado a las buenas practicas y arquitectura limpia en Unity. Se trata de un concepto que he estado investigando últimamente sobre un patrón de implementación a la hora de trabajar en mecánicas especificas de un juego. El objetivo final de esto es evitar la siguiente maraña de banderas que se ve muy comúnmente en el código de juegos:

mess.png
En este ejemplo usamos una bandera isDashing en el update para que una misma clase haga diferentes cosas.

No basta decir que el código de arriba no solo es ilegible a simple vista, sino que va a ser un dolor de cabeza a la hora de agregar nuevas mecánicas que interactuen con el movimiento sin romper nada previo.

La propuesta de esta publicación en particular es estructurar nuestras mecánicas de juego como servicios.  Para esto vamos a intentar acercarnos lo mas posible a un patrón de diseño de servicios. Lo esencial de este patrón es estructurar la clase para que se consuma similar a como seria un servicio web por ejemplo. Una API web expone métodos para que sus clientes accedan a las funcionalidades de dicho servicio, a veces los métodos retornan algo, a veces son de actualización o borrado de datos, etc.

Para cumplir con un patrón de servicio, nuestra clase tiene que tener una única responsabilidad, contar con dependencias concretas a otros servicios si es necesario y exponer su invocación de una manera clara y concisa. También un servicio debería tener un único control sobre su propio estado, es decir, nadie debería poder detener un servicio desde una llamada concreta.

En este caso implementaremos una funcionalidad de movimiento ideal para un juego de perspectiva top-down, similar a casos como Binding of Isaac, Enter the Gungeon, Children of Morta, etc. Vamos a plantear el movimiento, la detección de colisiones y una acción de Dash como diferentes servicios. Al estructurar nuestro código con este patrón también quedara claro quienes van a ser los clientes de dichos servicios.

Movimiento e Input

Empecemos entonces por la clase de movimiento, dato importante, el input del usuario NO esta comprendido en la clase de movimiento, de hecho la clase de movimiento no debería contar con un método Update ya que no tiene ninguna independencia.

movement-1
Las implementaciones de vista son únicamente para manejar las animaciones, pueden ser ignoradas en este paso o pueden ser extraídas del código del repositorio listado al final del post.

Algo que seguramente varios notaron del código anterior, es la inclusión del método Initialize, esto es una buena practica de servicios, tener un constructor o inicializador para pasar dependencias, nos ahorra tener que asociar el mismo componente a muchos scripts desde el editor. Y considerando que estamos trabajando en un personaje, seguramente hay mas profundidad que solo movimiento en las mecánicas que manejemos.

character
La clase Character va a ser nuestro punto de entrada y salida para manipular todo lo que sea relevante a un personaje, también va a encargarse de la inicialización de sus servicios.

Si analizamos nuevamente la clase de CharacterMovement, lo único que se expone públicamente ademas de la inicialización, es la función MoveFromVector(Vector3 vector). Esto es porque CharacterMovement esta propuesto como servicio y no como un script independiente. El cliente de CharacterMovement va a ser PlayerInput

input-1
Un simple script de input que maneja la entrada de comandos físicos y consume el servicio de movimiento.

Es hora de probar nuestro código, armemos la escena con un objeto Player que tenga PlayerInput y un objeto Runner que tenga los componentes de Character y CharacterMovement, una vez que este todo debidamente vinculado en el inspector podemos ver como nuestro personaje se mueve.

runner-1

walk
El juego debería verse asi, tenemos a nuestro personaje corriendo por input físico pero no tenemos colisiones.

Detección de colisiones

El próximo paso es implementar la detección de colisiones, para esto vamos a crear otro comportamiento como servicio. Este servicio va a tener la única responsabilidad de detectar si hay algún obstáculo hacia donde nos queremos mover. Para eso vamos a crear una nueva clase, ObstacleDetection

detector
El detector de obstáculos simplemente usa raycast para saber si hay algún obstáculo hacia la dirección y a la distancia que le pidan. No tiene la responsabilidad de detener el movimiento ni hacer un sonido ni nada de eso. Simplemente va a devolver un valor boolean.

Con este comportamiento a nuestra disposición, ahora podemos usarlo desde el movimiento para determinar si el personaje puede o no moverse.

movement-2
Para esto vamos a modificar nuestra función principal en CharacterMovement, le vamos a agregar una nueva validación a la operación del movimiento.

Esto tiene una excelente consecuencia, solo estamos chequeando colisiones cuando lo necesitamos, es decir, cuando el personaje se mueve. La única responsabilidad de ese servicio es verificar si hay o no un obstáculo hacia una dirección a cierta distancia. También queda claro quien va a ser el cliente de dicho servicio, CharacterMovement.

runner-2

obstacles-1
Teniendo nuestro servicio de detección de obstáculos, la navegación se torna un poco mas lógica.

Caso para pensar

En caso de que el juego tuviera una mecánica donde hay obstáculos móviles que lo empujan, ¿Como se implementaría dicha funcionalidad sin usar física y pensando como si fuera un servicio?

Mecánica de Dash

Por ultimo, implementemos la mecánica de dash, nuevamente planteándola como un servicio, primero analicemos los componentes de un dash. Podemos decir que un dash es un movimiento lineal en un vector con una velocidad y duración dada, por ende tomemos como parámetros invariables la duración y la velocidad, y el vector sera nuestro parámetro variable. Es decir, cada vez que se llama al dash varia el vector pero no la velocidad ni la duración del mismo. Pero también sabemos que el dash tiene una animación asociada y también tenemos que detectar colisiones, así que podemos decir que el servicio de dash depende del animador y de la detección de colisiones.

dash
Nuevamente, dash es un servicio, solo expone su inicialización y el método Do(Vector3 direction) y por dentro usamos la bandera enable de todo componente de Unity para controlar el estado.

A nivel de cliente, nuevamente la clase CharacterMovement debería consumir Dash, exponiendo una función mas simplificada hacia afuera. Es decir, nuestro CharacterMovement cuenta con un wrapper para esta funcionalidad.

movement-3
Agregamos la función boolean CanMove para validar el estado del dash y si hay o no obstáculo en frente. La bandera isDashing del primer ejemplo de código ya no esta en el estado de este script sino que es dinamicamente extraído del componente de Dash

Por ende, desde la clase PlayerInput vamos a estar consumiendo el método Dash de CharacterMovement y no directamente el Do de Dash. Esta decisión la tomamos porque la validación debería venir del movimiento y no del input.

input-2

dash
El producto final, todos los servicios integrados y un movimiento fluido y aun mas importante, confiable!

Conclusiones

Plantear mecánicas de juego como servicios nos permite modularizar comportamientos y evitar la maraña de banderas y condicionales. Si podemos tener la misma funcionalidad con menos variables entonces nuestro código es menos propenso a romperse y es mas fácil de escalar. Próximamente publicare un nuevo post extendiendo este concepto basado en tener habilidades (diferentes ataques por ejemplo) como servicios, y mostrar como es fácil agregar o quitar, haciendo que nuestro personaje pueda tener un inventario dinámico de habilidades listas para ser usadas sin dependencias estrictas.

Mantener el patrón es muy importante, especialmente a la hora de propagación de dependencias, de hecho algo que hice a propósito fue omitir algunas propagaciones de dependencias, si quieren ponerse un pequeño desafió, encuentren donde están esas omisiones y refactoricen.

¿Que mas podemos hacer con esto?

Pueden intentar seguir con otra mecánica, por ejemplo implementar un salto e implementarlo como su propio servicio.

De ahora en mas dejare un link a mis repositorios abiertos con todo el código y los recursos que use para hacer el ejercicio. Muchas gracias por leer y gracias a Kenney por sus excelentes packs de assets gratuitos.

Link al repositorio

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s