MVC simplificado

Los programadores principiantes generalmente comienzan a aprender el oficio con el Hello World. A partir de ahí, sin un patrón de diseño, se suele aumentar sin ninguna metodología los proyectos y terminan siendo un caos. Cada nuevo desafío lleva a casa una lección importante:

Cuanto más grande es el proyecto, más grande es el espagueti.

/img/c/mvc01.jpg
Snake

Es fácil ver que en equipos grandes o pequeños, uno no puede hacer imprudentemente lo que le plazca. El código debe mantenerse y puede durar mucho tiempo. Las empresas para las que ha trabajado no pueden simplemente buscar su información de contacto y preguntarle cada vez que quieren corregir o mejorar la base de código (y tampoco desean que lo hagan).

Es por eso que existen patrones de diseño de software, imponen reglas simples para dictar la estructura general de un proyecto de software. Ayudan a uno o más programadores a separar las piezas principales de un proyecto grande y organizarlas de manera estandarizada, eliminando la confusión cuando se encuentra una parte desconocida de la base de código.

/img/c/mvc02.jpg
.

Estas reglas, cuando son seguidas por todos, permiten que el código heredado se mantenga y navegue mejor y que el código nuevo se agregue más rápidamente. Se gasta menos tiempo planificando la metodología de desarrollo. Uno debe considerar cuidadosamente los puntos fuertes y débiles de cada patrón, y encontrar la mejor opción para el desafío en cuestión.

Relataré mi experiencia con el desarrollo de juegos Unity y el patrón Model-View-Controller (MVC) para el desarrollo de juegos. En mis siete años de desarrollo, habiendo luchado con mi parte justa de espagueti de desarrollo de juegos, he logrado una gran estructura de código y velocidad de desarrollo usando este patrón de diseño.

Empezaré explicando un poco de la arquitectura base de Unity, el patrón Entity-Component. Luego continuaré explicando cómo se ajusta MVC en la capa superior de la misma, y ​​usaré un pequeño proyecto simulado como ejemplo.

Motivación

En la literatura del software encontraremos una gran cantidad de patrones de diseño. A pesar de que tienen un conjunto de reglas, los desarrolladores usualmente harán un poco de flexión de reglas para adaptar mejor el patrón a su problema específico.

Esta “libertad de programación” es una prueba de que aún no hemos encontrado un método único y definitivo para diseñar software. Por lo tanto, este artículo no pretende ser la solución definitiva para su problema, sino más bien, para mostrar los beneficios y las posibilidades de dos patrones bien conocidos: Entity-Component y Model-View-Controller.


El patrón de Entity-Component(Entidad-Componente)

Entity-Component (EC) es un patrón de diseño donde primero definimos la jerarquía de los elementos que componen la aplicación (Entidades), y luego, definimos las características y los datos que cada uno contendrá (Componentes). En términos más de “programadores”, una entidad puede ser un objeto con una matriz de 0 o más componentes. Vamos a representar una entidad como esta:

Aquí hay un ejemplo simple de un árbol EC.

- app [Aplicacion]
   - game [Game]
      - player [KeyboardInput, Renderer]
      - enemigos
         - araña [ArañaAI, Renderer]
         - ogro [OgreAI, Renderer]
      - ui [UI]
         - hud [HUD, MouseInput, Renderer]
         - pause-menu [PauseMenu, MouseInput, Renderer]
         - victory-modal [VictoryModal, MouseInput, Renderer]
         - defeat-modal [DefeatModal, MouseInput, Renderer]

EC es un buen patrón para aliviar los problemas de herencia múltiple, donde una estructura de clase compleja puede introducir problemas como el problema de diamante donde una clase D, heredando dos clases, B y C, con la misma clase base A, puede introducir conflictos porque B y C modifican las características de A de manera diferente.

/img/c/mvc03.jpg
.

Este tipo de problemas pueden ser comunes en el desarrollo de juegos donde la herencia se usa a menudo extensivamente.

