Salut tout le monde. Je suis un développeur Unity expérimenté. J'ai décidé de participer au concours de Hackernoon et Tatum Games. Dans le premier article, je discuterai de l'architecture des projets de développement de jeux Unity et passerai en revue les approches les plus courantes que j'ai rencontrées. Et, bien sûr, je vais vous dire pourquoi je suis si masochiste et pourquoi je suis venu chez mon bien-aimé HMVС (HMVP). PS Tout ce qui est décrit ici est une opinion subjective. Chacun doit considérer les spécificités du développement et du projet dans son ensemble, mais en général, la meilleure architecture est celle qui n'existe pas et combine différentes approches dans un style pratique et efficace pour l'équipe :D Programmation monocomportementale et orientée composants (COP) Commençons par l'approche la plus basique utilisée principalement par les débutants. Je ne veux pas dire que cette approche est mauvaise, juste que la plupart des développeurs sont habitués à penser en termes de POO (programmation orientée objet). L'utilisation correcte de la COP (programmation orientée composants) nécessite un état d'esprit quelque peu différent. Dans le même temps, la mise en œuvre par Unity du COP basé sur MonoBehaviour ne semble pas idéale. Ainsi, l'approche de base implique que l'ensemble du jeu sera construit sur un GameObject avec des composants MonoBehaviour, ce qui vous permet de décomposer les différents sous-systèmes en petits morceaux et de construire le jeu à partir de cela. { private Awake() { } } Cependant, le diable est dans les détails. Cette approche entraîne des difficultés de mise à l'échelle, en particulier sur les grands projets, des liens inutiles, des problèmes de réflexe sous le capot de Unity et un lien fort avec l'API Unity, ce qui peut causer des problèmes plus tard, surtout si vous souhaitez dupliquer du code sur le client et serveur. Singleton Singleton pour la gestion est la première et la plus horrible chose qui puisse venir à l'esprit. Dans son essence, Singleton est un objet contenu dans la scène ou l'ensemble du projet dans une seule instance, qui est nécessaire pour lier et gérer les systèmes séparés dans notre jeu. Un exemple simple de Singleton, que je vois souvent chez les développeurs 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() { } } Sur les petits projets, cela n'entraînera pas de problèmes inutiles. Plus le projet est grand, plus il sera difficile à contrôler. Sans parler des fuites de mémoire, Singleton entraîne des problèmes de construction de tests, d'organisation du multithreading et de scripts trop longs (souvent observés chez les développeurs novices). Voici un peu à quoi ressemble généralement l'organisation Singleton (et est loin d'être la plus correcte): Alors, comment savez-vous si Singleton est diabolique ? Quand il lie toute la logique de votre jeu et contrôle tout Quand un tas de liens y est jeté directement Quand sa taille devient énorme Lorsque vous vous êtes déjà lancé dans le débogage ou la gestion de la mémoire, surtout si Singleton est un GameObject Quand est-il préférable d'utiliser un Singleton ? Petits projets Lorsque la gestion de la mémoire n'est pas un problème et que vous gérez des événements au lieu de transférer des liens directs Pour les petits systèmes, par exemple, pour gérer l'audio ou comme acquisition de point final pour les systèmes d'analyse Parlons maintenant des conteneurs DI. Conteneurs DI et putain de Zenject Oh-oh, beaucoup de gens font pression pour Zenject et implémentent des dépendances à l'aide d'un conteneur DI. En fait, beaucoup de gens utilisent cet énorme framework comme un Singleton régulier. Comme d'habitude, je l'ai vu sur des projets : Dans son essence, un conteneur DI est nécessaire pour mettre des références et résoudre les dépendances dans les objets finaux. L'exemple le plus simple du même 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); } } Cette approche est bonne, mais seulement jusqu'à ce que les choses commencent à se compliquer : Le DI-Container est essentiellement le même que Singleton mais amélioré, ce qui crée des liaisons avec le conteneur lui-même Très souvent, des classes d'installation "d'un kilomètre" sont créées qui font des liaisons de dépendance Difficile à comprendre pour les nouveaux arrivants au détriment d'une plus grande séparation des responsabilités, bien qu'une approche bien évolutive par la suite Difficile à déboguer en raison des conteneurs et des liaisons omniprésentes Il est très facile de transformer un conteneur DI ordinaire en localisateur de service Bien sûr, les conteneurs DI sont un bon moyen d'organiser le code entre de bonnes mains, mais vous avez besoin d'un haut niveau de formation pour éviter que les choses ne sombrent dans la bacchanale. MVC dans sa forme pure Pourquoi dans sa forme pure ? Parce que c'est assez facile à comprendre. Nous avons un contrôleur, un modèle et une vue pour organiser un projet. Mais tant qu'il y a MVC, il y a autant de sous-types qu'il y a de MVP, MVVM, etc. Mais pour l'instant nous allons nous concentrer sur un exemple basique et accessible à tous : Les avantages sont évidents - nous séparons le contrôle (entrée de l'utilisateur) des données et de la vue (ce que l'utilisateur voit à l'écran). La communication est généralement pilotée par les événements et initialisée dans le conteneur d'application. Cela supprime le besoin de beaucoup de cohésion; nous ne communiquons qu'avec les événements. Cependant, il y a quelques inconvénients ici: Au fur et à mesure que le projet évolue, notre application de classe d'installation (le même conteneur) se développe La disposition horizontale de MVC crée un grand nombre de classes différentes vaguement connectées les unes aux autres Abordons maintenant une autre approche. MVC dans des conteneurs Un autre scénario possible consiste à lier notre triade MVC à un conteneur DI. De cette façon, nous pouvons mieux contrôler les connexions entre les applications, mais il est très facile de tout transformer en Service Locator. L'approche est différente car au lieu de lier des contrôleurs à des événements, nous résolvons nos contrôleurs via un conteneur, puis travaillons avec des événements. Cependant, tout de même, des problèmes se posent ici comme avec le conteneur DI habituel, mais il y a une complexité accrue d'occurrence et plus de classes créées. Cependant, nous séparons la représentation, les modèles et les contrôleurs. HMVC/HMVP C'est là que j'aimerais parler un peu plus longtemps, car moi, en tant que masochiste, j'aime beaucoup cette approche. Avec lui, nous créons une division arborescente de notre MVC, ce qui offre plusieurs avantages malgré la base de code en forte augmentation. Alors, regardons le schéma d'interaction que j'utilise le plus souvent : Comment ça marche? Initialement, nous créons une scène vide avec GameInstaller, qui chargera les conteneurs pour chaque scène séparément. La classe GameInstaller elle-même stocke les triades globales (niveau supérieur) qui sont généralement responsables des grands systèmes (par exemple, la gestion audio) et stocke les événements généraux pour l'ensemble du cycle de vie du jeu. Ensuite, GameInstaller charge le conteneur de scène dont vous avez besoin, qui initialise les triades de niveau supérieur à l'intérieur de lui-même (par exemple, un contrôleur de lecteur générique), et qui, à son tour, initialisera les contrôleurs enfants à l'intérieur de lui-même (par exemple, un contrôleur de canon). Et ainsi de suite en descendant. La communication de toutes les branches se fait exclusivement par le biais d'événements et de champs dynamiques. Cela semble compliqué, mais c'est beaucoup plus simple : cette approche nous permet de séparer facilement toutes les triades tout en maintenant un lien contextuel adéquat entre leurs enfants. L'initialisation de chaque présentateur commence par obtenir le contexte des événements du parent. Je vois plusieurs avantages à cette approche : Les scènes de projet peuvent être chargées presque instantanément et nos objets, y compris la vue, peuvent être initialisés à la demande lorsque notre arbre est chargé. Si nous n'avons pas besoin de charger une vue avec des paramètres ou un magasin de jeux avant d'envoyer un événement, nous ne stockons rien d'autre que l'événement Structuration étroite et isolation des triades individuelles Faible cohésion due aux événements Dynamique au détriment des événements Assez facile à déboguer sur les branches de la triade plutôt que via des conteneurs Il y a quelques inconvénients : Si vous avez besoin d'enchaîner un événement à travers un arbre de 20 triades, cela va être une entreprise assez longue, mais l'approche implique une bonne conception initiale Grande base de code pour le projet, bien que bien structurée Si vous avez besoin de lier des branches ensemble, cela pourrait être un grand défi pour vous d'organiser des événements à travers une douzaine de classes En général, HMVC/HMVP est nécessaire pour les projets bien organisés avec une isolation élevée des sous-systèmes, des exigences de mémoire élevées et des ressources de jeu. Mais cela peut prendre plus de temps pour s'y habituer que d'autres approches. Conclusion Chaque approche d'organisation d'un projet a sa place. Tout dépend de l'objectif de conception. Si vous avez besoin d'une architecture étroite et d'une gestion rapide de la mémoire et que vous avez besoin de ressources rapides et dynamiques, optez pour HMVC. Si vous avez besoin de prototyper rapidement votre projet sans tracas, écrivez tout en Singletons.