using System.Collections; using TMPro; using UnityEngine; // Le bandeau qui explique le panneau ouvert. // // À chaque panneau son message. Quand l'un s'affiche, le guide prend son texte et // descend de sa position de repos à sa position d'affichage ; quand plus aucun // panneau surveillé n'est ouvert, il remonte se ranger. // // Le guide se surveille lui-même : les panneaux sont activés directement par leur // toggle, personne ne prévient. Il suffit donc de le poser dans la scène et de // remplir sa liste, sans rien brancher. // // Hiérarchie attendue : // Guide <- ce script + RectTransform // Text (TMP) <- le message, trouvé tout seul si le champ est vide [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] public class Guide : MonoBehaviour { [System.Serializable] public class Entry { [Tooltip("Le panneau surveillé. Son affichage descend le guide avec le message ci-dessous.")] public GameObject panel; [TextArea(2, 4)] [Tooltip("Le message affiché tant que ce panneau est ouvert.")] public string message; } [Header("Références (vide = trouvées automatiquement)")] [Tooltip("Le bandeau déplacé. Vide = cet objet.")] [SerializeField] private RectTransform banner; [Tooltip("L'étiquette à remplir. Vide = premier TMP_Text trouvé sous le guide.")] [SerializeField] private TMP_Text label; [Header("Positions")] [Tooltip("Abscisse du bandeau, la même au repos et à l'affichage.")] [SerializeField] private float x; [Tooltip("Ordonnée au repos, hors de vue.")] [SerializeField] private float hiddenY = 5f; [Tooltip("Ordonnée une fois affiché.")] [SerializeField] private float shownY = -95f; [Header("Animation")] [Tooltip("Durée du déplacement, en secondes. 0 = pas d'animation.")] [SerializeField] private float duration = 0.35f; [Tooltip("Allure du déplacement, de 0 à 1.")] [SerializeField] private AnimationCurve ease = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); [Header("Messages")] [Tooltip("Un message par panneau. Le premier panneau ouvert de la liste l'emporte.")] [SerializeField] private Entry[] messages; // Le panneau dont le message est affiché. Sert à ne réagir qu'aux changements : // sans lui, l'animation repartirait à chaque image. private Entry _current; private Coroutine _sliding; public bool IsShown { get; private set; } // Le message affiché, pour qui veut le relire. public string Message => label != null ? label.text : ""; private RectTransform Banner { get { if (banner == null) banner = (RectTransform)transform; return banner; } } private TMP_Text Label { get { if (label == null) label = GetComponentInChildren(true); return label; } } void OnEnable() { // Le guide reprend au repos : le panneau ouvert sera reconnu à la première // image et le fera redescendre, message compris. _current = null; IsShown = false; Place(hiddenY); } void OnDisable() { StopSliding(); } void Update() { if (messages == null || messages.Length == 0) return; Entry active = ActiveEntry(); if (active == _current) return; _current = active; if (active == null) Hide(); else Show(active.message); } // Le premier panneau ouvert de la liste. Deux panneaux ouverts à la fois ne // devraient pas arriver, mais l'ordre de la liste tranche si cela se produit. private Entry ActiveEntry() { foreach (Entry entry in messages) { if (entry != null && entry.panel != null && entry.panel.activeInHierarchy) return entry; } return null; } // Affiche un message et fait descendre le guide. // // L'animation repart toujours du repos, même si le guide est déjà en bas : un // panneau qui s'ouvre se signale, sinon le texte changerait sans que l'œil le // remarque. public void Show(string message) { if (Label != null) Label.text = message; IsShown = true; Slide(hiddenY, shownY); } // Range le guide, sans toucher au message : il ne clignote pas en partant. public void Hide() { if (!IsShown && _sliding == null) { Place(hiddenY); return; } IsShown = false; Slide(Banner.anchoredPosition.y, hiddenY); } private void Slide(float fromY, float toY) { StopSliding(); // Hors du Play, ou sans durée, le déplacement est immédiat : une coroutine // ne tournerait pas dans l'éditeur. if (duration <= 0f || !Application.isPlaying || !isActiveAndEnabled) { Place(toY); return; } Place(fromY); _sliding = StartCoroutine(Sliding(fromY, toY)); } private IEnumerator Sliding(float fromY, float toY) { float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float progress = Mathf.Clamp01(elapsed / duration); Place(Mathf.LerpUnclamped(fromY, toY, ease.Evaluate(progress))); yield return null; } Place(toY); _sliding = null; } private void StopSliding() { if (_sliding == null) return; StopCoroutine(_sliding); _sliding = null; } private void Place(float y) { Banner.anchoredPosition = new Vector2(x, y); } #if UNITY_EDITOR // De quoi régler les deux positions sans lancer le Play. [ContextMenu("Aperçu : position au repos")] private void PreviewHidden() { Place(hiddenY); } [ContextMenu("Aperçu : position affichée")] private void PreviewShown() { Place(shownY); } #endif }