Hola a todos. Soy un desarrollador experimentado de Unity. Decidí participar en el concurso de Hackernoon y Tatum Games. En el primer artículo, analizaré la arquitectura de los proyectos de desarrollo de juegos de Unity y repasaré los enfoques más comunes que he encontrado. Y, por supuesto, les diré por qué soy tan masoquista y por qué vine a mi amado HMVС (HMVP). PD Todo lo que se describe aquí es una opinión subjetiva. Todos deben considerar los detalles del desarrollo y el proyecto como un todo, pero en general, la mejor arquitectura es la que no existe y combina diferentes enfoques en un estilo conveniente y eficiente para el equipo :D MonoBehaviour y programación orientada a componentes (COP) Comencemos con el enfoque más básico utilizado principalmente por principiantes. No quiero decir que este enfoque sea malo, solo que la mayoría de los desarrolladores están acostumbrados a pensar en términos de POO (programación orientada a objetos). El uso correcto de COP (programación orientada a componentes) requiere una mentalidad algo diferente. Al mismo tiempo, la implementación de Unity de COP basado en MonoBehaviour no parece ideal. Por lo tanto, el enfoque básico implica que todo el juego se construirá sobre un GameObject con componentes MonoBehaviour, lo que le permite dividir los diferentes subsistemas en partes pequeñas y construir el juego a partir de eso. { private Awake() { } } Sin embargo, el diablo está en los detalles. Este enfoque conduce a dificultades de escalado, especialmente en proyectos grandes, enlaces innecesarios, problemas reflejados bajo el capó de Unity y un fuerte vínculo con la API de Unity, lo que puede causar problemas más adelante, especialmente si desea duplicar código en el cliente y servidor. único Singleton para la gerencia es lo primero y lo más horrible que puede venir a la mente. En esencia, Singleton es un objeto contenido en la escena o en todo el proyecto en una sola instancia, que se necesita para vincular y administrar los sistemas separados en nuestro juego. Un ejemplo simple de Singleton, que a menudo veo en los desarrolladores Junior: public sealed class MySingleton { private static MySingleton _instance = null; private MySingleton() { } public static MySingleton Instance { get { if (_instance == null) { _instance = new MySingleton(); } return _instance; } } public void OperationX() { } } En proyectos pequeños, esto no dará lugar a problemas innecesarios. Cuanto más grande sea el proyecto, peor será controlarlo. Sin mencionar las fugas de memoria, Singleton genera problemas con la creación de pruebas, la organización de subprocesos múltiples y scripts demasiado largos (que a menudo se ven en desarrolladores novatos). Aquí hay un poco sobre cómo se ve normalmente la organización Singleton (y está lejos de ser la más correcta): Entonces, ¿cómo sabes si Singleton es malvado? Cuando une toda la lógica en tu juego y controla todo Cuando se lanzan un montón de enlaces directamente en él Cuando su tamaño se vuelve enorme Cuando ya ha iniciado la depuración o la gestión de la memoria, especialmente si Singleton es un GameObject ¿Cuándo es mejor usar un Singleton? Proyectos más pequeños Cuando la administración de la memoria no es un problema y administra eventos en lugar de reenviar enlaces directos Para sistemas pequeños, por ejemplo, para administrar audio o como punto final de adquisición para sistemas analíticos Ahora analicemos los contenedores DI. Contenedores DI y maldito Zenject Oh-oh, muchas personas están presionando por Zenject e implementando dependencias usando un contenedor DI. De hecho, muchas personas usan este marco enorme como un Singleton regular. Como de costumbre, lo he visto en proyectos: En esencia, se necesita un contenedor DI para poner referencias y resolver dependencias en los objetos finales. El ejemplo más simple del mismo Zenject: public class TestInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<string>().FromInstance("Hello World!"); Container.Bind<Greeter>().AsSingle().NonLazy(); } } public class Greeter { public Greeter(string message) { Debug.Log(message); } } Este enfoque es bueno, pero solo hasta que las cosas empiezan a complicarse: El DI-Container es esencialmente el mismo que Singleton pero mejorado, lo que crea enlaces al propio contenedor. Muy a menudo, se crean clases de instalador de "kilómetros de largo" que hacen enlaces de dependencia Difícil de entender para los recién llegados a expensas de una mayor separación de responsabilidades, aunque un enfoque bien escalable más adelante Difícil de depurar debido a contenedores y enlaces ubicuos Es muy fácil convertir un contenedor DI común en un localizador de servicios Por supuesto, los contenedores DI son una buena manera de organizar el código en las manos adecuadas, pero se necesita un alto nivel de capacitación para evitar que las cosas se conviertan en bacanales. MVC en su forma pura ¿Por qué en su forma pura? Porque es bastante fácil de entender. Tenemos un controlador, un modelo y una vista para organizar un proyecto. Pero mientras haya MVC, habrá tantos subtipos como MVP, MVVM, etc. Pero por ahora nos centraremos en un ejemplo básico y accesible a todos: Los beneficios son obvios: separamos el control (entrada del usuario) de los datos y de la vista (lo que el usuario ve en la pantalla). La comunicación generalmente se basa en eventos y se inicializa en el contenedor de la aplicación. Esto elimina la necesidad de mucha cohesión; solo nos comunicamos con eventos. Sin embargo, hay algunas desventajas aquí: A medida que el proyecto escala, nuestra aplicación de clase de instalación (el mismo contenedor) crece La disposición horizontal de MVC crea una gran cantidad de clases diferentes ligeramente conectadas entre sí. Ahora analicemos otro enfoque. MVC en contenedores Otro escenario posible es vincular nuestra tríada MVC en un contenedor DI. De esta forma podemos controlar mejor las conexiones entre las aplicaciones, pero es muy fácil convertirlo todo en un Localizador de Servicios. El enfoque es diferente porque en lugar de vincular controladores con eventos, resolvemos nuestros controladores a través de un contenedor y luego trabajamos con eventos. Sin embargo, de todos modos, surgen problemas aquí como con el contenedor DI habitual, pero hay una mayor complejidad de ocurrencia y se crean más clases. Sin embargo, separamos la representación, los modelos y los controladores. HMVC/HMVP Aquí es donde me gustaría hablar un poco más, ya que yo, como masoquista, me he encariñado mucho con este enfoque. Con él, creamos una división en forma de árbol de nuestro MVC, lo que brinda varias ventajas a pesar del gran aumento de la base de código. Entonces, veamos el esquema de interacción que uso con más frecuencia: ¿Como funciona? Inicialmente, creamos una escena vacía con GameInstaller, que cargará contenedores para cada escena por separado. La propia clase GameInstaller almacena las tríadas globales (de nivel superior) que suelen ser responsables de los grandes sistemas (por ejemplo, el manejo de audio) y almacena los eventos generales de todo el ciclo de vida del juego. Luego, GameInstaller carga el contenedor de escena que necesita, que inicializa las tríadas de nivel superior dentro de sí mismo (por ejemplo, un controlador de reproductor genérico) y que, a su vez, inicializará los controladores secundarios dentro de sí mismo (por ejemplo, un controlador de cañón). Y así sigue descendiendo. La comunicación de todas las sucursales ocurre exclusivamente a través de eventos y campos dinámicos. Suena complicado, pero es mucho más simple: este enfoque nos permite separar fácilmente todas las tríadas manteniendo una adecuada conexión contextual entre sus hijos. La inicialización de cada presentador comienza con obtener el contexto de los eventos del padre. Veo varias ventajas en este enfoque: Las escenas del proyecto se pueden cargar casi instantáneamente, y nuestros objetos, incluida la Vista, se pueden inicializar a pedido a medida que se carga nuestro árbol. Si no necesitamos cargar una Vista con configuraciones o una tienda de juegos antes de enviar un evento, no almacenamos nada más que el evento. Estructuración estrecha y aislamiento de tríadas individuales. Cohesión débil debido a eventos. Dinámica a expensas de los acontecimientos Bastante fácil de depurar en las ramas de la tríada en lugar de a través de contenedores Hay algunos inconvenientes: Si necesita enhebrar un evento a través de un árbol de 20 tríadas, será una tarea bastante larga, pero el enfoque implica un buen diseño inicial. Gran código base para el proyecto, aunque bien estructurado. Si necesita unir ramas, este podría ser un gran desafío para usted para organizar eventos a través de una docena de clases. En general, se necesita HMVC/HMVP para proyectos bien organizados con alto aislamiento de subsistemas, altos requisitos de memoria y recursos de juegos. Pero puede llevar más tiempo acostumbrarse a él que a otros enfoques. Conclusión Cada enfoque para organizar un proyecto tiene su lugar. Todo depende del objetivo del diseño. Si necesita una arquitectura ajustada y un manejo rápido de la memoria y necesita recursos rápidos y dinámicos, tome HMVC. Si necesita crear rápidamente un prototipo de su proyecto sin problemas, escriba todo en Singletons.