Danro/Assets/Scripts/Settings/UI/SettingsUIGenerator.cs
Danil Iarosh 2417f58f61
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: Add text input system and balance management with settings improvements
- Добавлена система ввода текста через модальное окно с ячейками для аркадного автомата
- Реализован TextInputModal с поддержкой кастомных алфавитов и навигации
- Добавлен TextInputCell для ввода отдельных символов с циклической навигацией
- Создан TextInputSettingsBinder для интеграции ввода текста с системой настроек
- Реализована компактная сериализация настроек (только нужные поля в JSON)

- Внедрена система баланса игрока (BalanceManager)
- Баланс начисляется при оплате импульсом, списывается при начале/продолжении игры
- Добавлен InactivityManager для выхода в меню при отсутствии активности (5 минут)

- Улучшена система настроек
- Добавлены флаги shownInSettings и exposed для управления видимостью настроек
- Реализована автоматическая регистрация/отписка SettingsButtonBinder при активации/деактивации
- Исправлена загрузка настроек с проверкой типов через Sanitize()
- SettingsApplier упрощен - оставлена только поддержка AudioManager
- Добавлен ValueSelectorRow для выбора целочисленных значений с шагом

- Удалены примеры и документация из production кода (QUICK_SETUP.md, TextInputExample, AdvancedLoggingExample)
- Убран весь отладочный вывод Debug.Log из системы настроек и ввода текста
- Очищены неразрывные пробелы из файлов проекта
- Добавлены префабы UI (InputCell)
2026-02-11 16:27:33 +03:00

