Les astuces de script de l'éditeur avancé pour gagner du temps, partie 2

Je suis de retour pour la deuxième partie ! Si vous avez manqué le premier épisode de mes astuces de script avancées pour l'éditeur, consultez-le ici. Cet article en deux parties a pour but de vous présenter des conseils avancés de l'éditeur pour améliorer les flux de travail afin que votre prochain projet se déroule plus facilement que le précédent.
Chaque hack est basé sur un prototype de démonstration que j'ai mis en place - similaire à un RTS - où les unités d'une équipe attaquent automatiquement les bâtiments ennemis et les autres unités. Pour vous rafraîchir la mémoire, voici le prototype initial :
Dans l'article précédent, j'ai partagé les meilleures pratiques sur la façon d'importer et de configurer les ressources artistiques dans le projet. Maintenant, commençons à utiliser ces actifs dans le jeu, tout en gagnant le plus de temps possible.
Commençons par décortiquer les éléments du jeu. Lors de la mise en place des éléments d'un jeu, nous rencontrons souvent le scénario suivant :
D'une part, nous avons les préfabriqués qui proviennent de l'équipe artistique - qu'il s'agisse d'un préfabriqué généré par l'importateur FBX ou d'un préfabriqué qui a été soigneusement configuré avec tous les matériaux et animations appropriés, en ajoutant des accessoires à la hiérarchie, etc. Pour utiliser cette préfabrication dans le jeu, il est logique de créer une variante de préfabrication à partir de celle-ci et d'y ajouter tous les composants liés à la jouabilité. Ainsi, l'équipe artistique peut modifier et mettre à jour la préfabrication, et tous les changements sont immédiatement répercutés dans le jeu. Bien que cette approche fonctionne si l'élément ne nécessite que quelques composants avec des paramètres simples, elle peut ajouter beaucoup de travail si vous devez configurer quelque chose de complexe à partir de zéro à chaque fois.
D'un autre côté, de nombreux éléments auront les mêmes composants avec des valeurs similaires, comme tous les Prefabs de voitures ou les Prefabs d'ennemis similaires. Il est logique qu'il s'agisse de variantes du même préfabriqué de base. Cela dit, cette approche est idéale si la mise en place de l'art du préfabriqué est simple (c'est-à-dire la définition de la maille et de ses matériaux).
Ensuite, nous allons voir comment simplifier la configuration des composants de gameplay, afin de pouvoir les ajouter rapidement à nos art Prefabs et les utiliser directement dans le jeu.
La configuration la plus courante que j'ai vue pour les éléments complexes d'un jeu consiste à avoir un composant "principal" (comme "ennemi", "pickup" ou "porte") qui sert d'interface pour communiquer avec l'objet, et une série de petits composants réutilisables qui mettent en œuvre la fonctionnalité elle-même ; des choses comme "selectable", "CharacterMovement" ou "UnitHealth", et des composants intégrés d'Unity, comme les moteurs de rendu et les collisionneurs.
Certains composants dépendent d'autres composants pour fonctionner. Par exemple, le mouvement du personnage peut nécessiter un agent NavMesh. C'est pourquoi Unity dispose de l'attribut RequireComponent pour définir toutes ces dépendances. Ainsi, s'il existe un composant "principal" pour un type d'objet donné, vous pouvez utiliser l'attribut RequireComponent pour ajouter tous les composants nécessaires à ce type d'objet.
Par exemple, les unités de mon prototype ont les attributs suivants :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
En plus de définir un emplacement facile à trouver dans le menu AddComponentMenu, incluez tous les composants supplémentaires dont il a besoin. Dans ce cas, j'ai ajouté le Locomotion pour me déplacer et l'AttackComponent pour attaquer les autres unités.
En outre, la classe de base unité (qui est partagée avec les bâtiments) possède d'autres attributs RequireComponent qui sont hérités par cette classe, tels que le composant Santé. Ainsi, il me suffit d'ajouter le composant Soldier à un GameObject pour que tous les autres composants soient ajoutés automatiquement. Si j'ajoute un nouvel attribut RequireComponent à un composant, Unity mettra à jour tous les GameObjects existants avec le nouveau composant, ce qui facilite l'extension des objets existants.
RequireComponent présente également un avantage plus subtil : Si nous avons un "composant A" qui nécessite un "composant B", l'ajout de A à un GameObject ne garantit pas seulement que B sera également ajouté - il garantit en fait que B est ajouté avant A. Cela signifie que lorsque la méthode Reset est appelée pour le composant A, le composant B existera déjà et que nous y aurons facilement accès. Cela nous permet de définir des références aux composants, d'enregistrer des UnityEvents persistants et de faire tout ce qui est nécessaire pour configurer l'objet. En combinant l'attribut RequireComponent et la méthode Reset, nous pouvons entièrement configurer l'objet en ajoutant un seul composant.
Le principal inconvénient de la méthode présentée ci-dessus est que, si nous décidons de modifier une valeur, nous devrons la modifier manuellement pour chaque objet. Et si toute la configuration se fait par le biais d'un code, il devient difficile pour les concepteurs de le modifier.
Dans l'article précédent, nous avons vu comment utiliser AssetPostprocessor pour ajouter des dépendances et modifier des objets au moment de l'importation. Utilisons-le maintenant pour appliquer certaines valeurs à nos Prefabs.
Pour faciliter la modification de ces valeurs par les concepteurs, nous lirons les valeurs d'un préfabriqué. Cela permet aux concepteurs de modifier facilement ce préfabriqué pour changer les valeurs de l'ensemble du projet.
Si vous écrivez le code de l'éditeur, vous pouvez copier les valeurs d'un composant d'un objet vers un autre en tirant parti de la classe Preset.
Créez un préréglage à partir du composant original et appliquez-le au(x) autre(s) composant(s) comme suit :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
En l'état, il remplacera toutes les valeurs de la préfabrication, mais ce n'est probablement pas ce que nous voulons qu'il fasse. Au lieu de cela, vous ne copiez que certaines valeurs, tout en gardant les autres intactes. Pour ce faire, utilisez une autre surcharge de Preset.ApplyTo qui prend une liste des propriétés qu'elle doit appliquer. Bien sûr, nous pourrions facilement créer une liste codée en dur des propriétés que nous voulons remplacer, ce qui conviendrait parfaitement à la plupart des projets, mais voyons comment rendre cela complètement générique.
En fait, j'ai créé une préfabrication de base avec tous les composants, puis j'ai créé une variante à utiliser comme modèle. Ensuite, j'ai décidé des valeurs à appliquer à partir de la liste de dérogations figurant dans la variante.
Pour obtenir les dérogations, utilisez PrefabUtility.GetPropertyModifications. Cela vous permet d'accéder à toutes les options de la préfabrication, et de ne filtrer que celles qui sont nécessaires pour cibler ce composant. Il convient de garder à l'esprit que la cible de la modification est le composant de la préfabrication de base - et non le composant de la variante - et que nous devons donc obtenir la référence à ce composant à l'aide de la fonction GetCorrespondingObjectFromSource:
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Cela permet d'appliquer toutes les surcharges du modèle à nos Prefabs. Le seul détail restant est que le modèle peut être une variante d'une variante, et que nous voudrons également appliquer les surcharges de cette variante.
Pour ce faire, il suffit de rendre le système récursif :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Ensuite, trouvons le modèle pour nos Prefabs. Idéalement, nous devrions utiliser différents modèles pour différents types d'objets. Une façon efficace de procéder consiste à placer les modèles dans le même dossier que les objets auxquels nous voulons les appliquer.
Cherchez un objet nommé Template.prefab dans le même dossier que notre Prefab. Si nous ne le trouvons pas, nous chercherons dans le dossier parent de manière récursive :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
A ce stade, nous avons la possibilité de modifier le modèle Prefab, et tous les changements seront reflétés dans les Prefabs de ce dossier, même s'ils ne sont pas des variantes du modèle. Dans cet exemple, j'ai modifié la couleur par défaut du joueur (la couleur utilisée lorsque l'unité n'est rattachée à aucun joueur). Remarquez qu'il met à jour tous les objets :
Lors de l'équilibrage des jeux, toutes les statistiques que vous devrez ajuster sont réparties entre différents composants, stockés dans un Prefab ou un ScriptableObject pour chaque personnage. Le processus d'ajustement des détails est donc assez lent.
L'utilisation de feuilles de calcul est un moyen courant de faciliter l'équilibre. Ils peuvent être très pratiques car ils rassemblent toutes les données et vous pouvez utiliser des formules pour calculer automatiquement certaines données supplémentaires. Mais la saisie manuelle de ces données dans Unity peut s'avérer péniblement longue.
C'est là que les feuilles de calcul entrent en jeu. Ils peuvent être exportés dans des formats simples comme CSV(.csv) ou TSV(.tsv), ce qui est exactement la fonction des ScriptedImporters. Vous trouverez ci-dessous une capture d'écran des statistiques des unités du prototype :

