using System.Threading; using Cysharp.Threading.Tasks; using DanroJump.Gameplay; using UnityEngine; using UnityEngine.UIElements; using Zenject; namespace DanroJump.UI.Gameplay { /// /// Отображает runtime-показатели игрового забега в UI Toolkit HUD. /// public sealed class GameplayHudController : MonoBehaviour { [SerializeField] private VisualTreeAsset visualTreeAsset; [SerializeField] private PanelSettings panelSettings; [SerializeField] private int sortingOrder = 10; [SerializeField] private string scoreLabelName = "ScoreValue"; [SerializeField] private string heightLabelName = "HeightValue"; [SerializeField] private string bestHeightLabelName = "BestHeightValue"; [SerializeField] private string livesLabelName = "LivesValue"; [SerializeField] private string coinsLabelName = "CoinsValue"; [SerializeField] private string deathsLabelName = "DeathsValue"; [SerializeField] private string stateLabelName = "StateValue"; private UIDocument document; private GameplaySession session; private Label scoreLabel; private Label heightLabel; private Label bestHeightLabel; private Label livesLabel; private Label coinsLabel; private Label deathsLabel; private Label stateLabel; private CancellationTokenSource refreshCancellation; private bool isSubscribedToSession; /// /// Получает игровую сессию из DI-контейнера сцены. /// [Inject] public void Construct(GameplaySession injectedSession) { SetSession(injectedSession); } private void Start() { EnsureDocument(); BindLabels(); Refresh(); } /// /// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD. /// private void OnEnable() { SubscribeToSession(); if (refreshCancellation == null) { refreshCancellation = new CancellationTokenSource(); RefreshContinuouslyAsync(refreshCancellation.Token).Forget(Debug.LogException); } Refresh(); } /// /// Останавливает обновление HUD и снимает подписку с игровой сессии. /// private void OnDisable() { if (refreshCancellation != null) { refreshCancellation.Cancel(); refreshCancellation.Dispose(); refreshCancellation = null; } UnsubscribeFromSession(); } /// /// Создает или настраивает UIDocument, чтобы HUD мог жить на отдельном GameObject без ручной сборки. /// private void EnsureDocument() { if (document == null && !TryGetComponent(out document)) { document = gameObject.AddComponent(); } if (document == null) { return; } document.visualTreeAsset = visualTreeAsset; document.panelSettings = panelSettings; document.sortingOrder = sortingOrder; } /// /// Находит текстовые поля в UXML по именам, заданным в инспекторе. /// private void BindLabels() { var root = document != null ? document.rootVisualElement : null; if (root == null) { return; } scoreLabel = FindLabel(root, scoreLabelName); heightLabel = FindLabel(root, heightLabelName); bestHeightLabel = FindLabel(root, bestHeightLabelName); livesLabel = FindLabel(root, livesLabelName); coinsLabel = FindLabel(root, coinsLabelName); deathsLabel = FindLabel(root, deathsLabelName); stateLabel = FindLabel(root, stateLabelName); } /// /// Обновляет все значения HUD из текущей игровой сессии. /// private void Refresh() { if (session == null) { SetLabel(stateLabel, "WAITING"); return; } SetLabel(scoreLabel, session.Score.ToString()); SetLabel(heightLabel, $"{session.MaxRunHeight:0} м"); SetLabel(bestHeightLabel, $"{session.RecordHeight:0} м"); SetLabel(livesLabel, session.RemainingLives.ToString()); SetLabel(coinsLabel, session.Coins.ToString()); SetLabel(deathsLabel, session.DeathCount.ToString()); SetLabel(stateLabel, FormatState(session.State)); } /// /// Безопасно обновляет текст label, если элемент найден в UXML. /// private static void SetLabel(Label label, string value) { if (label != null) { label.text = value; } } /// /// Ищет Label по имени, заданному в настройках контроллера. /// private static Label FindLabel(VisualElement root, string labelName) { return root != null && !string.IsNullOrEmpty(labelName) ? root.Q