Olá a todos. Sou um desenvolvedor Unity experiente. Resolvi participar do concurso da Hackernoon e Tatum Games. No primeiro artigo, discutirei a arquitetura dos projetos de desenvolvimento de jogos Unity e abordarei as abordagens mais comuns que encontrei. E, claro, direi por que sou tão masoquista e por que vim ao meu amado HMVС (HMVP). PS Tudo o que é descrito aqui é uma opinião subjetiva. Todos precisam considerar as especificidades do desenvolvimento e do projeto como um todo, mas, em geral, a melhor arquitetura é aquela que não existe e combina diferentes abordagens de forma conveniente e eficiente para a equipe :D MonoBehaviour e programação orientada a componentes (COP) Vamos começar com a abordagem mais básica usada principalmente por iniciantes. Não quero dizer que essa abordagem seja ruim, apenas que a maioria dos desenvolvedores está acostumada a pensar em termos de OOP (programação orientada a objetos). O uso correto do COP (programação orientada a componentes) requer uma mentalidade um pouco diferente. Ao mesmo tempo, a implementação do Unity do COP baseado em MonoBehaviour não parece ideal. Portanto, a abordagem básica implica que todo o jogo será construído em um GameObject com componentes MonoBehaviour, o que permite dividir os diferentes subsistemas em pequenos pedaços e construir o jogo a partir disso. { private Awake() { } } No entanto, o diabo está nos detalhes. Essa abordagem leva a dificuldades de dimensionamento, especialmente em projetos grandes, vinculação desnecessária, problemas de reflexo sob o capô do Unity e um forte vínculo com a API do Unity, que pode causar problemas posteriormente, especialmente se você deseja duplicar o código no cliente e servidor. solteiro Singleton para gerenciamento é a primeira e mais horrível coisa que pode vir à mente. Em sua essência, Singleton é um objeto contido na cena ou em todo o projeto em uma única instância, necessária para vincular e gerenciar os sistemas separados em nosso jogo. Um exemplo simples de Singleton, que vejo frequentemente em desenvolvedores juniores: 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() { } } Em projetos pequenos, isso não levará a problemas desnecessários. Quanto maior o projeto, pior será o controle. Sem mencionar os vazamentos de memória, o Singleton leva a problemas com testes de construção, organização de multithreading e scripts muito longos (geralmente vistos em desenvolvedores novatos). Aqui está um pouco sobre como a organização Singleton costuma ser (e está longe de ser a mais correta): Então, como você sabe se Singleton é mau? Quando liga toda a lógica do seu jogo e controla tudo Quando um monte de links é jogado diretamente nele Quando seu tamanho fica enorme Quando você já disparou em depuração ou gerenciamento de memória, especialmente se Singleton for um GameObject Quando é melhor usar um Singleton? Projetos menores Quando o gerenciamento de memória não é um problema e você gerencia eventos em vez de encaminhar links diretos Para sistemas pequenos, por exemplo, para gerenciar áudio ou como aquisição de endpoint para sistemas analíticos Agora vamos discutir os contêineres de DI. Contêineres DI e porra do Zenject Oh-oh, muitas pessoas estão pressionando pelo Zenject e implementando dependências usando um contêiner de DI. Na verdade, muitas pessoas usam essa enorme estrutura como um Singleton regular. Como de costume, já vi isso em projetos: Em sua essência, um contêiner de DI é necessário para colocar referências e resolver dependências nos objetos finais. O exemplo mais simples do mesmo 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); } } Essa abordagem é boa, mas apenas até que as coisas comecem a ficar complicadas: O DI-Container é essencialmente igual ao Singleton, mas aprimorado, o que cria vínculos com o próprio contêiner Muitas vezes, são criadas classes de instalador de "quilômetros de comprimento" que fazem ligações de dependência Difícil para os recém-chegados entenderem à custa de mais separação de responsabilidades, embora uma abordagem bem escalável posteriormente Difícil de depurar devido a contêineres e ligações onipresentes É muito fácil transformar um contêiner de DI comum em um localizador de serviço É claro que os contêineres DI são uma boa maneira de organizar o código nas mãos certas, mas você precisa de um alto nível de treinamento para evitar que as coisas caiam em bacanais. MVC em sua forma pura Por que em sua forma pura? Porque é fácil de entender. Temos um controller, model e view para organizar um projeto. Mas enquanto houver MVC, existem tantos subtipos quanto MVP, MVVM, etc. Mas por enquanto vamos focar em um exemplo básico e acessível a todos: Os benefícios são óbvios - separamos o controle (entrada do usuário) dos dados e da visão (o que o usuário vê na tela). A comunicação geralmente é orientada a eventos e inicializada no contêiner do aplicativo. Isso elimina a necessidade de muita coesão; só nos comunicamos com eventos. No entanto, existem algumas desvantagens aqui: À medida que o projeto aumenta, nosso aplicativo de classe de instalação (o mesmo contêiner) cresce O arranjo horizontal do MVC cria um grande número de classes diferentes conectadas umas às outras Agora vamos discutir outra abordagem. MVC em contêineres Outro cenário possível é vincular nossa tríade MVC a um contêiner de DI. Dessa forma, podemos controlar melhor as conexões entre os aplicativos, mas é muito fácil transformar tudo em um localizador de serviço. A abordagem é diferente porque, em vez de vincular controladores a eventos, resolvemos nossos controladores por meio de um contêiner e depois trabalhamos com eventos. No entanto, mesmo assim, surgem problemas aqui como no contêiner DI usual, mas há uma maior complexidade de ocorrência e mais classes criadas. No entanto, separamos a representação, modelos e controladores. HMVC/HMVP É aqui que eu gostaria de falar um pouco mais, já que eu, como masoquista, gosto bastante dessa abordagem. Com ele, criamos uma divisão em árvore do nosso MVC, o que oferece várias vantagens, apesar da base de código aumentar bastante. Então, vamos ver o esquema de interação que uso com mais frequência: Como funciona? Inicialmente, criamos uma cena vazia com o GameInstaller, que irá carregar containers para cada cena separadamente. A própria classe GameInstaller armazena as tríades globais (nível superior) que geralmente são responsáveis por grandes sistemas (por exemplo, manipulação de áudio) e armazena os eventos gerais para todo o ciclo de vida do jogo. Em seguida, o GameInstaller carrega o contêiner de cena necessário, que inicializa as tríades de nível superior dentro de si (por exemplo, um controlador de jogador genérico) e que, por sua vez, inicializará os controladores filhos dentro de si (por exemplo, um controlador de canhão). E assim vai descendo. A comunicação de todas as filiais ocorre exclusivamente através de eventos e campos dinâmicos. Parece complicado, mas é muito mais simples: essa abordagem nos permite separar facilmente todas as tríades, mantendo uma conexão contextual adequada entre seus filhos. A inicialização de cada apresentador começa com a obtenção do contexto dos eventos do pai. Vejo várias vantagens nessa abordagem: As cenas do projeto podem ser carregadas quase instantaneamente, e nossos objetos, incluindo a Visualização, podem ser inicializados sob demanda conforme nossa árvore é carregada. Se não precisarmos carregar um View com configurações ou uma loja de jogos antes de enviar um evento, não armazenamos nada além do evento Estrutura rígida e isolamento de tríades individuais Coesão fraca devido a eventos Dinâmico à custa de eventos Bastante fácil de depurar em ramificações da tríade em vez de por meio de contêineres Existem algumas desvantagens: Se você precisar encadear um evento em uma árvore de 20 tríades, será uma tarefa bastante demorada, mas a abordagem envolve um bom design inicial Grande base de código para o projeto, embora bem estruturada Se você precisa amarrar branches, isso pode ser um grande desafio para você lançar eventos através de uma dúzia de classes Em geral, o HMVC/HMVP é necessário para projetos bem organizados com alto isolamento de subsistemas, altos requisitos de memória e recursos de jogo. Mas pode levar mais tempo para se acostumar com isso do que outras abordagens. Conclusão Cada abordagem para organizar um projeto tem seu lugar. Tudo depende apenas do objetivo do projeto. Se você precisa de arquitetura compacta e manuseio rápido de memória e precisa de recursos rápidos e dinâmicos, use o HMVC. Se você precisa prototipar rapidamente seu projeto sem problemas - escreva tudo em Singletons.