
Salut Habr! Dans cette publication, je veux partager l'expérience du développement d'un jeu mobile massif, avec une grande ville et du trafic. Les exemples et techniques décrits dans la publication ne prétendent pas être appelés référence et idéal. Je ne suis pas un spécialiste certifié et je ne demande pas de répéter mon expérience. Le but du jeu était d'acquérir une expérience intéressante, d'obtenir un jeu optimisé avec un monde ouvert. Pendant le développement, j'ai essayé de simplifier le code autant que possible. Malheureusement, je n'ai pas utilisé ECS, mais j'ai péché avec singleton.
Le jeu
Un jeu sur le thème de la mafia. Dans le jeu, j'ai essayé de recréer l'Amérique 30-40. Essentiellement, un jeu est une stratégie économique à la première personne. Le joueur capture l'entreprise et essaie de la maintenir à flot.
Mis en œuvre: circulation automobile (feux de circulation, prévention des collisions), circulation humaine, bar, casino, club, appartement du joueur, achat d'un costume, changement de costume, achat / peinture / ravitaillement, flics, sécurité / gangsters, économie, vente / achat de ressources.
L'architecture

Je regrette de ne pas avoir utilisé ECS, mais j'ai essayé de faire du vélo. Au final, tout s'est avéré lourd et trop dépendant. L'application a un point d'entrée - l'objet de jeu application (go), sur lequel se bloque la classe Application du même nom. Il est responsable du préchargement de la base de données, du remplissage des pools et des paramètres initiaux. De plus, plusieurs autres classes de composants de gestionnaire singleton tombent sur les épaules de l'application (allez).
- Audiomanager
- UIManager
- Inputmanager
J'ai essayé fanatiquement de créer une telle architecture dans laquelle je peux gérer divers composants à partir du gestionnaire. Par exemple, AudioManager gère tous les sons, UIManager contient tous les éléments d'interface utilisateur et les méthodes de gestion. Toutes les entrées sont traitées via InputManager à l'aide d'événements et de délégués.
AudioManager simplifié. Il vous permet d'ajouter autant de composants audio à l'objet de jeu et, si nécessaire, de jouer du son:
public class AudioManager : MonoBehaviour { public static AudioManager instance = null;
Au démarrage, la méthode AddAudio ajoute un composant, puis de n'importe où nous pouvons jouer le son dont nous avons besoin:
AudioManager.instance.isMetalHit = true;
Dans cet exemple, il serait plus judicieux de remettre la lecture de one shot dans la méthode.
À quoi ressemble un InputManager simplifié:
public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked;
Je
mets la méthode
AimKeyDown sur le
bouton et signe le script de contrôle des armes sur OnAimKeyClicked:
InputManager.instance.OnAimKeyClicked += GunShot;
L'ensemble de mon système de saisie est mis en œuvre de manière similaire. Je n'ai remarqué aucun problème de vitesse. Cela nous a permis de rassembler tous les gestionnaires de clics en un seul endroit - InputManager.
Optimisation
Passons au plus intéressant. Pour les débutants, le sujet de l'optimisation dans Unity est douloureux et semé d'embûches. Je vais partager ce à quoi je faisais face.
1. Mise en cache des composants (commencez par des bases simples)Souvent sur Toster, vous pouvez rencontrer des questions avec des exemples où, où GetComponent est utilisé dans Update. Vous ne pouvez pas faire cela, GetComponent recherche un composant sur l'objet. Cette opération est lente et la cause dans Update, vous risquez de perdre de précieux FPS. Voici une bonne explication de la
mise en cache des composants .
2. Utilisation de SendMessageL'utilisation de SendMessage () est plus lente que GetComponent (). SendMessage passe en revue chaque script pour trouver la méthode avec le nom souhaité en utilisant la comparaison de chaînes. GetComponent recherche le script par comparaison de types et appelle directement la méthode.
3. Comparaison des balises d'objetUtilisez la méthode CompareTag au lieu de obj.tag == «chaîne». Dans Unity, l'extraction de chaînes à partir d'objets de jeu crée une chaîne en double, ce qui ajoute du travail au garbage collector. Il vaut mieux éviter d'obtenir le nom de l'objet de jeu. Vous ne pouvez pas appeler CompareTag dans Update ni lire des opérations lourdes.
4. MatériauxMoins il y a de matériaux, mieux c'est. Réduisez la quantité de matériaux possible. Pour ce faire, aidez à texturer le satin. Par exemple, presque toute la ville de mon jeu est composée de 2-3 atlas. Il convient de noter que tous les appareils mobiles ne sont pas capables de fonctionner avec de grands atlas. Par conséquent, si vous souhaitez prendre en charge des appareils de 11 à 13 ans, cela vaut la peine d'être considéré. J'ai décidé de refuser la prise en charge d'Android en dessous de 5.1, car il s'agit principalement de vieux appareils. De plus, le jeu fonctionne sur OpenGL 3.x en raison du rendu linéaire.
5. PhysiqueIl est facile de ramener le FPS à 10. Il s'est avéré que même les objets statiques interagissent et participent aux calculs. J'ai pensé à tort que les objets physiques statiques (objets qui ont un composant RigidBody) sont complètement passifs à la demande. J'ai été égaré par l'ancien tutoriel qui disait que partout où il y a un collisionneur, il devrait y avoir RigidBody. Maintenant, tous mes objets statiques sont Static + BoxCollider. Là où j'ai besoin de physique, par exemple de lampadaires qui peuvent être abattus, je pense couper le composant RigidBody si nécessaire.
Les couches sont la bouée de sauvetage pour l'optimisation. Désactivez les interactions inutiles à l'aide de calques. Lors de la refonte, utilisez des masques de calque. Pourquoi avons-nous besoin d'erreurs de calcul supplémentaires? N'oubliez pas que si votre objet a une grille de collisionneur complexe et que vous tirez dessus avec un rayon, il est préférable de créer un collisionneur parent simple pour "attraper" les rayons. Plus le collisionneur est complexe, plus les calculs sont erronés.
6. Abattage d'occlusion + LodAvec une grande scène, l'élimination des occlusions est indispensable. Pour désactiver des objets (arbres, poteaux, etc.) à grande distance, j'utilise Lod.

7. Pool d'objetsToutes les implémentations prêtes à l'emploi du pool d'objets que j'ai trouvé utilisent instantiate. Ils suppriment et créent également des objets. J'ai peur d'instancier dans toutes ses manifestations. Fonctionnement lent, qui fige le jeu, avec un objet plus ou moins gros. J'ai décidé de suivre un chemin simple et rapide - ma piscine entière existe sous la forme d'objets de jeu physiques que je viens d'éteindre et de rallumer si nécessaire. Il atteint la RAM, mais c'est mieux. RAM pour les appareils modernes de 1 Go, le jeu consomme 300 à 500 Mo.
Pool simple pour gérer les robots de combat:
public List<Enemy> enemyPool = new List<Enemy>(); private void Start() {
Base de données
J'utilise sqlite comme base de données - facilement et rapidement. Les données sont présentées sous forme de tableau, vous pouvez faire des requêtes complexes. Dans la classe pour travailler avec la base de données, 800 lignes quand. Je ne peux pas imaginer à quoi cela ressemblerait en XML / JSON.
Problèmes et plans pour l'avenir
Pour passer de la ville aux «chambres» j'ai choisi la mise en place de «téléports». Le joueur s'approche de la porte, la salle de scène est chargée et le joueur est téléporté. Cela vous évite d'avoir à garder des chambres dans la ville. Si vous implémentez des chambres dans la ville, qui est +15 chambres avec remplissage, la consommation de mémoire augmentera à un minimum de 1 Go. Je n'aime pas cette implémentation, elle n'est pas réaliste et impose un tas de restrictions. Unity a récemment montré une démo de sa
Megacity , c'est impressionnant. Je souhaite transférer progressivement le jeu vers ESC et utiliser la technologie de Megacity pour charger des bâtiments et des locaux. C'est une expérience fascinante et intéressante, je pense que cela va devenir une ville vraiment dynamique. Pourquoi n'ai-je pas utilisé de
scène de chargement asynchrone ? C'est simple, cela ne fonctionne pas, il n'y a pas de scène de chargement asynchrone prête à l'emploi dans la version 2018.3. Au départ, j'espérais une scène de chargement asynchrone lors de la planification d'une ville, mais il s'avère que, sur de grandes scènes, cela gèle le jeu comme une scène de chargement régulière. Cela a été confirmé sur le forum Unity, vous pouvez vous déplacer, mais des béquilles sont nécessaires.
Quelques statistiques:
Textures: 304 / 374,3 Mo
Mailles: 295 / 304,0 Mo
Matériaux: 101 / 148,0 Ko (écart probable ici)
AnimationClips: 24 / 2,8 Mo
AudioClips: 22 / 30,3 Mo
Actifs: 21761
GameObjects in Scene: 29450
Total des objets dans la scène: 111645
Nombre total d'objets: 133406
Allocations GC par trame: 70 / 2,0 KoUn total de 4800 lignes de code C #.
Quelqu'un m'a dit qu'un tel jeu pouvait se faire en une semaine. Peut-être que je ne suis pas productif, peut-être que cette personne est talentueuse, mais pour moi, j'ai compris une chose - il est difficile de créer de tels jeux seul. Je voulais créer quelque chose d'intéressant sur fond de «doigts» décontractés, il me semble que j'ai approché mon rêve.
Vous pouvez exécuter un test bêta ouvert et le ressentir ici:
play.google.com/store/apps/details?id=com.ag.mafiaProject01 (si l'assemblage ne fonctionne pas, vous devez l'adorer un peu, les mises à jour arrivent tous les soirs). J'espère que ce n'est pas considéré comme un lien publicitaire, car cette version bêta et ces téléchargements ne m'apporteront pas de note ni de dividendes. De plus, je ne pense pas que habr soit le public cible de mon jeu.
Captures d'écran:

