Le développement de nouvelles fonctionnalités ou de modules d'interface utilisateur (UI) dans Unity implique souvent des tâches répétitives : créer plusieurs scripts, des préfabriqués, puis lier manuellement des composants UI. Ce processus peut être fastidieux et source d'erreurs. Cet article présente une approche pour automatiser ces tâches récurrentes au sein de l'éditeur Unity, accélérant ainsi le flux de travail.
Génération rapide de modules système
Une fonctionnalité clé est la capacité à générer en un seul clic un nouveau module complet, par exemple, un module UI. Cette opération est accessible via une fenêtre d'éditeur.
Pour l'utiliser, sélectionnez l'option "Outils/Générer Module UI" dans la barre de menus de Unity. Une fenêtre s'affichera, vous invitant à spécifier le nom du nouveau module. Si un module portant ce nom existe déjà, un avertissement s'affichera. Vous aurez également l'option de générer automatiquement le fichier de liaison (Binder) pour les objets UI de ce module.
Après avoir entré le nom et choisi les options, cliquez sur "Démarrer la Génération". L'outil créera une nouvelle arborescence de fichiers et dossiers :
- Un dossier pour les scripts du module (par exemple,
Assets/Scripts/UI/MonNouveauModule). - Un fichier script principal (
MonNouveauModuleView.cs). - Un préfabriqué UI de base (
MonNouveauModule.prefab) avec unRectTransformet uneImagepar défaut. - Si l'option a été sélectionnée, un script de liaison (
MonNouveauModuleBinder.cs) sera également généré, prêt à référencer les composants UI.
Liaison automatisée des objets UI
En complément de la création de modules, un autre outil permet de générer ou de mettre à jour un script de liaison pour un préfabriqué UI existant. Ce script simplifie l'accès aux composants UI en les rendant directement accessibles via des variables publiques.
Sélectionnez un préfabriqué UI dans la fenêtre "Projet" de Unity, puis faites un clic droit et choisissez "Assets/Lier les Composants UI". Le système analysera la hiérarchie du préfabriqué et générera un fichier de liaison (par exemple, MonPrefabBinder.cs) dans un dossier désigné (comme Assets/Scripts/UI/_Bind/).
Convention de nommage pour la liaison UI
Pour que l'outil puisse identifier et lier automatiquement des composants spécifiques, suivez une convention de nommage pour les objets dans votre préfabriqué UI :
- Prefixez les noms des objets par des alias courts séparés par des underscores (
_) pour indiquer les composants à lier. - Exemple : Un objet nommé
btn_txt_PlayButtonindiquera à l'outil de lier les composantsButtonetTextde cet objet, en plus de sonTransform. - Les alias supportés par défaut incluent :
trs(Transform),img(Image),btn(Button),txt(Text),scr(ScrollRect),cg(CanvasGroup),tog(Toggle),sld(Slider).
Le script généré contiendra des variables publiques pour chaque composant identifié et une méthode Init(Transform root) qui les trouvera et les attribuera lors de l'exécution.
Implémentation des outils d'édition
1. Copie du chemin d'accès aux assets
Un utilitaire simple pour copier le chemin d'accès d'un asset sélectionné vers le presse-papiers.
using UnityEditor;
using UnityEngine;
public static class AssetPathCopier
{
[MenuItem("Assets/Copier Chemin d'Accès")]
private static void CopySelectedAssetPath()
{
string[] guids = Selection.assetGUIDs;
if (guids.Length == 1)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
Debug.LogFormat($"Chemin copié au presse-papiers : <color=#ff0000>{assetPath}</color>");
GUIUtility.systemCopyBuffer = assetPath;
}
else if (guids.Length > 1)
{
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
Debug.LogFormat($"<color=#ff0000>{assetPath}</color>");
}
}
}
}
2. Générateur de scripts de liaison UI
Ce script analyse la hiérarchie d'un préfabriqué et génère un fichier C# qui lie automatiquement les composants UI basés sur les conventions de nommage.
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
namespace EditorTools.UI
{
public static class UIComponentBinder
{
// Définition des alias pour les types de composants
private static readonly Dictionary<string, string> ComponentAliases = new Dictionary<string, string>
{
{"trs", "Transform"},
{"img", "Image"},
{"btn", "Button"},
{"txt", "Text"},
{"scr", "ScrollRect"},
{"cg", "CanvasGroup"},
{"tog", "Toggle"},
{"sld", "Slider"}
};
[MenuItem("Assets/Lier les Composants UI")]
private static void CreateBindingScriptForSelection()
{
GameObject selectedObject = Selection.activeObject as GameObject;
Assert.IsNotNull(selectedObject, "Veuillez sélectionner un GameObject (préfabriqué ou objet de scène).");
if (selectedObject == null)
{
Debug.LogError("La sélection n'est pas un GameObject valide.");
return;
}
GenerateBindingScript(selectedObject);
}
public static void GenerateBindingScript(GameObject targetGameObject)
{
System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
stopwatch.Start();
List<string> hierarchyPaths = new List<string>();
CollectHierarchyPaths(targetGameObject, "", hierarchyPaths);
try
{
ProduceBindingFile(hierarchyPaths, targetGameObject.name, targetGameObject.transform);
stopwatch.Stop();
string bindPath = $"Assets/Scripts/UI/_Bind/{targetGameObject.name}Binder.cs";
Debug.LogFormat($"<color=#00ff00>Génération du script de liaison terminée : {bindPath} ({stopwatch.ElapsedMilliseconds} ms)</color>");
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<Object>(bindPath));
}
catch (System.Exception e)
{
Debug.LogError($"Échec de la génération du script de liaison : {e.Message}\n{e.StackTrace}");
}
}
// Collecte récursive des chemins d'accès de tous les enfants du GameObject
private static void CollectHierarchyPaths(GameObject currentGameObject, string currentPath, List<string> resultList)
{
foreach (Transform childTransform in currentGameObject.transform)
{
string newPath = string.IsNullOrEmpty(currentPath) ? childTransform.name : currentPath + "/" + childTransform.name;
resultList.Add(newPath);
CollectHierarchyPaths(childTransform.gameObject, newPath, resultList);
}
}
// Génère le contenu du fichier de liaison
private static void ProduceBindingFile(List<string> allPaths, string baseClassName, Transform rootTransform)
{
StringBuilder variableDeclarations = new StringBuilder();
StringBuilder initializationLogic = new StringBuilder();
Dictionary<string, int> nameOccurrenceCount = new Dictionary<string, int>();
foreach (string path in allPaths)
{
string objectName = path.Contains("/") ? path.Substring(path.LastIndexOf("/") + 1) : path;
string[] nameParts = objectName.Split('_');
if (nameParts.Length < 2) // Nécessite au moins un préfixe d'alias et un nom réel
{
continue;
}
string realObjectName = nameParts[nameParts.Length - 1];
realObjectName = Regex.Replace(realObjectName, @"[^a-zA-Z0-9]", ""); // Nettoyer le nom
if (string.IsNullOrEmpty(realObjectName)) continue;
foreach (string prefix in nameParts)
{
if (ComponentAliases.TryGetValue(prefix.ToLower(), out string componentType))
{
string fieldBaseName = realObjectName + "_" + prefix.ToUpper(); // e.g., MyButton_BTN
string uniqueFieldName = GetUniqueFieldName(fieldBaseName, nameOccurrenceCount);
variableDeclarations.AppendLine($"\t\tpublic {componentType} {uniqueFieldName};");
initializationLogic.AppendLine($"\t\t\t{uniqueFieldName} = rootTransform.Find(\"{path}\").GetComponent<{componentType}>();");
}
}
}
string className = baseClassName + "Binder";
string fileContent = GenerateBindingClassTemplate(className, variableDeclarations.ToString(), initializationLogic.ToString());
string scriptDirectory = "Assets/Scripts/UI/_Bind";
if (!Directory.Exists(scriptDirectory))
{
Directory.CreateDirectory(scriptDirectory);
}
File.WriteAllText($"{scriptDirectory}/{className}.cs", fileContent);
AssetDatabase.Refresh();
}
// Génère un nom de champ unique en cas de doublons
private static string GetUniqueFieldName(string baseName, Dictionary<string, int> occurrenceMap)
{
if (occurrenceMap.TryGetValue(baseName, out int count))
{
occurrenceMap[baseName]++;
return baseName + (count + 1);
}
else
{
occurrenceMap.Add(baseName, 1);
return baseName;
}
}
// Modèle de classe de liaison
private static string GenerateBindingClassTemplate(string className, string variables, string methods)
{
return $@"
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic; // Pour List, Dictionary, etc.
public class {className}
{{
{variables}
public void Init(Transform rootTransform)
{{
{methods}
}}
}}";
}
}
}
3. Fenêtre de génération de modules
Cette fenêtre d'éditeur permet de créer un nouveau module UI avec son préfabriqué, son script principal et, optionnellement, son script de liaison.
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using EditorTools.UI; // Assurez-vous d'importer le namespace du binder
public class ModuleGeneratorWindow : EditorWindow
{
private string _moduleName = "";
private bool _generateBinderScript = true;
private const string SCRIPTS_BASE_PATH = "Assets/Scripts/UI";
private const string PREFABS_BASE_PATH = "Assets/Resources/UI/Modules";
[MenuItem("Outils/Générer Module UI")]
private static void OpenModuleCreationWindow()
{
ModuleGeneratorWindow window = GetWindow<ModuleGeneratorWindow>("Générateur de Module UI");
window.minSize = new Vector2(400, 200);
window.Show();
}
private void OnGUI()
{
EditorGUILayout.Space(10);
_moduleName = EditorGUILayout.TextField("Nom du nouveau module :", _moduleName).Trim();
EditorGUILayout.Space(5);
GUIStyle errorStyle = new GUIStyle(GUI.skin.label) { normal = { textColor = Color.red } };
if (string.IsNullOrEmpty(_moduleName))
{
EditorGUILayout.LabelField("Le nom du module ne peut pas être vide.", errorStyle);
}
else if (ModuleExists(_moduleName))
{
EditorGUILayout.LabelField($"Un module nommé '{_moduleName}' existe déjà.", errorStyle);
}
else
{
_generateBinderScript = EditorGUILayout.Toggle("Générer le script de liaison (Binder) ?", _generateBinderScript);
if (GUILayout.Button("Démarrer la Génération"))
{
CreateNewModule(_moduleName, _generateBinderScript);
if (EditorUtility.DisplayDialog("Succès", $"Le module '{_moduleName}' a été généré avec succès !", "OK"))
{
Close();
}
}
}
}
// Vérifie si un module avec le même nom existe déjà
private bool ModuleExists(string name)
{
string scriptFolderPath = $"{SCRIPTS_BASE_PATH}/{name}";
string prefabPath = $"{PREFABS_BASE_PATH}/{name}.prefab";
return Directory.Exists(scriptFolderPath) || File.Exists(prefabPath);
}
// Crée le nouveau module
private void CreateNewModule(string name, bool generateBinder)
{
// 1. Création du préfabriqué UI
string prefabFolderPath = PREFABS_BASE_PATH;
if (!Directory.Exists(prefabFolderPath))
{
Directory.CreateDirectory(prefabFolderPath);
}
string prefabFilePath = $"{prefabFolderPath}/{name}.prefab";
GameObject uiRootObject = new GameObject(name);
RectTransform rectTransform = uiRootObject.AddComponent<RectTransform>();
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
Image backgroundImage = uiRootObject.AddComponent<Image>();
backgroundImage.color = new Color(0, 0, 0, 0.5f); // Semi-transparent noir par défaut
backgroundImage.raycastTarget = true;
GameObject createdPrefab = PrefabUtility.SaveAsPrefabAsset(uiRootObject, prefabFilePath);
DestroyImmediate(uiRootObject); // Détruire l'objet de scène après avoir créé le préfabriqué
// 2. Création du script principal du module
string scriptFolderPath = $"{SCRIPTS_BASE_PATH}/{name}";
if (!Directory.Exists(scriptFolderPath))
{
Directory.CreateDirectory(scriptFolderPath);
}
string scriptFilePath = $"{scriptFolderPath}/{name}View.cs";
string binderDeclaration = "";
string binderInitialization = "";
if (generateBinder)
{
UIComponentBinder.GenerateBindingScript(createdPrefab); // Générer le script de liaison
binderDeclaration = $"\tprivate {name}Binder _uiBinder = new {name}Binder();";
binderInitialization = $"\t\t_uiBinder.Init(transform);";
}
string moduleScriptContent = GenerateModuleScriptTemplate(name, binderDeclaration, binderInitialization);
File.WriteAllText(scriptFilePath, moduleScriptContent);
AssetDatabase.Refresh(); // Rafraîchir l'AssetDatabase pour voir les nouveaux fichiers
// Mettre en évidence les assets créés dans le Project Window
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<Object>(prefabFilePath));
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<Object>(scriptFilePath));
}
// Modèle de script pour le module principal
private static string GenerateModuleScriptTemplate(string moduleName, string binderDeclaration, string binderInitialization)
{
return $@"
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
public class {moduleName}View : MonoBehaviour
{{
{binderDeclaration}
private void Awake()
{{
{binderInitialization}
// Initialisation ou configuration supplémentaire ici
Debug.Log($""Module {moduleName} initialisé."");
}}
// Ajoutez ici la logique spécifique de votre module UI
}}";
}
}
Considérations importantes
- Les chemins de sauvegarde pour les scripts et les préfabriqués (
SCRIPTS_BASE_PATH,PREFABS_BASE_PATH) sont configurables dans le code des outils d'édition. Adaptez-les à la structure de votre projet. - La fenêtre de génération de modules crée un préfabriqué de base avec un
RectTransformet uneImage. Vous devrez personnaliser ce préfabriqué avec vos propres éléments UI après la génération. - Lors de l'utilisation du générateur de script de liaison, assurez-vous que les noms d'objets UI respectent la convention de préfixe (
alias_NomObjet) pour une détection et une liaison correctes. - Évitez les noms d'bojets en double au même niveau de la hiérarchie pour simplifier la génération du script de liaison.