Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавил новую систему настроек, возможности:
- Динамически создает список настроек
- Сохраняет настройки в JSON-файл
- Возможность группировать настройки
- Широкие возможности по кастомизации
- Применение настроек в реальном времени (частично)
- Добавил префабы для новых элементов настроек
- Изменил поведение игры исходя из нового формата настроек
394 lines
16 KiB
C#
394 lines
16 KiB
C#
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");
|
||
}
|
||
}
|
||
}
|
||
|