Danro/Assets/Scripts/UI/SettingsUI.cs
Danil Iarosh 4d05d1adbb
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
FEAT: Added new settings system
- Добавил новую систему настроек, возможности:
    - Динамически создает список настроек
    - Сохраняет настройки в JSON-файл
    - Возможность группировать настройки
    - Широкие возможности по кастомизации
    - Применение настроек в реальном времени (частично)
- Добавил префабы для новых элементов настроек
- Изменил поведение игры исходя из нового формата настроек
2026-02-04 14:32:15 +03:00

394 lines
16 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 UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.EventSystems;
using RR.Game2D.Core.Managers;
using RR.Common.Settings;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно настроек игры.
/// Управляет отображением и сохранением настроек через новый SettingsManager.
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class SettingsUI : Modal
{
[Header("Дополнительное управление")]
[SerializeField] private InputActionReference triggerKey;
[SerializeField] private Button saveButton;
[Header("Контент настроек")]
[SerializeField] private Transform content;
[SerializeField] private SettingsUIGenerator settingsGenerator;
[Header("Навигация")]
[Tooltip("Первый элемент для фокуса при открытии. Если не задан - ищется автоматически.")]
[SerializeField] private Selectable firstSelectable;
private SettingsManager settingsManager;
protected override void Awake()
{
base.Awake();
settingsManager = SettingsManager.Instance;
if (triggerKey != null)
triggerKey.action.started += OnTriggerKey;
if (settingsManager == null)
{
GameManager.Logger?.LogError(name, "SettingsManager is null!");
// Не выходим - подпишемся позже когда SettingsManager появится
}
else
{
settingsManager.OnSettingsLoaded += OnSettingsLoaded;
settingsManager.OnSettingsSaved += OnSettingsSaved;
}
if (saveButton)
saveButton.onClick.AddListener(SaveAndClose);
// Подписываемся на события settingsGenerator сразу (даже если неактивен)
SubscribeToGenerator();
}
private bool _subscribedToGenerator = false;
private void SubscribeToGenerator()
{
if (_subscribedToGenerator) return;
// Если не назначен - пробуем найти в детях
if (settingsGenerator == null)
{
settingsGenerator = GetComponentInChildren<SettingsUIGenerator>(true);
}
if (settingsGenerator != null)
{
Debug.Log($"[SettingsUI] Subscribing to settingsGenerator events: {settingsGenerator.name}");
settingsGenerator.OnBuildComplete += OnGeneratorBuildComplete;
settingsGenerator.OnGroupExpandedChanged += OnGroupExpandedChanged;
_subscribedToGenerator = true;
}
else
{
Debug.LogWarning("[SettingsUI] settingsGenerator is NULL - cannot subscribe to events! Make sure to assign it in Inspector or add SettingsUIGenerator as a child.");
}
}
protected override void OnEnable()
{
base.OnEnable();
// Пробуем подписаться снова если не удалось в Awake
SubscribeToGenerator();
}
private void OnDestroy()
{
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded -= OnSettingsLoaded;
settingsManager.OnSettingsSaved -= OnSettingsSaved;
}
if (_subscribedToGenerator && settingsGenerator != null)
{
settingsGenerator.OnBuildComplete -= OnGeneratorBuildComplete;
settingsGenerator.OnGroupExpandedChanged -= OnGroupExpandedChanged;
}
if (triggerKey != null)
triggerKey.action.started -= OnTriggerKey;
if (saveButton)
saveButton.onClick.RemoveListener(SaveAndClose);
}
private void OnGeneratorBuildComplete()
{
// Настраиваем навигацию после построения
SetupExplicitNavigation();
// Выбираем первый элемент
if (settingsGenerator != null && settingsGenerator.FirstSelectable != null)
{
if (EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(settingsGenerator.FirstSelectable.gameObject);
Debug.Log($"[SettingsUI] Selected first element from generator: {settingsGenerator.FirstSelectable.name}");
}
}
}
private void OnGroupExpandedChanged()
{
Debug.Log("[SettingsUI] OnGroupExpandedChanged called - rebuilding navigation");
// При сворачивании/разворачивании группы - перестраиваем навигацию
StartCoroutine(RebuildNavigationOnGroupChange());
}
private System.Collections.IEnumerator RebuildNavigationOnGroupChange()
{
Debug.Log("[SettingsUI] RebuildNavigationOnGroupChange started");
// Запоминаем текущий выбранный объект
GameObject currentSelected = EventSystem.current?.currentSelectedGameObject;
// Ждём кадр чтобы layout обновился
yield return null;
Debug.Log("[SettingsUI] Calling SetupExplicitNavigation after group change");
// Перестраиваем навигацию
SetupExplicitNavigation();
// Проверяем, активен ли текущий выбранный объект
if (currentSelected == null || !currentSelected.activeInHierarchy)
{
Debug.Log("[SettingsUI] Current selected is inactive, reselecting first element");
// Текущий элемент стал неактивен (свёрнут) - выбираем первый активный
var allSelectables = new System.Collections.Generic.List<Selectable>();
Transform searchRoot = settingsGenerator != null && settingsGenerator.ContentRoot != null
? settingsGenerator.ContentRoot
: content;
if (searchRoot != null)
{
CollectActiveSelectables(searchRoot, allSelectables);
if (allSelectables.Count > 0 && EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(allSelectables[0].gameObject);
Debug.Log($"[SettingsUI] Reselected first element after group change: {allSelectables[0].name}");
}
}
}
}
/// <summary>
/// Настраивает явную (Explicit) навигацию между всеми элементами настроек и кнопкой сохранения
/// </summary>
private void SetupExplicitNavigation()
{
// Собираем все активные Selectable элементы
var allSelectables = new System.Collections.Generic.List<Selectable>();
// Ищем в ContentRoot из settingsGenerator или в content
Transform searchRoot = null;
if (settingsGenerator != null && settingsGenerator.ContentRoot != null)
{
searchRoot = settingsGenerator.ContentRoot;
}
else if (content != null)
{
searchRoot = content;
}
if (searchRoot == null)
{
Debug.LogWarning("[SettingsUI] No content root found for navigation setup");
return;
}
Debug.Log($"[SettingsUI] Setting up navigation, searchRoot: {searchRoot.name}");
// Получаем все Selectable в порядке иерархии (только активные)
CollectActiveSelectables(searchRoot, allSelectables);
Debug.Log($"[SettingsUI] Found {allSelectables.Count} selectables");
if (allSelectables.Count == 0)
{
Debug.LogWarning("[SettingsUI] No active selectables found for navigation setup");
return;
}
// Настраиваем вертикальную навигацию для каждого элемента
for (int i = 0; i < allSelectables.Count; i++)
{
var current = allSelectables[i];
var nav = new Navigation();
nav.mode = Navigation.Mode.Explicit;
// Вверх - предыдущий элемент или saveButton (для первого)
if (i == 0)
{
nav.selectOnUp = saveButton;
}
else
{
nav.selectOnUp = allSelectables[i - 1];
}
// Вниз - следующий элемент или saveButton (для последнего)
if (i == allSelectables.Count - 1)
{
nav.selectOnDown = saveButton;
}
else
{
nav.selectOnDown = allSelectables[i + 1];
}
// Горизонтальная навигация - оставляем null (не переходим влево/вправо на другие элементы)
nav.selectOnLeft = null;
nav.selectOnRight = null;
current.navigation = nav;
Debug.Log($"[SettingsUI] Set Explicit nav for: {current.name}, mode: {current.navigation.mode}");
}
// Настраиваем кнопку с<><D181>хранения
if (saveButton != null)
{
var saveNav = new Navigation();
saveNav.mode = Navigation.Mode.Explicit;
saveNav.selectOnUp = allSelectables[allSelectables.Count - 1]; // Последний элемент
saveNav.selectOnDown = allSelectables[0]; // Первый элемент (циклическая)
saveNav.selectOnLeft = null;
saveNav.selectOnRight = null;
saveButton.navigation = saveNav;
}
Debug.Log($"[SettingsUI] Explicit navigation setup complete. Elements: {allSelectables.Count}");
}
/// <summary>
/// Рекурсивно собирает все активные Selectable элементы в порядке иерархии
/// </summary>
private void CollectActiveSelectables(Transform parent, System.Collections.Generic.List<Selectable> result)
{
for (int i = 0; i < parent.childCount; i++)
{
var child = parent.GetChild(i);
// Пропускаем неактивные объекты
if (!child.gameObject.activeInHierarchy) continue;
// Проверяем, есть ли Selectable на этом объекте
var selectable = child.GetComponent<Selectable>();
if (selectable != null && selectable.interactable)
{
result.Add(selectable);
}
// Рекурсивно ищем в детях
CollectActiveSelectables(child, result);
}
}
private void OnTriggerKey(InputAction.CallbackContext ctx)
{
if (IsOpen)
{
SaveAndClose();
}
else
{
Open();
}
}
/// <summary>
/// Переопределяем Open для обновления настроек при открытии.
/// </summary>
public override void Open()
{
RefreshSettings();
base.Open();
// Выбираем первый элемент для навигации
SelectFirstElement();
}
private void SelectFirstElement()
{
// Запускаем корутину с задержкой, чтобы UI успел построиться
StartCoroutine(SelectFirstElementDelayed());
}
private System.Collections.IEnumerator SelectFirstElementDelayed()
{
// Ждём несколько кадров чтобы SettingsUIGenerator успел построить элементы
yield return null;
yield return null;
yield return new WaitForSecondsRealtime(0.1f);
// Настраиваем явную навигацию
SetupExplicitNavigation();
Selectable target = firstSelectable;
// Если не задан явно - ищем первый интерактивный элемент в content
if (target == null && content != null)
{
target = content.GetComponentInChildren<Selectable>();
}
// Если всё ещё не нашли - ищем в settingsGenerator
if (target == null && settingsGenerator != null)
{
target = settingsGenerator.GetComponentInChildren<Selectable>();
}
// Последняя попытка - ищем во всём объекте
if (target == null)
{
target = GetComponentInChildren<Selectable>();
}
if (target != null && EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(target.gameObject);
Debug.Log($"[SettingsUI] Selected first element: {target.name}");
}
else
{
Debug.LogWarning("[SettingsUI] Could not find any selectable element for focus");
}
}
private void RefreshSettings()
{
// Если используется новый генератор
if (settingsGenerator != null)
{
settingsGenerator.RefreshValues();
}
}
private void Save()
{
if (settingsManager != null)
{
settingsManager.SaveSettings();
}
}
public void SaveAndClose()
{
Save();
Close();
}
private void OnSettingsLoaded()
{
RefreshSettings();
}
private void OnSettingsSaved()
{
GameManager.Logger?.Log(name, "Saved settings");
}
}
}