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

Dans la plupart des projets que j'ai vus, les développeurs effectuent de nombreuses tâches répétitives et sujettes aux erreurs, en particulier lorsqu'il s'agit d'intégrer de nouveaux éléments artistiques. Par exemple, la configuration d'un personnage implique souvent de glisser-déposer de nombreuses références d'actifs, de cocher des cases et de cliquer sur des boutons : Définir le rig du modèle à Humanoid, désactiver le sRGB de la texture SDF, définir les normal maps comme des normal maps, et les textures UI comme des sprites. En d'autres termes, un temps précieux est perdu et des étapes cruciales peuvent encore être manquées.
Dans cet article en deux parties, je vous présenterai des astuces qui peuvent vous aider à améliorer ce flux de travail afin que votre prochain projet se déroule plus facilement que le précédent. Pour illustrer cela, j'ai créé un prototype simple - similaire à un STR - où les unités d'une équipe attaquent automatiquement les bâtiments et les autres unités ennemies. Avec chaque hack de script, j'améliorerai un aspect de ce processus, qu'il s'agisse des textures ou des modèles.
Voici à quoi ressemble le prototype :
La raison principale pour laquelle les développeurs doivent régler tant de petits détails lors de l'importation d'actifs est simple : Unity ne sait pas comment vous allez utiliser une ressource, il ne peut donc pas savoir quels sont les meilleurs paramètres pour cette ressource. Si vous souhaitez automatiser certaines de ces tâches, c'est le premier problème à résoudre.
Le moyen le plus simple de savoir à quoi sert une ressource et comment elle est liée à d'autres est de respecter une convention de dénomination et une structure de dossier spécifiques, par exemple :
- Convention d'appellation: Nous pouvons ajouter des éléments au nom de la ressource elle-même. Ainsi, Shield_BC.png est la couleur de base, tandis que Shield_N.png est la carte normale.
- Structure du dossier: Knight/Animations/Walk.fbx est clairement une animation, tandis que Knight/Models/Knight.fbx est un modèle, même s'ils partagent tous deux le même format (.fbx).
Le problème, c'est que cette méthode ne fonctionne que dans un sens. Ainsi, alors que vous pouvez déjà savoir à quoi sert un actif lorsqu'on vous donne sa trajectoire, vous ne pouvez pas déduire sa trajectoire si vous ne disposez que d'informations sur ce que fait l'actif. La possibilité de trouver une ressource - par exemple, le matériel d'un personnage - est utile pour automatiser la configuration de certains aspects de la ressource. Bien que ce problème puisse être résolu par l'utilisation d'une convention de dénomination rigide pour s'assurer que le chemin est facile à déduire, il est toujours susceptible d'être source d'erreurs. Même si vous vous souvenez de la convention, les fautes de frappe sont fréquentes.
Une approche intéressante pour résoudre ce problème consiste à utiliser des étiquettes. Vous pouvez utiliser un script de l'éditeur qui analyse les chemins d'accès des ressources et leur attribue des étiquettes en conséquence. Les étiquettes étant automatisées, il est possible de déterminer l'étiquette exacte qui sera apposée sur un bien. Vous pouvez même rechercher des actifs à partir de leur étiquette en utilisant la fonction AssetDatabase.FindAssets.
Si vous souhaitez automatiser cette séquence, il existe une classe qui peut s'avérer très pratique : la classe AssetPostprocessor. L'AssetPostprocessor reçoit divers messages lorsque Unity importe des actifs. L'un d'entre eux est OnPostprocessAllAssetsune méthode qui est appelée chaque fois qu'Unity a fini d'importer des éléments. Il vous indiquera tous les chemins d'accès aux ressources importées, ce qui vous permettra de traiter ces chemins. Vous pouvez écrire une méthode simple, comme la suivante, pour les traiter :
Dans le cas du prototype, concentrons-nous sur la liste des actifs importés - à la fois pour essayer de repérer les nouveaux actifs et les actifs déplacés. Après tout, si le chemin change, nous pourrions vouloir mettre à jour les étiquettes.
Pour créer les étiquettes, il faut analyser le chemin d'accès et rechercher les dossiers, les préfixes et les suffixes du nom, ainsi que les extensions. Une fois les étiquettes générées, combinez-les en une seule chaîne et attribuez-les à la ressource.
Pour attribuer les étiquettes, chargez le bien à l'aide de la commande AssetDatabase.LoadAssetAtPath, puis attribuez ses étiquettes à l'aide de la commande AssetDatabase.SetLabels.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
N'oubliez pas qu'il est important de ne modifier les étiquettes que si elles ont réellement changé. La définition d'étiquettes déclenche une réimportation de l'actif, ce qui n'est pas souhaitable, sauf en cas de nécessité absolue.
Si vous vérifiez ce point, la réimportation ne posera pas de problème : Les étiquettes sont définies lors de la première importation d'une ressource et enregistrées dans le fichier .meta, ce qui signifie qu'elles sont également enregistrées dans votre système de contrôle de version. Une réimportation ne sera déclenchée que si vous renommez ou déplacez vos actifs.
Une fois les étapes ci-dessus terminées, tous les actifs sont automatiquement étiquetés, comme dans l'exemple ci-dessous.