Al desglosar las características y los manejadores de datos en componentes más pequeños, se pueden adjuntar y reutilizar en diferentes entidades sin depender de la herencia múltiple (que, dicho sea de paso, ni siquiera es una característica de C# o Javascript, los principales idiomas utilizados por Unity)

Donde el componente de la entidad se queda corto

Viendo un nivel por encima de POO, EC ayuda a desfragmentar y organizar mejor la arquitectura de su código. Sin embargo, en proyectos grandes todavía somos “demasiado libres” y podemos encontrarnos en un “océano de características”, teniendo dificultades para encontrar las Entidades y Componentes correctos, o averiguando cómo deben interactuar. Hay infinitas formas de ensamblar entidades y componentes para una tarea determinada.

/img/c/mvc04.jpg
.

Una forma de evitar un desastre es imponer algunas pautas adicionales sobre Entity-Component. Por ejemplo, una forma en que me gusta pensar sobre el software es dividirlo en tres categorías diferentes:

  • Algunos manejan los datos brutos, lo que permite su creación, lectura, actualización, eliminación o búsqueda (es decir, el concepto CRUD ).
  • Otros implementan la interfaz para que interactúen otros elementos, detectando eventos relacionados con su alcance y disparando notificaciones cuando ocurren.
  • Finalmente, algunos elementos son responsables de recibir estas notificaciones, tomar decisiones de lógica y decidir cómo se deben manipular los datos.

Afortunadamente, ya tenemos un patrón que se comporta de esta manera exacta.

El patrón Model-View-Controller (Modelo-Vista-Controlador) (MVC)

El patrón Model-View-Controller (MVC) divide el software en tres componentes principales: Modelos (CRUD de datos), Vistas (Interfaz / Detección) y Controladores (Decisión / Acción). MVC es lo suficientemente flexible como para implementarse incluso en ECS o POO.

El desarrollo del juego y la interfaz de usuario tiene el flujo de trabajo habitual de esperar la entrada de un usuario u otra condición desencadenante, el envío de notificaciones de esos eventos en algún lugar, decidir qué hacer en respuesta y actualizar los datos en consecuencia. Estas acciones muestran claramente la compatibilidad de estas aplicaciones con MVC.

Esta metodología introduce otra capa de abstracción que ayudará con la planificación del software, y también permitirá a los nuevos programadores navegar incluso en una base de código más grande. Al dividir el proceso de pensamiento en datos, interfaz y decisiones, los desarrolladores pueden reducir el número de archivos fuente que se deben buscar para agregar o corregir la funcionalidad.

Unity y EC

Primero veamos de cerca lo que Unity nos da por adelantado.

Unity es una plataforma de desarrollo basada en CE, donde todas las entidades son instancias GameObject y las características que las hacen ser “visibles”, “movibles”, “interactivas”, etc., son proporcionadas por las clases que se extienden <Component>.

El panel de jerarquía y el panel Inspector del editor de Unity proporcionan una forma poderosa de ensamblar su aplicación, adjuntar componentes, configurar su estado inicial y arrancar su juego con mucho menos código fuente de lo que lo haría normalmente.

Aún así, como hemos discutido, podemos abordar el problema de las “demasiadas características” y nos encontramos en una jerarquía gigantesca, con características diseminadas por todas partes, lo que hace que la vida de un desarrollador sea mucho más difícil.

Pensando de la manera MVC, podemos, en cambio, comenzar dividiendo las cosas según su función, estructurando nuestra aplicación.

Adaptación de MVC a un entorno de desarrollo de juegos

Ahora, me gustaría introducir dos pequeñas modificaciones en el patrón MVC genérico, que ayudan a adaptarlo a las situaciones únicas en las que me he encontrado construyendo proyectos de Unity con MVC:

1- Las referencias de la clase MVC se dispersan fácilmente por todo el código.

  • Dentro de Unity, los desarrolladores generalmente deben arrastrar y soltar instancias para hacer que sean accesibles, o bien llegar a ellos a través de sentencias como find, GetComponent( … ).
  • El infierno de referencia perdida se producirá si la Unidad se bloquea o algún error hace desaparecer todas las referencias arrastradas.
  • Esto hace que sea necesario tener un único objeto de referencia raíz, a través del cual se puede llegar y recuperar a todas las instancias en la Aplicación .

2- Algunos elementos encapsulan funcionalidades generales que deberían ser altamente reutilizables, y que naturalmente no entran en una de las tres categorías principales de Modelo, Vista o Controlador. A estos me gusta llamarlos simplemente Componentes . También son “Componentes” en el sentido Entidad-Componente, pero simplemente actúan como ayudantes en el marco MVC.

  • Por ejemplo, un Rotator Componente, que solo rota cosas con una velocidad angular determinada y no notifica, almacena ni decide nada.

Para ayudar a aliviar estos dos problemas, se me ocurrió un patrón modificado que llamo AMVCC , o Application-Model-View-Controller-Component.

/img/c/mvc05.jpg
.
  • Aplicación : punto de entrada único para su aplicación y contenedor de todas las instancias críticas y datos relacionados con la aplicación.
  • MVC : ya deberías saber esto. :)
  • Componente : scripts pequeños y bien contenidos que pueden reutilizarse.