336 lines
13 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;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using RR.Common.Settings;
using RR.Common.Settings.UI;
using TMPro;
using UnityEngine;
namespace RR.Common.UI
{
public class SettingsUIGenerator : MonoBehaviour
{
[Header("Source")] [SerializeField] private SettingsDatabase database;
[Header("Layout")] [SerializeField] private Transform contentRoot;
/// <summary>
/// Корневой Transform для контента настроек
/// </summary>
public Transform ContentRoot => contentRoot;
[Header("Grouping")] [SerializeField] private bool groupByGroup = true;
[SerializeField] private bool collapsibleGroups = true;
[SerializeField] private SettingsGroupPanel groupPanelPrefab; // префаб группы (foldout)
[SerializeField] private bool defaultGroupsExpanded = true;
[SerializeField] private bool rememberGroupFoldout = true;
[Tooltip("Если заполнить — группы будут идти в этом порядке, остальные добавятся в конец по алфавиту.")]
[SerializeField]
private List<string> groupOrder = new();
[Header("Sorting")] [SerializeField] private bool sortByNameInsideGroup = true;
[Header("Prefabs (rows)")] [SerializeField]
private SettingUIItem toggleRowPrefab;
[SerializeField] private SettingUIItem sliderRowPrefab;
[SerializeField] private SettingUIItem valueSelectorRowPrefab;
[SerializeField] private SettingUIItem dropdownRowPrefab;
[SerializeField] private SettingUIItem inputRowPrefab;
[SerializeField] private SettingUIItem buttonRowPrefab;
[SerializeField] private SettingUIItem optionPickerRowPrefab;
[Header("Behaviour")] [SerializeField] private bool buildOnEnable = true;
[SerializeField] private bool clearBeforeBuild = true;
[SerializeField] private bool showDescription = true;
private readonly List<SettingUIItem> spawnedItems = new();
private readonly List<SettingsGroupPanel> spawnedGroups = new();
private Coroutine _buildCoroutine;
/// <summary>
/// Событие вызывается когда построение UI завершено
/// </summary>
public event Action OnBuildComplete;
/// <summary>
/// Событие вызывается при сворачивании/разворачивании любой группы настроек
/// </summary>
public event Action OnGroupExpandedChanged;
/// <summary>
/// Первый созданный Selectable элемент
/// </summary>
public UnityEngine.UI.Selectable FirstSelectable { get; private set; }
private void OnEnable()
{
if (buildOnEnable)
{
// Запускаем корутину, которая ждёт SettingsManager
if (_buildCoroutine != null) StopCoroutine(_buildCoroutine);
_buildCoroutine = StartCoroutine(BuildWhenReady());
}
else
{
RefreshValues();
}
}
private void OnDisable()
{
if (_buildCoroutine != null)
{
StopCoroutine(_buildCoroutine);
_buildCoroutine = null;
}
}
private IEnumerator BuildWhenReady()
{
// Ждём пока SettingsManager инициализируется (максимум 2 секунды)
float timeout = 2f;
float elapsed = 0f;
while (!SettingsManager.Instance && elapsed < timeout)
{
elapsed += Time.unscaledDeltaTime;
yield return null;
}
if (!SettingsManager.Instance)
{
Debug.LogError("[SettingsUIGenerator] SettingsManager.Instance not found after waiting. Make sure SettingsManager is in scene.");
yield break;
}
Build();
// Ждём кадр чтобы layout обновился
yield return null;
// Принудительно обновляем layout для корректной навигации
if (contentRoot != null && contentRoot is RectTransform rectTransform)
{
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
}
// Ещё кадр для применения layout
yield return null;
// Находим первый Selectable элемент
if (contentRoot != null)
{
FirstSelectable = contentRoot.GetComponentInChildren<UnityEngine.UI.Selectable>();
}
// Оповещаем о завершении
OnBuildComplete?.Invoke();
_buildCoroutine = null;
}
public void Build()
{
if (!database || !contentRoot)
{
Debug.LogWarning("[SettingsUIGenerator] database/contentRoot not set.");
return;
}
if (!SettingsManager.Instance)
{
Debug.LogWarning("[SettingsUIGenerator] SettingsManager.Instance not found in scene.");
return;
}
if (clearBeforeBuild) Clear();
spawnedItems.Clear();
List<SettingEntry> entries = database.entries
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.key) && e.shownInSettings)
.ToList();
if (!groupByGroup)
{
// Без групп — просто одним списком
if (sortByNameInsideGroup)
entries = entries.OrderBy(e => e.displayName ?? e.key).ToList();
foreach (SettingEntry entry in entries)
SpawnSettingRow(entry, contentRoot);
return;
}
// Группировка по e.group
Dictionary<string, List<SettingEntry>> groups = entries
.GroupBy(e => string.IsNullOrWhiteSpace(e.group) ? "General" : e.group)
.ToDictionary(g => g.Key, g => g.ToList());
// Порядок групп: сначала заданные, потом остальные по алфавиту
List<string> orderedGroupNames = new();
if (groupOrder != null && groupOrder.Count > 0)
{
orderedGroupNames.AddRange(
groupOrder.Where(g => !string.IsNullOrWhiteSpace(g) && groups.ContainsKey(g)));
}
List<string> rest = groups.Keys
.Except(orderedGroupNames)
.OrderBy(x => x)
.ToList();
orderedGroupNames.AddRange(rest);
foreach (string groupName in orderedGroupNames)
{
List<SettingEntry> list = groups[groupName];
if (sortByNameInsideGroup)
list = list.OrderBy(e => e.displayName ?? e.key).ToList();
Transform parentForRows = contentRoot;
// Создаём UI группы (foldout) если включено
if (collapsibleGroups && groupPanelPrefab)
{
SettingsGroupPanel panel = Instantiate(groupPanelPrefab, contentRoot);
panel.name = $"Group_{groupName}";
string prefsKey = $"settings.ui.group.{groupName}";
panel.Init(groupName, prefsKey, defaultGroupsExpanded, rememberGroupFoldout);
// Подписываемся на изменение состояния группы
panel.OnExpandedChanged += OnGroupPanelExpandedChanged;
spawnedGroups.Add(panel);
parentForRows = panel.ContentRoot;
}
else
{
// Если не используем foldout — можно хотя бы заголовок простым текстом
// (не обязательно; если не надо — удали блок)
GameObject headerGO = new GameObject($"Header_{groupName}", typeof(RectTransform),
typeof(TextMeshProUGUI));
headerGO.transform.SetParent(contentRoot, false);
TextMeshProUGUI text = headerGO.GetComponent<TextMeshProUGUI>();
text.text = groupName;
text.fontSize = 28;
text.fontStyle = FontStyles.Bold;
}
foreach (var entry in list)
SpawnSettingRow(entry, parentForRows);
}
}
private void SpawnSettingRow(SettingEntry entry, Transform parent)
{
if (entry == null || parent == null)
{
Debug.LogWarning("[SettingsUIGenerator] Cannot spawn setting row: entry or parent is null");
return;
}
SettingUIItem prefab = PickPrefab(entry);
if (!prefab)
{
Debug.LogWarning(
$"[SettingsUIGenerator] No prefab for '{entry.key}' ({entry.valueType}/{entry.applyTarget})");
return;
}
SettingUIItem item = Instantiate(prefab, parent);
item.name = $"Setting_{entry.key}";
item.Bind(entry, showDescription);
// Если это Int ValueSelector, настраиваем компонент ValueSelectorRow
if (entry.valueType == SettingValueType.Int &&
entry.intDisplayFormat == IntDisplayFormat.ValueSelector)
{
ValueSelectorRow valueSelector = item.GetComponent<ValueSelectorRow>();
if (valueSelector != null)
{
int currentValue = SettingsManager.Instance.GetInt(entry.key);
valueSelector.Setup(entry, currentValue);
}
else
{
Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'");
}
}
spawnedItems.Add(item);
}
private SettingUIItem PickPrefab(SettingEntry e)
{
return e.valueType switch
{
SettingValueType.Bool => toggleRowPrefab,
SettingValueType.Float => sliderRowPrefab,
SettingValueType.Int => e.intDisplayFormat == IntDisplayFormat.ValueSelector
? valueSelectorRowPrefab
: sliderRowPrefab ? sliderRowPrefab : dropdownRowPrefab,
SettingValueType.String => inputRowPrefab,
SettingValueType.Button => buttonRowPrefab,
SettingValueType.OptionPicker => optionPickerRowPrefab ? optionPickerRowPrefab : dropdownRowPrefab,
_ => null
};
}
public void RefreshValues()
{
for (int i = 0; i < spawnedItems.Count; i++)
{
if (spawnedItems[i])
{
spawnedItems[i].RefreshFromSettings();
// Если есть компонент ValueSelectorRow, обновляем и его
ValueSelectorRow valueSelector = spawnedItems[i].GetComponent<ValueSelectorRow>();
if (valueSelector != null && !string.IsNullOrEmpty(valueSelector.Key))
{
int value = SettingsManager.Instance.GetInt(valueSelector.Key);
valueSelector.SetValue(value);
}
}
}
}
private void OnGroupPanelExpandedChanged(SettingsGroupPanel panel, bool expanded)
{
// Уведомляем подписчиков (например SettingsUI) об изменении состояния группы
OnGroupExpandedChanged?.Invoke();
}
public void Clear()
{
// Отписываемся от событий групп
foreach (var group in spawnedGroups)
{
if (group != null)
group.OnExpandedChanged -= OnGroupPanelExpandedChanged;
}
spawnedGroups.Clear();
if (contentRoot != null)
{
for (int i = contentRoot.childCount - 1; i >= 0; i--)
{
Transform child = contentRoot.GetChild(i);
if (child != null)
Destroy(child.gameObject);
}
}
spawnedItems.Clear();
}
}
}