L'importation de textures dans un projet implique généralement de modifier les paramètres de chaque texture. S'agit-il d'une texture régulière ? Une carte normale ? Un sprite ? Est-il linéaire ou sRGB ? Si vous souhaitez modifier les paramètres d'un importateur d'actifs, vous pouvez à nouveau utiliser l'AssetPostprocessor.
Dans ce cas, vous devez utiliser la fonction OnPreprocessTexture qui est appelé juste avant l'importation d'une texture. Cela vous permet de modifier les paramètres de l'importateur.
Lorsqu'il s'agit de sélectionner les bons paramètres pour chaque texture, vous devez vérifier avec quel type de texture vous travaillez - c'est exactement pourquoi les étiquettes sont essentielles dans la première étape.
Avec ces informations, vous pouvez écrire un simple TexturePreprocessor :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Il est important de s'assurer que vous n'exécutez cette opération que pour les textures qui ont le label art (nos propres textures). Vous obtiendrez alors une référence à l'importateur qui vous permettra de tout configurer, à commencer par la taille de la texture.
L'AssetPostprocessor dispose d'une propriété contextuelle qui permet de déterminer la plate-forme cible. Ainsi, vous pouvez effectuer des modifications spécifiques à la plate-forme, comme la réduction de la résolution des textures pour les téléphones portables :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Ensuite, vérifiez l'étiquette pour voir si la texture est une texture d'interface utilisateur et définissez-la en conséquence :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Pour le reste des textures, définissez les valeurs par défaut. Il convient de noter que l'albédo est la seule texture pour laquelle l'option sRGB est activée :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Grâce au script ci-dessus, lorsque vous glisserez et déposerez les nouvelles textures dans l'éditeur, elles auront automatiquement les bons paramètres en place.
Le "Channel packing" désigne la combinaison de diverses textures en une seule en utilisant les différents canaux. Elle est courante et présente de nombreux avantages. Par exemple, la valeur du canal rouge est métallique et la valeur du canal vert est sa douceur.
Cependant, la combinaison de toutes les textures en une seule nécessite un travail supplémentaire de la part de l'équipe artistique. Si l'emballage doit être modifié pour une raison quelconque (c'est-à-dire un changement dans le shader), l'équipe artistique devra refaire toutes les textures utilisées avec ce shader.
Comme vous pouvez le constater, des améliorations sont possibles. L'approche que j'aime utiliser pour le channel packing consiste à créer un type de ressource spécial dans lequel vous définissez les textures "brutes" et générez une texture channel packed à utiliser dans vos matériaux.
Tout d'abord, je crée un fichier fictif avec une extension spécifique, puis j'utilise une application importateur de scripts qui se charge de toutes les tâches lourdes lors de l'importation de cette ressource. Voici comment cela fonctionne :
- Les importateurs peuvent avoir des paramètres, tels que les textures à combiner.
- Depuis l'importateur, vous pouvez définir les textures comme une dépendance, ce qui permet de réimporter la ressource factice chaque fois que l'une des textures sources est modifiée. Cela vous permet de reconstruire les textures générées en conséquence.
- L'importateur a une version. Si vous devez changer la façon dont les textures sont emballées, vous pouvez modifier l'importateur et augmenter la version. Cela forcera une régénération de toutes les textures emballées dans votre projet et tout sera emballé de la nouvelle manière, immédiatement.
- Un effet secondaire intéressant de la génération d'éléments dans un importateur est que les éléments générés ne se trouvent que dans le dossier Library, ce qui évite d'encombrer votre contrôle de version.
Pour ce faire, créez un objet scriptable qui contiendra les textures créées et servira de résultat à l'importateur. Dans l'exemple, j'ai appelé cette classe TexturePack.
Une fois celle-ci créée, vous pouvez commencer par déclarer la classe de l'importateur et ajouter l'attribut ScriptedImporterAttribute pour définir la version et l'extension associées à l'importateur :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Dans l'importateur, déclarez les champs que vous souhaitez utiliser. Ils apparaîtront dans l'inspecteur, tout comme les MonoBehaviours et les ScriptableObjects :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.

Les paramètres étant prêts, créez de nouvelles textures à partir de celles que vous avez définies comme paramètres. Notez cependant que dans le préprocesseur (de la section précédente), nous avons fixé isReadable à True pour ce faire.
Dans ce prototype, vous remarquerez deux textures : l'albédo, qui contient l'albédo dans le canal RVB et un masque pour l'application de la couleur du lecteur dans le canal Alpha, et la texture du masque, qui contient le métallique dans le canal rouge et le lisse dans le canal vert.
Bien que cela sorte du cadre de cet article, voyons comment combiner l'albédo et le masque du joueur à titre d'exemple. Tout d'abord, il faut vérifier si les textures sont définies et, si c'est le cas, obtenir leurs données de couleur. Définissez ensuite les textures en tant que dépendances à l'aide de AssetImportContext.DependsOnArtifact. Comme mentionné ci-dessus, cela obligera l'objet à être recalculé si l'une des textures est modifiée.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Vous devez également créer une nouvelle texture. Pour ce faire, obtenez la taille du TexturePreprocessor que vous avez créé dans la section précédente de manière à ce qu'elle respecte les restrictions prédéfinies :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Ensuite, remplissez toutes les données de la nouvelle texture. Il serait possible d'optimiser massivement ce processus en utilisant Jobs et Burst (mais cela nécessiterait un article entier en soi). Nous utiliserons ici une simple boucle :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Définir ces données dans la texture :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Vous pouvez maintenant créer la méthode de génération d'une autre texture de manière très similaire. Une fois que cela est prêt, créez le corps principal de l'importateur. Dans ce cas, nous ne créerons que l'objet scriptable qui contient les résultats, crée les textures et définit le résultat de l'importateur à travers le contexte AssetImportContext.
Lorsque vous écrivez un importateur, tous les actifs générés doivent être enregistrés à l'aide de la commande AssetImportContext.AddObjectToAsset afin qu'ils apparaissent dans la fenêtre du projet. Sélectionnez un actif principal à l'aide de AssetImportContext.SetMainObject. Voici à quoi cela ressemble :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Il ne reste plus qu'à créer les actifs fictifs. Comme il s'agit de produits personnalisés, vous ne pouvez pas utiliser la fonction CreateAssetMenu. Vous devez les faire manuellement.
À l'aide de l'attribut MenuItem, indiquez le chemin complet du menu de création de la ressource, Assets/Create. Pour créer la ressource, utilisez ProjectWindowUtil.CreateAssetWithContent, qui génère un fichier avec le contenu que vous avez spécifié et permet à l'utilisateur d'entrer un nom pour ce fichier. Il se présente comme suit :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Enfin, créez les textures à canaux multiples.
La plupart des projets utilisent des shaders personnalisés. Parfois, ils sont utilisés pour ajouter des effets supplémentaires, comme un effet de dissolution pour faire disparaître les ennemis vaincus, et d'autres fois, les shaders mettent en œuvre un style artistique personnalisé, comme les shaders de toon. Quel que soit le cas d'utilisation, Unity créera de nouveaux matériaux avec le shader par défaut, et vous devrez le modifier pour utiliser le shader personnalisé.
Dans cet exemple, le shader utilisé pour les unités comporte deux caractéristiques supplémentaires : l'effet de dissolution et la couleur du joueur (rouge et bleu dans le prototype vidéo). Lorsque vous les mettez en œuvre dans votre projet, vous devez vous assurer que tous les bâtiments et toutes les unités utilisent le shader approprié.
Pour valider qu'un actif répond à certaines exigences - dans ce cas, qu'il utilise le bon shader - il existe une autre classe utile : l'AssetModificationProcessor. AssetModificationProcessor. Avec AssetModificationProcessor.OnWillSaveAssetsen particulier, vous serez notifié lorsque Unity est sur le point d'écrire un actif sur le disque. Vous aurez ainsi la possibilité de vérifier si l'actif est correct et de le corriger avant qu'il ne soit enregistré.
En outre, vous pouvez "demander" à Unity de ne pas enregistrer l'actif, ce qui est efficace lorsque le problème que vous détectez ne peut pas être résolu automatiquement. Pour ce faire, créez la méthode OnWillSaveAssets :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Pour traiter les biens, vérifiez s'il s'agit de matériaux et s'ils portent les bonnes étiquettes. S'ils correspondent au code ci-dessous, vous avez le bon shader :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Ce qui est pratique ici, c'est que ce code est également appelé lors de la création de l'actif, ce qui signifie que le nouveau matériau aura le bon shader.
En tant que nouvelle fonctionnalité dans Unity 2022, nous disposons également de Variantes de matériaux. Les variantes de matériaux sont extrêmement utiles lors de la création de matériaux pour les unités. En fait, vous pouvez créer un matériau de base et en dériver les matériaux pour chaque unité, en remplaçant les champs pertinents (comme les textures) et en héritant du reste des propriétés. Cela permet d'avoir des valeurs par défaut solides pour nos matériaux, qui peuvent être mises à jour si nécessaire.
L'importation d'animations est similaire à l'importation de textures. Plusieurs paramètres doivent être définis et certains d'entre eux peuvent être automatisés.
Unity importe par défaut les matériaux de tous les fichiers FBX (.fbx). Pour les animations, les matériaux que vous souhaitez utiliser se trouvent soit dans le projet, soit dans le FBX du maillage. Les matériaux supplémentaires provenant du fichier FBX de l'animation apparaissent chaque fois que vous recherchez des matériaux dans le projet, ce qui ajoute pas mal de bruit.
Pour configurer le rig - c'est-à-dire choisir entre Humanoïde et Générique, et dans les cas où nous utilisons un avatar soigneusement configuré, l'assigner - appliquer la même approche que celle appliquée aux textures. Mais pour les animations, le message à utiliser est le suivant AssetPostprocessor.OnPreprocessModel. Il sera appelé pour tous les fichiers FBX, vous devez donc distinguer les fichiers FBX d'animation des fichiers FBX de modèle.
Grâce aux étiquettes que vous avez créées précédemment, cela ne devrait pas être trop compliqué. La méthode commence de la même manière que pour les textures :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Ensuite, vous voudrez utiliser le rig du mesh FBX, vous devez donc trouver cet actif. Pour localiser le bien, utilisez à nouveau les étiquettes. Dans le cas de ce prototype, les animations ont des étiquettes qui se terminent par "animation", tandis que les mailles ont des étiquettes qui se terminent par "modèle". Vous pouvez effectuer un simple remplacement pour obtenir l'étiquette correspondant à votre modèle. Une fois que vous avez l'étiquette, recherchez votre bien en utilisant AssetDatabase.FindAssets avec "l:label-name" .
Lorsque vous accédez à d'autres actifs, il y a un autre élément à prendre en compte : Il est possible qu'au milieu du processus d'importation, l'avatar n'ait pas encore été importé lorsque cette méthode est appelée. Dans ce cas, la commande LoadAssetAtPath renverra un résultat nul et vous ne pourrez pas définir l'avatar. Pour contourner ce problème, définissez une dépendance sur le chemin de l'avatar. L'animation sera à nouveau importée une fois l'avatar importé, et vous pourrez la définir à cet endroit.
Si l'on transpose tout cela dans le code, on obtiendra quelque chose comme ceci :
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Vous pouvez maintenant faire glisser les animations dans le bon dossier, et si votre maillage est prêt, chacune d'entre elles sera configurée automatiquement. Mais si aucun avatar n'est disponible lorsque vous importez les animations, le projet ne pourra pas le récupérer une fois qu'il aura été créé. Au lieu de cela, vous devrez réimporter l'animation manuellement après l'avoir créée. Pour ce faire, cliquez avec le bouton droit de la souris sur le dossier contenant les animations et sélectionnez Réimporter.
Vous pouvez voir tout cela dans l'exemple de vidéo ci-dessous.
En reprenant exactement les mêmes idées que dans les sections précédentes, vous devez mettre en place les modèles que vous allez utiliser. Dans ce cas, utilisez AssetPostrocessor.OnPreprocessModel pour définir les paramètres de l'importateur pour ce modèle.
Pour le prototype, j'ai paramétré l'importateur pour qu'il ne génère pas de matériaux (j'utiliserai ceux que j'ai créés dans le projet) et j'ai vérifié si le modèle est une unité ou un bâtiment (en vérifiant l'étiquette, comme toujours). Les unités sont configurées pour générer un avatar, mais la création d'avatar pour les bâtiments est désactivée, car les bâtiments ne sont pas animés.
Pour votre projet, vous pouvez définir les matériaux et les animateurs (et tout ce que vous souhaitez ajouter) lors de l'importation du modèle. Ainsi, la préfabrication générée par l'importateur est prête à être utilisée immédiatement.
Pour ce faire, utilisez la méthode AssetPostprocessor.OnPostprocessModel pour ce faire. Cette méthode est appelée lorsque l'importation d'un modèle est terminée. Il reçoit en paramètre la préfabrication qui a été générée, ce qui nous permet de modifier la préfabrication à notre guise.
Pour le prototype, j'ai trouvé le matériau et le contrôleur d'animation en faisant correspondre l'étiquette, tout comme j'ai trouvé l'avatar pour les animations. Avec le Renderer et l'Animator dans le Prefab, je règle le matériau et le contrôleur comme dans un gameplay normal.
Vous pouvez ensuite déposer le modèle dans votre projet et il sera prêt à être intégré dans n'importe quelle scène. Sauf que nous n'avons pas défini de composants liés au gameplay, ce que j'aborderai dans la deuxième partie de ce blog.
Grâce à ces astuces de script avancées, vous êtes prêt à jouer. Restez à l'écoute pour le prochain article de cette série en deux parties intitulée Tech from the Trenches qui traitera des astuces pour équilibrer les données du jeu et bien plus encore.
Si vous souhaitez discuter de cet article ou partager vos idées après l'avoir lu, rendez-vous sur notre forum Scripting. Vous pouvez également me contacter sur Twitter à l'adresse @CaballolD.