Estas dos modificaciones han satisfecho mis necesidades para todos los proyectos en los que los he usado.

Ejemplo: 10 Bounces

Como un simple ejemplo, veamos un pequeño juego llamado 10 Bounces , donde usaré los elementos centrales del patrón AMVCC.

La configuración del juego es simple: Un Ball con un SphereCollider y un Rigidbody(que comenzará a caer después de “Jugar”), un Cube como suelo y 5 scripts para componer el AMVCC.

Jerarquía

Antes de crear scripts, generalmente comienzo en la jerarquía y creo un esquema de mi clase y mis recursos. Siempre siguiendo este nuevo estilo AMVCC.

/img/c/mvc06.jpg
.

Como podemos ver, view GameObject contiene todos los elementos visuales y también los que tienen otros View scripts. El model y controller GameObjects, para proyectos pequeños, por lo general contienen solo sus respectivos scripts. Para proyectos más grandes, contendrán GameObjects con scripts más específicos.

Cuando alguien que navega por tu proyecto quiere acceder:

  • Datos: ir a application > model > …
  • Lógica/Workflow: ir a application > controller > …
  • Representación/Interfaz/Detección: ir a application > view > …

Si todos los equipos siguen estas simples reglas, los proyectos heredados no deberían convertirse en un problema.

Tenga en cuenta que no hay componentes contenedores, como ya hemos comentado, son más flexibles y se pueden unir a diferentes elementos en el ocio del desarrollador.

Scripting

Nota
Los scripts que se muestran a continuación son versiones abstractas de implementaciones del mundo real. Una implementación detallada no beneficiaría mucho al lector.

Echemos un vistazo a la estructura de los scripts para el ejemplo.

Antes de comenzar, para aquellos que no estén familiarizados con el flujo de trabajo de Unity, aclaremos brevemente cómo funcionan los scripts y GameObjects juntos. En Unity, los “Componentes”, en el sentido Entidad-Componente, están representados por la clase MonoBehaviour. Para que uno exista durante el tiempo de ejecución, el desarrollador debe arrastrar y soltar su archivo fuente en un GameObject (que es la “Entidad” del patrón Entidad-Componente) o usar el comando AddComponent<TuMonobehaviour>(). Después de esto, el script será instanciado y listo para usar durante la ejecución.

Para comenzar, definimos la clase de aplicación (la “A” en AMVCC), que será la clase principal que contiene referencias a todos los elementos del juego instanciados. También crearemos una clase base auxiliar llamada Element, que nos da acceso a la instancia de la Aplicación y a sus instancias MVC hijos.

Con esto en mente, definamos la clase Application (la “A” en AMVCC), que tendrá una instancia única. En su interior, tres variables, model, view, y controller, nos darán puntos de acceso para todas las instancias de MVC en tiempo de ejecución. Estas variables deben ser MonoBehaviours con public referencias a los scripts deseados.

