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 explica el patrón observador y cómo puede ayudar a apoyar el principio de acoplamiento débil entre objetos que interactúan entre sí.
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:
En tiempo de ejecución, pueden ocurrir muchas cosas en tu juego. ¿Qué ocurre cuando el jugador destruye a un enemigo? ¿O cuando consiguen un aumento de poder o de nivel? A menudo se necesita un mecanismo que permita a unos objetos notificar a otros sin referenciarlos directamente. Desgraciadamente, a medida que aumenta la base de código, se añaden dependencias innecesarias que pueden provocar inflexibilidad y una sobrecarga excesiva en el mantenimiento del código.
El patrón observador es una solución habitual a este problema. Permite que los objetos se comuniquen entre sí, pero con un acoplamiento débil gracias a la dependencia "uno a muchos". Cuando un objeto cambia de estado, se notifica automáticamente a todos los objetos dependientes.
Una analogía para ayudar a visualizar esto es una torre de radio que emite a muchos oyentes diferentes. No necesita saber quién está sintonizando, sólo que la emisión se está emitiendo en directo en la frecuencia y el momento adecuados.
El objeto que emite se llama sujeto. Los otros objetos que escuchan se denominan observadores, o a veces suscriptores (en esta página se utiliza el nombre observador en todo momento).
La ventaja del patrón es que desvincula al sujeto del observador, que en realidad no conoce a los observadores ni le importa lo que hagan una vez recibida la señal. Mientras que los observadores tienen una dependencia del sujeto, los propios observadores no se conocen entre sí.
Los observadores reciben una simple notificación cada vez que cambia el estado del sujeto, lo que les permite actualizarse en consecuencia. De este modo, resulta más fácil modificar o ampliar el código sin afectar a otras partes del sistema.
Otra ventaja es que el patrón observador fomenta el desarrollo de código reutilizable, ya que los observadores pueden reutilizarse en diferentes contextos sin necesidad de modificarlos. Por último, a menudo mejora la legibilidad del código, ya que las dependencias entre objetos están claramente definidas.
Puedes diseñar tus propias clases sujeto-observador, pero normalmente es innecesario ya que C# ya implementa el patrón usando eventos. El patrón observador está tan extendido que se ha incorporado al lenguaje C#, y por una buena razón: Puede ayudarle a crear un código más modular, reutilizable y fácil de mantener.
¿Qué es un acontecimiento? Es una notificación que indica que algo ha sucedido, e implica algunos pasos:
El editor (también conocido como el sujeto) crea un evento basado en un delegado que establece una firma de función específica. El evento no es más que una acción que el sujeto realizará en tiempo de ejecución (por ejemplo, recibir daño, pulsar un botón, etc.). El publicador mantiene una lista de sus dependientes (los observadores) y les envía notificaciones cuando cambia su estado, que está representado por este evento.
A continuación, cada observador crea un método llamado manejador de eventos, que debe coincidir con la firma del delegado. Los observadores son objetos que reciben notificaciones del editor y se actualizan en consecuencia.
Cada controlador de eventos del observador se suscribe al evento del editor. Se pueden suscribir tantos observadores como sea necesario. Todos ellos esperarán a que se active el evento.
Cuando el editor señala la ocurrencia de un evento en tiempo de ejecución, se denomina lanzar el evento. Esto, a su vez, invoca los manejadores de eventos de los observadores, que ejecutan su propia lógica interna en respuesta.
De este modo, se consigue que muchos componentes reaccionen a un único evento del sujeto. Si el sujeto indica que ha pulsado un botón, los observadores podrían reproducir una animación o un sonido, desencadenar una escena o guardar un archivo. Su respuesta puede ser cualquier cosa, por lo que a menudo se utiliza el patrón observador para enviar mensajes entre objetos.
Delegados frente a actos
Un delegado es un tipo que define una firma de método. Esto permite pasar métodos como argumentos a otros métodos. Piense en ello como una variable que contiene una referencia a un método, en lugar de un valor.
Un evento, por otro lado, es esencialmente un tipo especial de delegado que permite a las clases comunicarse entre sí de una manera poco acoplada. Para obtener información general sobre las diferencias entre delegados y eventos, consulte Distinción entre delegados y eventos en C#.
Veamos cómo definir un asunto/editor básico en el código siguiente.
En la clase Subject del ejemplo de código de abajo, se hereda de MonoBehaviour para poder adjuntarlo a un GameObject más fácilmente, aunque no es un requisito.
Aunque eres libre de definir tu propio delegado personalizado, también puedes utilizar el System.Action, que funciona en la mayoría de los casos de uso. En el ejemplo de código, no hay necesidad de enviar parámetros con el evento, pero si fuera necesario, es tan fácil como usar el delegado Action<T> y pasarlos como una List<T> dentro de los paréntesis angulares (hasta 16 parámetros).
En el fragmento de código, ThingHappened es el evento real, que el sujeto invoca en el método DoThing.
El operador "?." es un operador condicional nulo, lo que significa que el evento sólo se invocará si no es nulo. El método Invoke se utiliza para lanzar un evento, lo que significa que ejecutará cualquier manejador de eventos que esté suscrito al evento. En este caso, el método DoThing lanzará el evento ThingHappened si no es nulo, lo que ejecutará cualquier manejador de eventos que esté suscrito al evento.
Puede descargar un proyecto de ejemplo que muestra el observador y otros patrones de diseño en acción. Este ejemplo de código está disponible aquí.
Para escuchar el evento, puede crear una clase Observador de ejemplo, como el ejemplo de código reducido que se muestra a continuación (también disponible en el proyecto Github).
Adjunte este script a un GameObject como un componente y haga referencia al subjectToObserver en el Inspector para escuchar el evento ThingHappened.
El método OnThingHappened puede contener cualquier lógica que el observador ejecute en respuesta al evento. A menudo los desarrolladores añaden el prefijo "On" para denotar el manejador de eventos (utilice la convención de nomenclatura de su guía de estilo).
En Despertar o Iniciar, puede suscribirse al evento con el operador +=. Que combina el método OnThingHappened del observador con el ThingHappened del sujeto.
Si algo ejecuta el método DoThing del sujeto, eso lanza el evento. A continuación, el controlador de eventos OnThingHappened del observador se invoca automáticamente e imprime la declaración de depuración.
Nota: Si borras o eliminas el observador en tiempo de ejecución mientras todavía está suscrito al ThingHappened, llamar a ese evento podría dar lugar a un error. Por lo tanto, es importante darse de baja del evento en el método OnDestroy de MonoBehaviour con un operador -= en los momentos adecuados del ciclo de vida del objeto.
Si descargas el proyecto de ejemplo y vas a la carpeta llamada 11 Observer, encontrarás un ejemplo que muestra un simple botón (ExampleSubject) y un altavoz (AudioObserver), una animación (AnimObserver) y un efecto de partículas (ParticleSystemObserver).
Al pulsar el botón el ExampleSubject invoca un evento ThingHappened. El AudioObserver, AnimObserver, y ParticleSystemObserver invocan sus métodos de manejo de eventos en respuesta.
Los observadores pueden existir en el mismo o en diferentes GameObjects. Note que el AnimObserver produce la animación del botón en el ExampleSubject, mientras que los AudioObservers y ParticleSystemObserver ocupan GameObjects diferentes.
El ButtonSubject permite al usuario invocar un evento Clicked con el botón del ratón. Varios otros GameObjects con los componentes AudioObserver y ParticleSystemObserver pueden entonces responder en sus propias maneras al evento.
Determinar qué objeto es un sujeto y cuál es un observador sólo varía según el uso. Todo lo que suscita el acontecimiento actúa como sujeto, y todo lo que responde al acontecimiento es el observador. Diferentes componentes en el mismo GameObject pueden ser sujetos u observadores. Incluso un mismo componente puede ser sujeto en un contexto y observador en otro.
Por ejemplo, el AnimObserver del ejemplo añade un poco de movimiento al botón cuando se pulsa. Actúa como un observador aunque es parte del GameObject ButtonSubject.
Unity también incluye un sistema separado de UnityEventsque utiliza la función UnityAction de la API UnityEngine.Events. Puede configurarse en el Inspector (que proporciona una interfaz gráfica para el patrón observador), lo que permite a los desarrolladores especificar qué métodos deben invocarse cuando se produce el evento.
Si usted ha usado el sistema UI de Unity (por ejemplo, creando un evento OnClick de un Botón UI), usted ya tiene alguna experiencia con esto.
En la imagen anterior, el evento OnClick del botón invoca y desencadena una respuesta de los métodos OnThingHappened de los dos AudioObservers. De este modo, puede configurar el evento de un sujeto sin código.
Los UnityEvents son útiles si quieres permitir a diseñadores o no programadores crear eventos de juego. Sin embargo, tenga en cuenta que pueden ser más lentos que sus eventos o acciones equivalentes del espacio de nombres System. Los UnityActions también tienen el beneficio extra de ser usados para invocar métodos que toman argumentos, mientras que los UnityEvents están limitados a métodos que no tienen argumentos.
Sopese el rendimiento frente al uso cuando considere UnityEvents y UnityActions. Los UnityEvents son más simples y fáciles de usar, pero son más limitados en términos de los tipos de métodos que pueden ser invocados. Algunos también podrían argumentar que pueden ser más propensos a errores al exponer todos los eventos en el Inspector.
Consulte el módulo Crear un sistema de mensajería simple con eventos en Unity Learn para ver un ejemplo.
Implementar un evento añade algo de trabajo extra, pero ofrece ventajas:
El patrón observador ayuda a desacoplar los objetos: El editor del evento no necesita saber nada sobre los propios suscriptores del evento. En lugar de crear una dependencia directa entre una clase y otra, el sujeto y el observador se comunican manteniendo cierto grado de separación (loose-coupling).
No tienes que construirlo: C# incluye un sistema de eventos establecido, y puedes utilizar el delegado System.Action en lugar de definir tus propios delegados. Alternativamente, Unity también incluye UnityEvents y UnityActions.
Cada observador implementa su propia lógica de gestión de eventos: De este modo, cada objeto observador mantiene la lógica que necesita para responder. Esto facilita la depuración y las pruebas unitarias.
Es muy adecuado para la interfaz de usuario: El código principal del juego puede vivir separado de la lógica de la interfaz de usuario. A continuación, los elementos de la interfaz de usuario escuchan eventos o condiciones específicos del juego y responden adecuadamente. Los patrones MVP y MVC utilizan el patrón observador para este propósito.
Pero también hay que tener en cuenta estas advertencias:
Añade complejidad adicional: Al igual que otros patrones, la creación de una arquitectura basada en eventos requiere más configuración al principio. Además, tenga cuidado al eliminar sujetos u observadores. Asegúrate de anular el registro de observadores en OnDestroy para que la referencia de memoria se libere correctamente cuando el observador ya no sea necesario.
Los observadores necesitan una referencia a la clase que define el evento: Los observadores siguen dependiendo de la clase que publica el evento. El uso de un EventManager estático (véase la sección siguiente) que gestione todos los eventos puede ayudar a separar los objetos entre sí.
El rendimiento puede ser un problema: La arquitectura basada en eventos añade una sobrecarga adicional. Las escenas grandes y muchos GameObjects pueden dificultar el rendimiento.
Aunque aquí sólo se presenta una versión básica del patrón observador, puedes ampliarlo para cubrir todas las necesidades de tu aplicación de juego.
Tenga en cuenta estas sugerencias a la hora de configurar el patrón del observador:
Utilice la clase ObservableCollection: C# proporciona una ObservableCollection para rastrear cambios específicos. Puede notificar a sus observadores cuando se añaden o eliminan elementos, o cuando se actualiza la lista.
Pasa un ID de instancia único como argumento: Cada GameObject en la jerarquía tiene un ID de instancia único. Si activa un evento que podría aplicarse a más de un observador, pase el ID único al evento (utilice el tipo Action<int>). Entonces sólo ejecuta la lógica en el manejador de eventos si el GameObject coincide con el ID único.
Crea un EventManager estático: Debido a que los eventos pueden conducir gran parte de su juego, muchas aplicaciones de Unity utilizan un EventManager estático o singleton. De este modo, tus observadores pueden hacer referencia a una fuente central de eventos del juego como sujeto para facilitar la configuración.
El FPS Microgame tiene una buena implementación de un EventManager estático que implementa GameEvents personalizados e incluye métodos de ayuda estáticos para añadir o eliminar oyentes.
El Proyecto Abierto Unity también muestra una arquitectura de juego donde ScriptableObjects retransmiten UnityEvents. Utiliza eventos para reproducir audio o cargar nuevas escenas.
Crear una cola de eventos: Si tienes muchos objetos en tu escena, puede que no quieras lanzar los eventos todos a la vez. Imagina la cacofonía de mil objetos reproduciendo sonidos cuando invoques un único evento.Combinar el patrón observador con el patrón comando te permite encapsular tus eventos en una cola de eventos. A continuación, puedes utilizar un búfer de comandos para reproducir los eventos de uno en uno o ignorarlos selectivamente según sea necesario (por ejemplo, si tienes un número máximo de objetos que pueden emitir sonidos a la vez).
El patrón del observador está muy presente en el patrón arquitectónico Modelo Vista Presentador (MVP), que se trata en el libro electrónico Mejora tu código con patrones de programación de juegos.
Encontrarás muchos más consejos sobre cómo utilizar patrones de diseño en tus aplicaciones Unity, así como los principios SOLID, en el libro electrónico gratuito Mejora tu código con patrones de programación de juegos.
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.