Le code pour cela est assez simple : Créez un ScriptableObject avec toutes les statistiques d'une unité, puis vous pourrez lire le fichier. Pour chaque ligne du tableau, créez une instance du ScriptableObject et remplissez-la avec les données de cette ligne.
Enfin, ajoutez tous les ScriptableObjects à la ressource importée en utilisant le contexte. Nous devons également ajouter une ressource principale, que j'ai définie comme étant un TextAsset vide (car nous n'utilisons pas vraiment la ressource principale pour quoi que ce soit ici).
Cela fonctionne à la fois pour les bâtiments et les unités, mais vous devez vérifier lequel des deux vous importez, car les unités ont beaucoup plus de statistiques.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Une fois cette étape franchie, il existe maintenant des objets scriptables qui contiennent toutes les données de la feuille de calcul.

Les objets scriptables générés sont prêts à être utilisés dans le jeu selon les besoins. Vous pouvez également utiliser le PrefabPostprocessor qui a été mis en place précédemment.
Dans la méthode OnPostprocessPrefab, nous avons la possibilité de charger cet actif et d'utiliser ses données pour remplir automatiquement les paramètres des composants. De plus, si vous créez une dépendance à l'égard de ces données, les Prefabs seront réimportés chaque fois que vous modifierez les données, ce qui permettra de les mettre à jour automatiquement.
Lorsque vous essayez de créer des niveaux impressionnants, il est essentiel de pouvoir modifier et tester les choses rapidement, en faisant de petits ajustements et en essayant à nouveau. C'est pourquoi il est si important d'avoir des temps d'itération rapides et de réduire les étapes nécessaires pour commencer les tests.
L'une des premières choses auxquelles nous pensons lorsqu'il s'agit de temps d'itération dans Unity est le rechargement du domaine. Le rechargement du domaine est utile dans deux situations clés : après la compilation du code afin de charger les nouvelles bibliothèques liées dynamiquement (DLL) et lors de l'entrée et de la sortie du mode lecture. Le rechargement du domaine qui accompagne la compilation ne peut être évité, mais vous avez la possibilité de désactiver les rechargements liés au mode lecture dans Paramètres du projet > Éditeur > Paramètres d'entrée en mode lecture.
La désactivation du rechargement du domaine lors de l'entrée en mode lecture peut entraîner certains problèmes si votre code n'y est pas préparé, le problème le plus courant étant que les variables statiques ne sont pas réinitialisées après la lecture. Si votre code peut fonctionner avec cette désactivation, ne vous en privez pas. Pour ce prototype, le rechargement du domaine est désactivé, ce qui vous permet d'entrer en mode jeu presque instantanément.
Un autre problème lié aux temps d'itération concerne le recalcul des données nécessaires pour jouer. Il s'agit souvent de sélectionner certains composants et de cliquer sur des boutons pour déclencher les nouveaux calculs. Par exemple, dans ce prototype, il y a un TeamController pour chaque équipe de la scène. Ce contrôleur dispose d'une liste de tous les bâtiments ennemis afin d'envoyer les unités les attaquer. Pour remplir ces données automatiquement, utilisez la fonction IProcessSceneWithReport pour remplir automatiquement ces données. Cette interface est appelée pour les scènes à deux occasions différentes : pendant les constructions et lors du chargement d'une scène en mode lecture. Il permet de créer, de détruire et de modifier n'importe quel objet. Notez toutefois que ces changements n'affecteront que les constructions et le mode lecture.
C'est dans ce rappel que les contrôleurs sont créés et que la liste des bâtiments est établie. Grâce à cela, il n'est pas nécessaire de faire quoi que ce soit manuellement. Les contrôleurs contenant une liste actualisée des bâtiments seront présents lorsque le jeu commencera, et la liste sera mise à jour en fonction des changements que nous aurons apportés.
Pour le prototype, une méthode utilitaire a été mise en place qui permet d'obtenir toutes les instances d'un composant dans une scène. Vous pouvez l'utiliser pour obtenir tous les bâtiments :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Le reste du processus est assez trivial : Récupérez tous les bâtiments, récupérez toutes les équipes auxquelles appartiennent ces bâtiments et créez un contrôleur pour chaque équipe avec une liste de bâtiments ennemis.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Outre la scène en cours d'édition, vous devez également charger d'autres scènes pour pouvoir jouer (par exemple, une scène avec les gestionnaires, avec l'interface utilisateur, etc.) Cela peut prendre un temps précieux. Dans le cas du prototype, le Canvas avec les barres de santé se trouve dans une scène différente appelée InGameUI.
Une méthode efficace consiste à ajouter un composant à la scène avec une liste des scènes qui doivent être chargées en même temps qu'elle. Si vous chargez ces scènes de manière synchrone dans la méthode Awake, la scène sera chargée et toutes ses méthodes Awake seront invoquées à ce moment-là. Ainsi, lorsque la méthode Start est appelée, vous pouvez être sûr que toutes les scènes sont chargées et initialisées, ce qui vous permet d'accéder aux données qu'elles contiennent, telles que les singletons du gestionnaire.
N'oubliez pas que certaines scènes peuvent être ouvertes lorsque vous passez en mode lecture, il est donc important de vérifier si la scène est déjà chargée avant de la charger :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Dans les parties 1 et 2 de cet article, je vous ai montré comment tirer parti de certaines des fonctionnalités les moins connues d'Unity. Tout ce qui est décrit n'est qu'une fraction de ce qui peut être fait, mais j'espère que vous trouverez ces astuces utiles pour votre prochain projet ou, à tout le moins, intéressantes.
Les ressources utilisées pour créer le prototype sont disponibles gratuitement dans l'Asset Store :
- Squelettes : Toon RTS Units - Démonstration de morts-vivants
- Chevaliers : Toon RTS Units - Démo
- Tours : Tour de mage stylisée
Si vous souhaitez discuter de ce double épisode, ou partager vos idées après l'avoir lu, rendez-vous sur notre forum Scripting. Je m'arrête là pour l'instant, mais vous pouvez toujours me contacter sur Twitter à l'adresse @CaballolD. Ne manquez pas les prochains blogs techniques d'autres développeurs Unity dans le cadre de lasérie permanente Tech from the Trenches.