Luego, también crearemos una clase base auxiliar llamada Element, que nos da acceso a la instancia de la Aplicación. Este acceso permitirá que cada clase MVC llegue a todos los demás.

Tenga en cuenta que ambas clases extienden MonoBehaviour. Son “Componentes” que se adjuntarán a “Entidades” de GameObject.


// BounceApplication.cs

// Clase base para todos los elementos en esta aplicación.
public class BounceElement : MonoBehaviour
{
   // Da acceso a la aplicación y a todas las instancias.
   public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }}
}

// 10 Bounces Punto de Entrada.
public class BounceApplication : MonoBehaviour
{
   // Referencia a las instancias de raíz del MVC.
   public BounceModel model;
   public BounceView view;
   public BounceController controller;

   // Init
   void Start() { }
}

Desde BounceElement podemos crear las clases principales de MVC. Las BounceModel, BounceView y BounceController, los scripts suelen actuar como contenedores para los casos más especializados, pero ya que este es un ejemplo sencillo sólo la vista tendrá una estructura anidada. El Modelo y el Controlador se pueden hacer en un script para cada uno:


// BounceModel.cs

// Contiene todos los datos relacionados con la aplicación.
public class BounceModel : BounceElement
{
   // Data
   public int bounces;	
   public int winCondition;
}


// BounceView .cs

// Contiene todas las vistas relacionadas con la aplicación.
public class BounceView : BounceElement
{
   // Referencia a la pelota
   public BallView ball;
}

// BallView.cs

// Describe la vista Bola y sus características.
public class BallView : BounceElement
{
   // Solo esto es necesario. La física hace el resto del trabajo.
   // Callback colision
   void OnCollisionEnter() { app.controller.OnBallGroundHit(); }
}

// BounceController.cs

// Controla el flujo de trabajo de la aplicación.
public class BounceController : BounceElement
{
   // Handles el evento de pelota golpeada
   public void OnBallGroundHit()
   {
      app.model.bounces++;
      Debug.Log(Bounce  + app.model.bounce);
      if(app.model.bounces >= app.model.winCondition)
      {
         app.view.ball.enabled = false;
         app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball
         OnGameComplete();
      }	
   }

   // Handles la condición de victoria
   public void OnGameComplete() { Debug.Log(Victoria!!); }
}

Con todos los scripts creados, podemos proceder a adjuntarlos y configurarlos.

El diseño de la jerarquía debería ser así:

- application [BounceApplication]
    - model [BounceModel]
    - controller [BounceController]
    - view [BounceView]
        - ...
        - ball [BallView]
        - ...

Usando BounceModel como ejemplo, podemos ver cómo se ve en el editor de Unity:

/img/c/mvc07.jpg
.

Con todos los scripts configurados y el juego funcionando, deberíamos obtener una salida en el panel de la consola .

Notificaciones

Como se muestra en el ejemplo anterior, cuando la pelota toca el suelo se ejecuta su vista, app.controller.OnBallGroundHit() que es un método. No es, de ninguna manera, “incorrecto” hacer eso para todas las notificaciones en la aplicación. Sin embargo, en mi experiencia, he logrado mejores resultados utilizando un sistema de notificación simple implementado en la clase de aplicación AMVCC.

Para implementar eso, actualicemos el diseño del BounceApplication:

// BounceApplication.cs

class BounceApplication 
{
   // Itera todos los Controladores y delega los datos de notificación
   // Este método se puede encontrar fácilmente porque cada clase es "BounceElement" y 
   // tiene una instancia de "aplicación".
   public void Notify(string p_event_path, Object p_target, params object[] p_data)
   {
      BounceController[] controller_list = GetAllControllers();
      foreach(BounceController c in controller_list)
      {
         c.OnNotification(p_event_path,p_target,p_data);
      }
   }

   // Obtiene todos los controladores de escena.
   public BounceController[] GetAllControllers() { /* ... */ }
}

