Подробные изменения: - Расширен основной gameplay loop: джетпак, новые платформы, оборудование, враги, прогрессия сложности и ограничения взаимодействий игрока. - Добавлены Bootstrap-сервисы запуска приложения, защита, фокус окна, управление курсором и настройки сервисов через DI. - Добавлена COM-интеграция с технической схемой Input System, обработкой кнопок/ADC, подсветкой и консольной диагностикой. - Добавлен отдельный debug/logging модуль с каналами, обработчиками консоли и файловым выводом. - Добавлены asmdef, link.xml, EditMode/PlayMode тесты и обновлена дорожная карта проекта. Проверки: - dotnet build .\\DanroJump.Runtime.csproj --no-restore - dotnet build .\\DanroJump.Tests.EditMode.csproj --no-restore - dotnet build .\\DanroJump.Tests.PlayMode.csproj --no-restore
234 lines
7.8 KiB
C#
234 lines
7.8 KiB
C#
using System.Threading;
|
||
using Cysharp.Threading.Tasks;
|
||
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 CancellationTokenSource refreshCancellation;
|
||
private bool isSubscribedToSession;
|
||
|
||
/// <summary>
|
||
/// Получает игровую сессию из DI-контейнера сцены.
|
||
/// </summary>
|
||
[Inject]
|
||
public void Construct(GameplaySession injectedSession)
|
||
{
|
||
SetSession(injectedSession);
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
EnsureDocument();
|
||
BindLabels();
|
||
Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD.
|
||
/// </summary>
|
||
private void OnEnable()
|
||
{
|
||
SubscribeToSession();
|
||
|
||
if (refreshCancellation == null)
|
||
{
|
||
refreshCancellation = new CancellationTokenSource();
|
||
RefreshContinuouslyAsync(refreshCancellation.Token).Forget(Debug.LogException);
|
||
}
|
||
|
||
Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Останавливает обновление HUD и снимает подписку с игровой сессии.
|
||
/// </summary>
|
||
private void OnDisable()
|
||
{
|
||
if (refreshCancellation != null)
|
||
{
|
||
refreshCancellation.Cancel();
|
||
refreshCancellation.Dispose();
|
||
refreshCancellation = null;
|
||
}
|
||
|
||
UnsubscribeFromSession();
|
||
}
|
||
|
||
/// <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>
|
||
/// Ищет 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 async UniTask RefreshContinuouslyAsync(CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
while (enabled && !cancellationToken.IsCancellationRequested)
|
||
{
|
||
Refresh();
|
||
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
|
||
}
|
||
}
|
||
catch (System.OperationCanceledException)
|
||
{
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Преобразует состояние сессии в короткий текст для HUD.
|
||
/// </summary>
|
||
private static string FormatState(GameplaySessionState state)
|
||
{
|
||
return state switch
|
||
{
|
||
GameplaySessionState.Running => "RUN",
|
||
GameplaySessionState.Respawning => "RESPAWN",
|
||
GameplaySessionState.GameOver => "GAME OVER",
|
||
_ => "WAITING",
|
||
};
|
||
}
|
||
|
||
private void SetSession(GameplaySession injectedSession)
|
||
{
|
||
if (session == injectedSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnsubscribeFromSession();
|
||
session = injectedSession;
|
||
SubscribeToSession();
|
||
Refresh();
|
||
}
|
||
|
||
private void SubscribeToSession()
|
||
{
|
||
if (!isActiveAndEnabled || session == null || isSubscribedToSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
session.Changed += Refresh;
|
||
isSubscribedToSession = true;
|
||
}
|
||
|
||
private void UnsubscribeFromSession()
|
||
{
|
||
if (session == null || !isSubscribedToSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
session.Changed -= Refresh;
|
||
isSubscribedToSession = false;
|
||
}
|
||
}
|
||
}
|