Danro-Jump/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs
Danil Iarosh 1f6884c5ac Build gameplay systems and service settings
Подробные изменения:
- Добавлены и доработаны модальные окна главного меню, сервисные настройки, шаблоны UI Toolkit и единый scene-flow сервис для переходов между сценами.
- Расширена gameplay-архитектура: сессия забега, HUD, спавн контента платформ, collectible-предметы, оборудование, хрупкие и движущиеся платформы.
- Добавлена база противников: типы спавна, патрулирование, процедурный спавнер, правила размещения через ScriptableObject-сложности и убийство врагов прыжком сверху.
- Исправлены смерть и respawn игрока, финальный GameOver, отскоки от врагов, пружин и boost-предметов, а также проход сквозь врагов при усиленном подъеме.
- Проведен рефакторинг взаимодействия систем: общий landing context, единые правила player-contact, NonLazy gameplay session, fail-fast installer и централизованный выбор точек спавна.
- Обновлены сцены, prefab-ассеты, difficulty stages, ресурсы главного меню, настройки проекта, roadmap и аппаратная COM-интеграция.
- Добавлены ассеты летучей мыши, врагов, collectibles, TextMesh Pro, ProjectContext и gameplay UI.

Проверки:
- dotnet build .\\Assembly-CSharp.csproj --no-restore
- Unity validation ключевых gameplay/UI/scene-flow скриптов
- Unity Console после force refresh без ошибок
2026-05-27 16:34:51 +03:00

219 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using DanroJump.Gameplay;
using UnityEngine;
using UnityEngine.UIElements;
using Zenject;
namespace DanroJump.UI.Gameplay
{
/// <summary>
/// Отображает runtime-показатели игрового забега в UI Toolkit HUD.
/// </summary>
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 Coroutine refreshRoutine;
/// <summary>
/// Получает игровую сессию из DI-контейнера сцены.
/// </summary>
[Inject]
public void Construct(GameplaySession injectedSession)
{
session = injectedSession;
}
private void Start()
{
EnsureDocument();
BindLabels();
ResolveSessionFromSceneContext();
Refresh();
}
/// <summary>
/// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD.
/// </summary>
private void OnEnable()
{
if (session != null)
{
session.Changed += Refresh;
}
refreshRoutine ??= StartCoroutine(RefreshContinuously());
Refresh();
}
/// <summary>
/// Останавливает обновление HUD и снимает подписку с игровой сессии.
/// </summary>
private void OnDisable()
{
if (refreshRoutine != null)
{
StopCoroutine(refreshRoutine);
refreshRoutine = null;
}
if (session != null)
{
session.Changed -= Refresh;
}
}
/// <summary>
/// Создает или настраивает UIDocument, чтобы HUD мог жить на отдельном GameObject без ручной сборки.
/// </summary>
private void EnsureDocument()
{
if (document == null && !TryGetComponent(out document))
{
document = gameObject.AddComponent<UIDocument>();
}
if (document == null)
{
return;
}
document.visualTreeAsset = visualTreeAsset;
document.panelSettings = panelSettings;
document.sortingOrder = sortingOrder;
}
/// <summary>
/// Находит текстовые поля в UXML по именам, заданным в инспекторе.
/// </summary>
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);
}
/// <summary>
/// Обновляет все значения HUD из текущей игровой сессии.
/// </summary>
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));
}
/// <summary>
/// Безопасно обновляет текст label, если элемент найден в UXML.
/// </summary>
private static void SetLabel(Label label, string value)
{
if (label != null)
{
label.text = value;
}
}
/// <summary>
/// Аварийно получает сессию из SceneContext, если HUD включился раньше DI-инъекции.
/// </summary>
private void ResolveSessionFromSceneContext()
{
if (session != null)
{
return;
}
var sceneContext = FindFirstObjectByType<SceneContext>();
if (sceneContext == null || sceneContext.Container == null)
{
return;
}
try
{
session = sceneContext.Container.Resolve<GameplaySession>();
session.Changed += Refresh;
}
catch (ZenjectException)
{
Debug.LogWarning($"{nameof(GameplayHudController)} could not resolve {nameof(GameplaySession)} from SceneContext yet.", this);
}
}
/// <summary>
/// Ищет Label по имени, заданному в настройках контроллера.
/// </summary>
private static Label FindLabel(VisualElement root, string labelName)
{
return root != null && !string.IsNullOrEmpty(labelName)
? root.Q<Label>(labelName)
: null;
}
/// <summary>
/// Подстраховывает HUD от событий, которые могли прийти до подписки.
/// </summary>
private IEnumerator RefreshContinuously()
{
while (enabled)
{
Refresh();
yield return null;
}
}
/// <summary>
/// Преобразует состояние сессии в короткий текст для HUD.
/// </summary>
private static string FormatState(GameplaySessionState state)
{
return state switch
{
GameplaySessionState.Running => "RUN",
GameplaySessionState.Respawning => "RESPAWN",
GameplaySessionState.GameOver => "GAME OVER",
_ => "WAITING",
};
}
}
}