UI Toolkit pour UI de jeu Unity
Structure en UXML, apparence en USS, comportement dans de petits contrôleurs C# qui réagissent aux événements du jeu. Issu d'un projet réel (HUD de jeu d'horreur-tir) ; la liste de dépannage est la partie la plus susceptible de faire gagner du temps.
1. Avant d'écrire du code
Demandez uniquement ce qui manque, en un seul lot court :
- API UI.
[RequireComponent(typeof(PanelRenderer))]+RegisterUIReloadCallback= workflow PanelRenderer (pas deUIDocument, pas derootVisualElement).UIDocument+rootVisualElement= classique. Déduisez-le du code collé ; si ce n'est pas clair, demandez un contrôleur fonctionnel et son UXML. Ne mélangez jamais les deux (provoque des NullReferenceExceptions). Un fichier fonctionnel de l'utilisateur est meilleur que votre mémoire de l'API. - Genre/thème (avec captures d'écran ou palette), plateforme et entrées, résolution/rapport de référence.
- Les scripts du jeu auxquels l'UI s'abonne. Demandez-les, lisez les événements/propriétés publiques réels, liez-vous à ceux-ci. Si un événement manque, proposez le plus petit ajout au script du jeu.
- Répondez dans la langue de l'utilisateur ; écrivez les commentaires de code dans la langue utilisée par les scripts existants.
2. Guide de style d'abord
La cohérence est ce que les utilisateurs remarquent en premier. Avant de construire plusieurs parties : proposez un court guide, construisez UNE petite partie, laissez l'utilisateur réagir, et transformez ses retours en règles (« pas de boîte d'arrière-plan », « plus compact »). Sauvegardez-le dans le projet (ex. Assets/UI/UI_STYLE_GUIDE.md) et dites à l'utilisateur de le coller dans ses futures demandes UI. Mettez-le à jour lorsque des parties ou des règles changent. Contenu : langage de conception (ton, conteneurs, densité, mouvement), tableau de palette (texte, atténué, avertissement, critique, ombre), typographie (tailles par rôle, règle d'ombre), mise en page (zones, marges, Panel Settings), conventions de nommage USS et classes d'état, règles d'architecture de code, pièges UXML, liste des parties existantes, liste de contrôle pour une nouvelle partie.
3. Architecture
- Une partie = un contrôleur + un UXML + un USS, avec une source de données. Pas de méga-contrôleur.
- Les parties qui doivent être ensemble partagent un UXML/conteneur flex (deux contrôleurs peuvent partager un GameObject/PanelRenderer ; chacun trouve ses propres éléments par nom).
- Les contrôleurs ne font que trouver des éléments, définir du texte/valeurs et basculer des classes USS. Les couleurs, tailles et animations vivent dans USS.
- PanelRenderer : la racine arrive via
RegisterUIReloadCallback(OnUIReload)(enregistrer dans OnEnable, désenregistrer dans OnDisable). Il se déclenche quand l'UI est prête et à chaque rechargement, donc TOUTES les recherchesQ<T>("name"), la mise en cache, la liaison d'événements et la réinitialisation d'état y vont. ProtégezUpdate()avec un null-guard jusqu'à ce que les éléments existent. - Les événements du jeu peuvent se déclencher avant que l'UI n'existe : mettez en cache les dernières valeurs dans des champs, réappliquez à la fin de
OnUIReload. - Les objets joueur/jeu peuvent apparaître tard : référence sérialisée, sinon
FindFirstObjectByTypelimité (~0.5 s), puis liez ; désabonnez-vous dans OnDisable. - Poussez, ne scrutez pas. Abonnez-vous aux événements et écrivez uniquement en cas de changement. Scrutez uniquement les valeurs sans événement (endurance continue, un booléen de rechargement), en basculant les classes en cas de changement. Pas de construction de chaîne par frame.
- Construisez les éléments répétés (pips, segments) en code une fois ; reconstruisez uniquement lorsque leur source change ; effacez les listes mises en cache lors du rechargement.
- Minuteurs/pulsations :
element.schedule.Execute(...).Every(ms)créé en pause, repris uniquement si nécessaire. Flashs uniques :.StartingIn(ms). - Éléments HUD :
picking-mode="Ignore"partout, pas de contrôles focusables. Les menus sont l'exception. - Nom de fichier = nom de la classe MonoBehaviour.
4. Mise en page (source de la plupart des bugs visuels)
- Ne placez jamais des parties liées avec des décalages en pixels devinés séparément : deux panneaux absolus se chevauchaient parce que
bottoma été estimé à l'aveugle. Mettez-les dans un seul conteneur flex et positionnez uniquement ce conteneur. - Un conteneur en position absolue par zone d'écran ; les enfants s'écoulent avec flex et marges.
- Masquez avec
opacity+ transition lorsque la mise en page ne doit pas sauter ;display:noneuniquement lorsque l'espace doit se réduire. widthfixe sur les étiquettes numériques afin que 9, 10 et 100 ne décalent pas les voisins.- Les boîtes sont en border-box : largeur de rangée de segments =
count x (width + margin) - margin. Alignez les widgets liés par un bord gauche identique et une largeur calculée. - Panel Settings : Scale With Screen Size (ex. 1920x1080, match 0.5) ; pour les HUD, désactivez Clear Color et Clear Depth Stencil. Utilisez Sort Order du PanelRenderer pour superposer les parties (vignette plein écran en dessous du texte).
5. USS et état
- État = classe basculée depuis C# (
EnableInClassList) ; animation = transition USS. Ne définissez jamais les couleurs ou n'animez pas en code. - Nommez les états par leur signification :
--low,--critical,--hidden,--hit,--pulse,--unavailable. Préférez BEM pour les nouvelles parties. - Les règles de surcharge (segments dépensés, flash) vont APRÈS les règles qu'elles remplacent ; spécificité égale, la dernière gagne.
- Une transition utilise la durée/délai de l'état de destination :
.x--hidden { opacity:0; transition-duration:.5s; transition-delay:.9s; }disparaît tard et apparaît instantanément ;.x--flash { transition-duration:.05s; }s'allume rapidement, s'éteint à la vitesse de base. - Le texte flottant sans arrière-plan a besoin de
text-shadow. Pas detext-transform(majuscule en C#) ; pas de dégradés radiaux (voir vignette). - Pour une UI re-skinable, mettez les couleurs/tailles dans des variables USS sur une classe racine (
--c-text, utilisé commevar(--c-text)) ; vérifiez que la version Unity de l'utilisateur le prend en charge.
6. Squelette de contrôleur (PanelRenderer)
[RequireComponent(typeof(PanelRenderer))]
public class PartController : MonoBehaviour
{
[SerializeField] private SourceComponent source; // vide = trouver à l'exécution
private PanelRenderer _pr; private VisualElement _block; private Label _value;
private bool _bound; private float _v = -1f; private int _shown = -1; private float _nextFind;
private void Awake() => _pr = GetComponent<PanelRenderer>();
private void OnEnable() { _pr.RegisterUIReloadCallback(OnUIReload); TryBind(); }
private void OnDisable() { _pr.UnregisterUIReloadCallback(OnUIReload); Unbind(); _block = null; _value = null; }
private void Update()
{
if (_bound || Time.unscaledTime < _nextFind) return;
_nextFind = Time.unscaledTime + 0.5f; TryBind();
}
private void TryBind()
{
if (_bound) return;
if (source == null) source = FindFirstObjectByType<SourceComponent>();
if (source == null) return;
source.OnChanged += OnChanged; _bound = true;
_v = source.Current; Apply(); // récupérer l'état actuel une fois
}
private void Unbind() { if (_bound && source != null) source.OnChanged -= OnChanged; _bound = false; }
private void OnUIReload(PanelRenderer r, VisualElement root)
{
_block = root.Q<VisualElement>("part"); _value = root.Q<Label>("part-value");
if (_block == null || _value == null) { Debug.LogError("Part : éléments introuvables dans UXML (vérifier Source Asset).", this); _block = null; return; }
_shown = -1; Apply(); // nouvel arbre : forcer la réécriture depuis le cache
}
private void OnChanged(float v) { _v = v; Apply(); }
private void Apply()
{
if (_block == null || _v < 0f) return;
int n = Mathf.CeilToInt(_v);
if (n != _shown) { _shown = n; _value.text = n.ToString(); } // écrire uniquement en cas de changement
_block.EnableInClassList("part--low", _v <= 0.3f);
}
}
7. Extraits réutilisables
- Segments/pips : dans
OnUIReload,row.Clear(); list.Clear();puis ajoutez NVisualElement{pickingMode=Ignore}avec la classeseg; en cas de changementlist[i].EnableInClassList("seg--spent", i >= filled). Affichez au moins 1 rempli tant que la valeur > 0 :filled = v<=0 ? 0 : Clamp(CeilToInt(v*N-0.001f),1,N). - Pulsation :
_pulse = _block.schedule.Execute(() => _block.ToggleInClassList("part--pulse")).Every(400); _pulse.Pause();Reprendre en entrant dans l'état, Pause + retirer la classe en sortant ; suivez un booléen_pulsinget réinitialisez-le lors du rechargement. - Flash sur événement :
var b=_block; b.AddToClassList("part--hit"); b.schedule.Execute(()=>b.RemoveFromClassList("part--hit")).StartingIn(150); - Vignette : générez une fois dans Awake un
Texture2D128x128 (RGBA32, Clamp, Bilinear,HideFlags.HideAndDontSave) où alpha =SmoothStep(0,1,InverseLerp(inner,outer,dist/1.4142))depuis le centre, RGB blanc ; définissez_el.style.backgroundImage = new StyleBackground(tex)dans OnUIReload ; détruisez dans OnDestroy. USS : élément absolu plein écran,opacity:0,-unity-background-scale-mode: stretch-to-fill,-unity-background-image-tint-colorpour la couleur, classes--low/--critical/--pulse/--flashdéfinissant l'opacité (flash en dernier, durée rapide). Sort Order plus bas que les parties de texte.
8. Genres et plateformes
L'architecture est identique d'un genre à l'autre ; le guide de style et le modèle d'entrée changent. Arcade : gros chiffres en bloc, comptage, pop d'échelle, lueur de combo, transitions percutantes de 0,08 à 0,25 s, conteneurs bienvenus. Rétro/pixel : police pixel, mise à l'échelle entière, textures filtrées Point, bordures 9-slice sprite. Mobile : cibles tactiles d'environ 44-48 dp, zone de sécurité de l'appareil comme rembourrage racine (vérifiez l'API actuelle pour la version Unity de l'utilisateur), pas d'états au survol, police minimale plus grande, testez sur un appareil réel. Console/manette : éléments focusables avec un style :focus fort, focus initial explicite, marges TV-safe (3-5 pour cent), texte plus grand, glyphes qui changent avec l'appareil actif. VR/espace monde : dépend de la version Unity, vérifiez d'abord. Ces points de plateforme sont une bonne pratique générale mais non testés dans un projet réel : dites-le et demandez à l'utilisateur de vérifier sur l'appareil. Conservez les étiquettes d'entrée (nom de touche ou glyphe) dans les données lues par le contrôleur, pas codées en dur dans UXML.
9. Livraison d'une partie
Dans l'ordre : UXML complet, USS complet, contrôleur complet, étapes de configuration (GameObject, PanelRenderer + Panel Settings partagé + Source Asset, Sort Order, références à remplir, quoi voir dans Play), boutons de réglage (quelle valeur USS change la taille/position/couleur/seuils), et une courte entrée dans le guide de style. Lorsque vous modifiez des fichiers existants, donnez les fichiers entiers si la majeure partie du fichier change (des collages partiels ont causé de vraies erreurs).
10. Dépannage
| Symptôme | Cause / correctif |
|---|---|
NullReference sur rootVisualElement / GetComponent<UIDocument>() | Le code utilise UIDocument mais l'objet a PanelRenderer. Utilisez RegisterUIReloadCallback, liez dans OnUIReload |
| Le contrôleur journalise « élément introuvable » | Mauvais Source Asset, nom ne correspondant pas à Q<>(), ou UXML invalide |
| UI Builder « Échec d'ouverture de l'actif » | UXML invalide (duplicata </ui:UXML>, mauvais <Style>, GUID de placeholder, caractères cachés). Corrigez, puis cliquez droit sur le fichier > Reimport. Lisez le message dans la console au-dessus de l'erreur générique |
| Éléments non stylisés | <Style> manquant ou mauvais chemin. Dans UI Builder : StyleSheets > + > Add Existing USS (écrit le chemin complet project://database/...guid=...) |
| Tout a disparu après une modification | L'UXML s'est cassé, ou la référence Source Asset est devenue obsolète après un renommage. Corrigez, Reimport, réassignez. Testez avec background-color: rgba(255,0,0,.3) sur la racine |
| Partie mal placée ou chevauchant une autre | GameObject obsolète dessinant encore l'ancien UXML, ou décalages absolus séparés. Supprimez l'ancien objet, fusionnez les voisins dans un seul conteneur |
| Texte de placeholder dans la vue Game | Le mode édition dessine l'UXML ; appuyez sur Play |
| Le script ne peut pas être ajouté | Nom de fichier != nom de classe. Renommez dans la fenêtre Projet Unity pour que .meta suive |
Correctifs non valides : la balise de feuille de style est <Style src="..."/> SANS le préfixe ui: (<ui:Style> est silencieusement ignoré) ; n'inventez ni ne laissez jamais un GUID de placeholder ; utilisez l'en-tête standard <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">.
11. Travailler sans voir l'éditeur
Demandez une capture d'écran en cas de plainte de mise en page et lisez-la attentivement ; cela a résolu des bugs que les devinettes ne pouvaient pas. Reconnaissez brièvement vos erreurs et corrigez avec des fichiers complets. Pour les erreurs de compilation, demandez le texte exact et la version Unity plutôt que de deviner à nouveau. Ne promettez pas de comportement sur appareil ou de chiffres de performance ; construisez la structure et demandez à l'utilisateur de tester.