Al implementar patrones de diseño de programación de juegos comunes en su proyecto Unity, puede construir y mantener de manera eficiente una base de código limpia, organizada y legible. Los patrones de diseño no sólo reducen la refactorización y el tiempo dedicado a las pruebas, sino que también aceleran los procesos de incorporación y desarrollo, contribuyendo a crear una base sólida para el crecimiento de su juego, equipo de desarrollo y negocio.
Piense en los patrones de diseño no como soluciones acabadas que puede copiar y pegar en su código, sino como herramientas adicionales que pueden ayudarle a crear aplicaciones más grandes y escalables cuando se utilizan correctamente.
Esta página examina el patrón State y cómo puede facilitar la gestión de su código base.
El contenido se basa en el libro electrónico gratuito, Level up your code with game programming patternsque explica patrones de diseño bien conocidos y comparte ejemplos prácticos para utilizarlos en tu proyecto Unity.
Otros artículos de la serie de patrones de diseño de programación de juegos de Unity están disponibles en el hub de mejores prácticas de Unity, o, haz clic en los siguientes enlaces:
Imagina que construyes un personaje jugable. En un momento dado, el personaje puede estar de pie en el suelo. Mueve el mando y parece que corre o camina. Pulsa el botón de salto y el personaje saltará en el aire. Unos fotogramas más tarde, aterriza y vuelve a su posición de reposo, de pie.
La interactividad de los juegos de ordenador requiere el seguimiento y la gestión de muchos sistemas que cambian en tiempo de ejecución. Si dibujas un diagrama que represente los diferentes estados de tu personaje, puede que te salga algo como la imagen de arriba:
Se parece a un diagrama de flujo, con algunas diferencias:
- Consta de varios estados (quieto/parado, caminando, corriendo, saltando, etc.) y sólo uno de ellos está activo en cada momento.
- Cada estado puede desencadenar una transición a otro estado en función de las condiciones en tiempo de ejecución.
- Cuando se produce una transición, el estado de salida pasa a ser el nuevo estado activo.
Este diagrama ilustra algo llamado máquina de estados finitos (FSM). En el desarrollo de juegos, un caso típico de uso de un FSM es el seguimiento del estado interno de un accesorio o un "actor del juego", como el personaje jugable. Hay muchos casos de uso para un FSM en el desarrollo de juegos, y si usted tiene alguna experiencia desarrollando un proyecto en Unity, es probable que ya haya empleado un FSM en el contexto de las Máquinas de Estado de Animación en Unity.
Un FSM se define mediante una lista de sus estados. Tiene un estado inicial con condiciones para cada transición. Un FSM puede estar exactamente en uno de un número finito de estados en un momento dado, con la posibilidad de cambiar de un estado a otro en respuesta a entradas externas que dan lugar a una transición.
El patrón de diseño State, por otro lado, define una interfaz que representa un estado y una clase que implementa esta interfaz para cada estado. El contexto, o la clase, que necesita alterar su comportamiento basado en el estado mantiene una referencia al objeto de estado actual. Cuando el estado interno del contexto cambia, simplemente actualiza la referencia al objeto de estado para que apunte a un objeto diferente, lo que cambia el comportamiento del contexto.
El patrón Estado es similar al FSM en que también permite la gestión de diferentes estados y la transición entre ellos. Sin embargo, un FSM se implementa normalmente utilizando una sentencia switch, mientras que el patrón de diseño State define una interfaz que representa un estado y una clase que implementa esta interfaz para cada estado.
El patrón de estados es ampliamente utilizado en el desarrollo de juegos, y puede ser una forma eficaz de gestionar los diferentes estados de un juego, como un menú principal, un estado de juego y un estado de finalización de juego.
Veamos el patrón de estado en acción con el ejemplo de la siguiente sección.
En Github hay disponible un proyecto de demostración que proporciona el código de ejemplo de esta sección.
Una forma simplificada de describir un FSM básico en código podría parecerse al ejemplo siguiente que utiliza un enum y una sentencia switch.
En primer lugar, se define un enum PlayerControllerState que consta de tres estados: Ralentí, Caminar y Saltar.
A continuación, switch se utiliza como una sentencia condicional en el bucle Actualizar para comprobar en qué estado se encuentra actualmente. Dependiendo del estado, puede llamar a las funciones apropiadas para llevar a cabo el comportamiento específico que corresponda.
Esto puede funcionar, pero el script PlayerController puede complicarse rápidamente, sobre todo porque hay que formular las condiciones para la transición entre los estados. Utilizar una sentencia switch para gestionar el estado de un juego con un script no se considera la mejor práctica porque puede llevar a un código complejo y difícil de mantener. La sentencia de conmutación puede hacerse grande y difícil de entender a medida que aumenta el número de estados y transiciones.
Además, hace que sea más difícil añadir nuevos estados o transiciones porque hay que hacer cambios en la sentencia switch. El patrón Estado, por otro lado, permite un diseño más modular y extensible, facilitando la adición de nuevos estados o transiciones.
Vamos a reimplementar el patrón de estado para reorganizar la lógica de PlayerController. Este ejemplo de código también está disponible en el proyecto de demostración alojado en Github.
Según el Gang of Four original, el patrón de diseño de estados resuelve dos problemas:
- Un objeto debe cambiar su comportamiento cuando cambia su estado interno.
- El comportamiento específico de cada estado se define de forma independiente. Añadir nuevos estados no afecta al comportamiento de los existentes.
En el ejemplo de código anterior, el UnrefactoredPlayerController puede realizar un seguimiento de los cambios de estado, pero no satisface la segunda cuestión. Cuando añada nuevos estados, querrá minimizar el impacto en los existentes. En su lugar, puede encapsular un estado como un objeto.
Imagine que estructura cada uno de los estados de su ejemplo como el diagrama anterior. Aquí, se entra en el estado apropiado y se hace un bucle en cada fotograma hasta que una condición provoque la salida del flujo de control. En otras palabras, se encapsula el estado específico con una Entrada, Actualización y Salida.
Para implementar el patrón anterior, cree una interfaz llamada IState. Cada estado concreto de tu juego implementará la interfaz siguiendo esta convención:
- Una entrada: Esta lógica se ejecuta al entrar en el estado por primera vez.
- Actualización: Esta lógica se ejecuta cada fotograma (a veces llamado Ejecutar o Tick). Puedes segmentar aún más el método Update como hace MonoBehaviour, utilizando un FixedUpdate para la física, LateUpdate, etc. Cualquier funcionalidad de la Actualización se ejecuta cada fotograma hasta que se detecta una condición que desencadena un cambio de estado.
- Una salida: El código aquí se ejecuta antes de salir del estado y la transición a un nuevo estado.
Necesitarás crear una clase para cada estado que implemente IState. En el proyecto de ejemplo, se ha creado una clase separada para WalkState, IdleState y JumpState.
Otra clase, StateMachine.cs, gestionará cómo el flujo de control entra y sale de los estados. Con los tres estados de ejemplo, la máquina de estados podría parecerse al ejemplo de código siguiente.
Para seguir el patrón, la máquina de estados hace referencia a un objeto público para cada estado bajo su gestión (en este caso, walkState, jumpState y idleState). Dado que la máquina de estados no hereda de MonoBehaviour, utilice un constructor para configurar cada instancia.
Puedes pasar cualquier parámetro necesario al constructor. En el proyecto de ejemplo, se hace referencia a un PlayerController en cada estado. A continuación, se utiliza para actualizar cada estado por fotograma (véase el ejemplo IdleState a continuación).
Tenga en cuenta lo siguiente sobre el concepto de máquina de estados:
- El atributo Serializable permite mostrar StateMachine.cs (y sus campos públicos) en el Inspector. Otro MonoBehaviour (por ejemplo, un PlayerController o EnemyController) puede entonces utilizar la máquina de estados como un campo.
- La propiedad CurrentState es de sólo lectura. El propio StateMachine.cs no establece explícitamente este campo. Un objeto externo como el PlayerController puede entonces invocar el método Initialize para establecer el Estado por defecto.
- Cada objeto de estado determina sus propias condiciones para llamar al método TransitionTo para cambiar el estado actualmente activo. Puedes pasar cualquier dependencia necesaria (incluyendo la propia máquina de estados) a cada estado cuando configures la instancia StateMachine.
En el proyecto de ejemplo, el PlayerController ya incluye una referencia al StateMachine, por lo que sólo hay que pasar un parámetro de jugador.
Cada objeto de estado manejará su propia lógica interna, y usted puede hacer tantos estados como sean necesarios para describir su GameObject o componente. Cada uno tiene su propia clase que implementa IState. De acuerdo con los principios SOLID, añadir más estados tiene un impacto mínimo en los estados creados previamente.
He aquí un ejemplo del IdleState.
De forma similar al script StateMachine.cs, el constructor se utiliza para pasar el objeto PlayerController. Este reproductor contiene una referencia a la Máquina de Estado y todo lo necesario para la lógica de Actualización. El IdleState monitoriza el estado de velocidad o salto del Character Controller y luego invoca el método TransitionTo de la máquina de estados apropiadamente.
Revise también el proyecto de ejemplo para la implementación de WalkState y JumpState. En lugar de tener una gran clase que cambia el comportamiento, cada estado tiene su propia lógica de actualización, lo que les permite funcionar de forma independiente el uno del otro.
El patrón de estado puede ayudarle a adherirse a los principios SOLID cuando configure la lógica interna de un objeto. Cada estado es relativamente pequeño y sólo registra las condiciones para pasar a otro estado. De acuerdo con el principio abierto-cerrado, puede añadir más estados sin afectar a los existentes y evitar engorrosas sentencias switch o if en un script monolítico.
También puede ampliar su funcionalidad para comunicar cambios de estado a objetos externos. Es posible que desee añadir eventos (véase el patrón de observador). Tener un evento al entrar o salir de un estado puede notificar a los oyentes relevantes y hacer que respondan en tiempo de ejecución.
Por otro lado, si sólo tiene que hacer un seguimiento de unos pocos estados, la estructura adicional puede resultar excesiva. Este patrón sólo tiene sentido si espera que sus estados crezcan hasta una cierta complejidad. Como con cualquier otro patrón de diseño, tendrás que evaluar los pros y los contras en función de las necesidades de tu juego en particular.
Recursos más avanzados para programar en Unity
El libro electrónico Mejora tu código con patrones de programación de juegosofrece más ejemplos de cómo utilizar patrones de diseño en Unity.
Todos los libros electrónicos y artículos técnicos avanzados de Unity están disponibles en el centro de mejores prácticas. Los libros electrónicos también están disponibles en la página de mejores prácticas avanzadas de la documentación.