A continuación, necesitamos un nuevo script donde todos los desarrolladores agregarán los nombres del evento de notificación, que se pueden enviar durante la ejecución.

// BounceNotifications.cs

// Esta clase dará acceso estático a las cadenas de eventos.
class BounceNotification
{
   static public string BallHitGround = ball.hit.ground;
   static public string GameComplete  = game.complete;
   /* ...  */
   static public string GameStart     = game.start;
   static public string SceneLoad     = scene.load;
   /* ... */
}

Es fácil ver que, de esta forma, se mejora la legibilidad del código porque los desarrolladores no necesitan buscar en todo el código fuente los métodos controller.OnSomethingComplexName para comprender qué tipo de acciones pueden ocurrir durante la ejecución. Al solo verificar un archivo, es posible comprender el comportamiento general de la aplicación.

Ahora, solo tenemos que adaptar el BallView y BounceController para manejar este nuevo sistema.

// BallView.cs

public class BallView : BounceElement
{
   void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); }
}

// BounceController.cs

public class BounceController : BounceElement
{
   public void OnNotification(string p_event_path,Object p_target,params object[] p_data)
   {
      switch(p_event_path)
      {
         case BounceNotification.BallHitGround:
            app.model.bounces++;
            Debug.Log(Bounce +app.model.bounce);
            if(app.model.bounces >= app.model.winCondition)
            {
               app.view.ball.enabled = false;
               app.view.ball.GetComponent<RigidBody>().isKinematic=true;
               app.Notify(BounceNotification.GameComplete,this);            
            }
         break;
         
         case BounceNotification.GameComplete:
            Debug.Log(Victoria!!);
         break;
      }	
   }
}

Los proyectos más grandes tendrán muchas notificaciones. Por lo tanto, para evitar tener una gran estructura de switch-case, es aconsejable crear diferentes controladores y hacer que manejen diferentes ámbitos de notificación.

Reglas de oro

No hay ninguna “Guía universal para la clasificación de MVC” en ninguna parte. Pero hay algunas reglas simples que normalmente sigo para ayudarme a determinar si se debe definir algo como Modelo, Vista o Controlador, y también cuándo dividir una clase determinada en partes más pequeñas.

Por lo general, esto sucede de forma orgánica mientras pienso en la arquitectura del software o durante la creación de scripts.

y vuelvo a decir, es mi opinion, no algo universal.

Clasificación de clase

Modelos

  • Mantenga los datos básicos y el estado de la aplicación, como jugador vida arma …
  • Serializar, deserializar y/o convertir entre tipos.
  • Cargar/guardar datos (localmente o en la web).
  • Notificar a los controladores el progreso de las operaciones.
  • Almacene el estado del juego para la máquina de estados finitos del juego.
  • Nunca acceda a Vistas.

Vista

  • Puede obtener datos de los Modelos para representar el estado del juego actualizado para el usuario. Por ejemplo, un método de Vista player.Run()puede usar internamente model.speed para manifestar las habilidades del jugador.
  • Nunca debería mutilar los modelos.
  • Implementa estrictamente las funcionalidades de su clase.

Controladores

  • No almacene datos básicos.
  • A veces puede filtrar las notificaciones de Vistas no deseadas.
  • Actualice y use los datos del Modelo.
  • Gestiona el flujo de trabajo de escena de Unity.

Conclusión

Hay toneladas de patrones de software por ahí. En este post intenté mostrar el que más me ayudó en proyectos anteriores. Los desarrolladores siempre deben absorber el conocimiento nuevo, pero siempre deberan cuestionarlo también. Espero que sirva como un trampolín a medida que desarrolles tu propio estilo.

Además, realmente te animo a buscar otros patrones y encontrar el que más te convenga. Un buen punto de partida es [este artículo de Wikipedia][1] , con su excelente lista de patrones y sus características.


Habilidad Especial Dsbloqueada
Juegos de Unity con el patrón MVC.

GitHub
/img/ref.png
.