sea-battle/Assets/Scripts/Settings/SettingsScreen.cs

684 lines
30 KiB
C#
Raw Permalink 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 System.Collections.Generic;
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class SettingsScreen : MonoBehaviour
{
[Header("Панель с опциями")]
[SerializeField] RectTransform OptionsRect;
[Header("Панель с вариантами опций")]
[SerializeField] RectTransform ValuesRect;
[Header("Панель с статистикой")]
[SerializeField] CountersView StatisticRect;
[Header("Указатель опций")]
[SerializeField] RectTransform PointerRect;
[Header("Префаб текста настройки")]
[SerializeField] GameObject SettingPrefab;
[Header("Префаб текста опции")]
[SerializeField] GameObject OptionPrefab;
[Header("Префаб прогресс-бара (для Panel/Action)")]
[SerializeField] GameObject ProgressBarPrefab;
[Header("Префаб слайдера (для Svitcher)")]
[SerializeField] GameObject SwitcherSliderPrefab;
[Header("Настройки прокрутки")]
[SerializeField] int maxVisibleSettings = 5; // максимально видимых элементов
[SerializeField] float scrollSpeed = 10f; // скорость сглаживания прокрутки
// Скроллбар UI
[Header("UI Scrollbar")]
[SerializeField] Scrollbar Scrollbar;
List<GameObject> settingsLabels;
List<GameObject> optionsLabels;
Dictionary<int, Image> progressBars; // индекс настройки -> прогресс-бар
Dictionary<int, TextMeshProUGUI> progressTexts; // индекс настройки -> текст процентов
Dictionary<int, Slider> switcherSliders; // индекс настройки -> слайдер для Svitcher
substate currentState;
List<SettingOption> gameSettings => SettingsManager.instance.gameSettings.settings;
int settingsAmount => SettingsManager.instance.gameSettings.settings.Count;
List<string> chosenSettingOptions => SettingsManager.instance.gameSettings.settings[currentSettingIndex].options;
int chosenSettingOptionsAmount => chosenSettingOptions.Count;
public int currentSettingIndex = 0;
int currentOptionIndex = 0;
[Header("Позиционирование настроек")]
[Tooltip("Авто: вычислить starty/offsetY из высоты OptionsRect при открытии")]
[SerializeField] bool autoLayout = true;
[Tooltip("Отступ первого элемента от левого края (px)")]
[SerializeField] float startx = 50;
[Tooltip("Отступ первого элемента от верхнего края (px, отрицательный — вниз)")]
[SerializeField] float starty = -50;
[Tooltip("Горизонтальный сдвиг каждого следующего элемента (px, обычно 0)")]
[SerializeField] float offsetx = 0;
[Tooltip("Вертикальный шаг между элементами (px)")]
[SerializeField] float offsety = 70;
[Tooltip("Сдвиг значения/слайдера/прогресс-бара вправо от правого края лейбла (px)")]
[SerializeField] float valueOffsetX = 0;
[Tooltip("На сколько px указатель левее левого края лейбла (startx)")]
[SerializeField] float pointerOffsetX = 35f;
Coroutine chooseOption;
[SerializeField] float optionsPadding = 20;
// Переменные для прокрутки
int scrollOffset = 0; // текущее смещение (индекс верхнего видимого элемента)
float targetScrollY = 0f; // целевая позиция прокрутки
float currentScrollY = 0f; // текущая позиция прокрутки (интерполируемая)
// Вспомогательные переменные для скроллбара
int maxScrollSteps = 0; // settingsAmount - maxVisibleSettings
public void Open()
{
optionsLabels = new List<GameObject>();
settingsLabels = new List<GameObject>();
progressBars = new Dictionary<int, Image>();
progressTexts = new Dictionary<int, TextMeshProUGUI>();
switcherSliders = new Dictionary<int, Slider>();
PointerRect.gameObject.SetActive(true);
currentState = substate.SettingChoosing;
currentSettingIndex = 0;
// обнуляем прокрутку
scrollOffset = 0;
targetScrollY = 0f;
currentScrollY = 0f;
// Авто-расчёт шага и начального Y из реального размера контейнера
if (autoLayout && OptionsRect != null)
{
// Canvas.ForceUpdateCanvases() гарантирует актуальный rect.height
Canvas.ForceUpdateCanvases();
float h = OptionsRect.rect.height;
if (h > 1f)
{
float step = h / maxVisibleSettings;
offsety = step;
starty = -(step * 0.5f); // первый элемент по центру первого слота
}
}
CreateSettingsLabels();
StatisticRect.Initialize();
// Подписка на скроллбар
if (Scrollbar != null)
{
// вычислим параметры
maxScrollSteps = Mathf.Max(0, settingsAmount - maxVisibleSettings);
Scrollbar.gameObject.SetActive(maxScrollSteps > 0);
if (maxScrollSteps > 0)
{
// размер «видимого окна» на скроллбаре
Scrollbar.size = (float)maxVisibleSettings / (float)settingsAmount;
float value = (maxScrollSteps > 0) ? 1f - (float)scrollOffset / (float)maxScrollSteps : 1f;
Scrollbar.SetValueWithoutNotify(value);
Scrollbar.onValueChanged.AddListener(OnScrollbarChanged);
}
else
{
Scrollbar.onValueChanged.RemoveListener(OnScrollbarChanged);
}
}
}
public void Close()
{
// отписываемся от событий скроллбара
if (Scrollbar != null)
{
Scrollbar.onValueChanged.RemoveListener(OnScrollbarChanged);
}
ClearOptionsPanel();
ClearSettingsPanel();
if (chooseOption != null) StopCoroutine(chooseOption);
}
/// <summary>True, когда открыта нижняя панель выбора опции (второй шаг Accept)</summary>
public bool IsChoosingOption => currentState == substate.OptionChoosing;
/// <summary>Отменяет выбор опции без сохранения — возвращает в режим навигации по настройкам</summary>
public void CancelOption()
{
if (currentState != substate.OptionChoosing) return;
if (chooseOption != null) StopCoroutine(chooseOption);
ClearOptionsPanel();
PointerRect.gameObject.SetActive(true);
currentState = substate.SettingChoosing;
}
public void Switch(int direction)
{
switch (currentState)
{
case substate.SettingChoosing:
currentSettingIndex = (currentSettingIndex + settingsAmount + direction) % settingsAmount;
UpdateScrollPosition();
// не устанавливаем pointer напрямую на целевой, пусть он позиционируется относительно currentScrollY
UpdatePointerPosition();
break;
case substate.OptionChoosing:
currentOptionIndex = (currentOptionIndex + chosenSettingOptionsAmount + direction) % chosenSettingOptionsAmount;//ТУТ НАДО КОЛИЧЕСТВО ОПЦИЙ НАСТРОЙКИ ПОЛУЧИТЬ
break;
}
//SoundManager.instance.playSwitch();
}
//менять состояние, сохранять данные
public void Accept()
{
if (currentState == substate.SettingChoosing)
{
createOptionsLabels(gameSettings[currentSettingIndex]);
currentState = substate.OptionChoosing;
PointerRect.gameObject.SetActive(false);
if (chooseOption != null) StopCoroutine(chooseOption);
chooseOption = StartCoroutine(DisplayChosenOption());
}
else if (currentState == substate.OptionChoosing)
{
SettingsManager.instance.ChangeSetting(settingName: gameSettings[currentSettingIndex].settingName,
newIndex: currentOptionIndex);
PointerRect.gameObject.SetActive(true);
ClearOptionsPanel();
//Обновляем для учета изменений
ClearSettingsPanel();
CreateSettingsLabels();
if (chooseOption != null) StopCoroutine(chooseOption);
currentState = substate.SettingChoosing;
}
//SoundManager.instance.playChoose();
}
public void AcceptView()
{
StatisticRect.View();
}
/// <summary>
/// Можно ли открыть панель или выполнить экшн для текущей настройки
/// </summary>
public bool CanOpenPanel()
{
if (currentState != substate.SettingChoosing) return false;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return false;
SettingType type = gameSettings[currentSettingIndex].type;
return type == SettingType.Panel || type == SettingType.Action;
}
/// <summary>
/// Возвращает ID панели для текущей выбранной настройки (-1 если не панель)
/// </summary>
public int GetCurrentPanelId()
{
if (currentState != substate.SettingChoosing) return -1;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return -1;
return gameSettings[currentSettingIndex].panelId;
}
/// <summary>
/// Возвращает тип текущей выбранной настройки
/// </summary>
public SettingType GetCurrentSettingType()
{
if (currentState != substate.SettingChoosing) return SettingType.Normal;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return SettingType.Normal;
return gameSettings[currentSettingIndex].type;
}
/// <summary>
/// Обновляет заполнение прогресс-бара для текущей настройки (если она типа Panel/Action)
/// </summary>
public void UpdateProgressBar(float fillAmount)
{
fillAmount = Mathf.Clamp01(fillAmount);
if (progressBars.TryGetValue(currentSettingIndex, out Image progressBar))
{
progressBar.fillAmount = fillAmount;
}
if (progressTexts.TryGetValue(currentSettingIndex, out TextMeshProUGUI progressText))
{
// Показываем текст только если прогресс-бар заполняется
if (fillAmount > 0f)
{
progressText.gameObject.SetActive(true);
int percentage = Mathf.RoundToInt(fillAmount * 100f);
progressText.text = $"{percentage}%";
}
else
{
progressText.gameObject.SetActive(false);
}
}
}
//Шерстим все существующие настройки
//Создаем для каждой настройки текстовое поле
private void CreateSettingsLabels()
{
// ...existing code...
settingsLabels = new List<GameObject>();
for (int i = 0; i < SettingsManager.instance.gameSettings.settings.Count; i++)
{
createLabelFromSettingOption(SettingsManager.instance.gameSettings.settings[i], i);
}
// Обновляем прокрутку и позиции после создания
UpdateScrollPosition();
currentScrollY = targetScrollY;
UpdateSettingsPositions();
//Процедура которая собственно создает лейблы
void createLabelFromSettingOption(SettingOption option, int index)
{
GameObject label = Instantiate(SettingPrefab, OptionsRect);
label.name = option.settingName + " Setting";
settingsLabels.Add(label);
RectTransform rt = label.GetComponent<RectTransform>();
// Добавляем CanvasGroup для управления видимостью
CanvasGroup canvasGroup = rt.GetComponent<CanvasGroup>();
if (canvasGroup == null) canvasGroup = rt.gameObject.AddComponent<CanvasGroup>();
// Изначальная позиция будет обновлена в UpdateSettingsPositions()
rt.anchoredPosition = new Vector2(startx + offsetx * index, starty - offsety * index);
//Загоняем название настройки
TextMeshProUGUI text = rt.gameObject.GetComponent<TextMeshProUGUI>();
text.text = option.settingName;
// реальная ширина лейбла (sizeDelta некорректен при стретч-якорях)
float labelW = rt.rect.width > 1f ? rt.rect.width : rt.sizeDelta.x;
// Для Panel/Action создаём прогресс-бар, для Svitcher — слайдер, для Normal — текст опц
if (option.type == SettingType.Panel || option.type == SettingType.Action)
{
if (ProgressBarPrefab != null)
{
GameObject progressBarObj = Instantiate(ProgressBarPrefab, rt);
progressBarObj.name = option.settingName + " ProgressBar";
RectTransform progressRT = progressBarObj.GetComponent<RectTransform>();
progressRT.anchoredPosition = new Vector2(labelW + valueOffsetX, 0);
Image progressImage = progressBarObj.GetComponentInChildren<Image>();
if (progressImage != null)
{
progressImage.fillAmount = 0f;
progressBars[index] = progressImage;
}
// Находим TextMeshProUGUI для отображения процентов
TextMeshProUGUI progressText = progressBarObj.GetComponentInChildren<TextMeshProUGUI>();
if (progressText != null)
{
progressText.text = "0%";
progressText.gameObject.SetActive(false); // скрываем до начала заполнения
progressTexts[index] = progressText;
}
}
}
else if (option.type == SettingType.Svitcher)
{
// Создаём слайдер для переключателя
if (SwitcherSliderPrefab != null)
{
GameObject sliderObj = Instantiate(SwitcherSliderPrefab, rt);
sliderObj.name = option.settingName + " Slider";
RectTransform sliderRT = sliderObj.GetComponent<RectTransform>();
sliderRT.anchoredPosition = new Vector2(labelW + valueOffsetX, 0);
Slider slider = sliderObj.GetComponent<Slider>();
if (slider != null)
{
// Устанавливаем начальное значение: инвертируем для корректного отображения
// 0 (выкл визуально) = индекс 1 ("Выкл"), 1 (вкл визуально) = индекс 0 ("Вкл")
slider.minValue = 0;
slider.maxValue = 1;
// ВАЖНО: используем SetValueWithoutNotify, чтобы не вызывать событие при инициализации
int initialValue = (option.chosenIndex == 0) ? 1 : 0;
slider.SetValueWithoutNotify(initialValue);
// Сохраняем ссылку на слайдер
switcherSliders[index] = slider;
// НЕ подписываемся на onValueChanged - сохранение происходит через callback в ToggleSwitcher
}
}
}
else
{
// Обычная настройка — показываем текущую опцию
GameObject label2 = Instantiate(OptionPrefab, rt);
label2.name = option.settingName + " Value";
RectTransform option_rt = label2.GetComponent<RectTransform>();
option_rt.anchoredPosition = new Vector2(labelW + valueOffsetX, 0);
text = option_rt.gameObject.GetComponent<TextMeshProUGUI>();
text.text = option.ChosenOption;
}
}
}
// Вычисляем и задаём смещение прокрутки и целевую позицию
private void UpdateScrollPosition()
{
if (settingsAmount <= maxVisibleSettings)
{
scrollOffset = 0;
targetScrollY = 0f;
if (Scrollbar != null) Scrollbar.gameObject.SetActive(false);
return;
}
if (currentSettingIndex >= scrollOffset + maxVisibleSettings)
{
scrollOffset = currentSettingIndex - maxVisibleSettings + 1;
}
else if (currentSettingIndex < scrollOffset)
{
scrollOffset = currentSettingIndex;
}
targetScrollY = scrollOffset * offsety;
// обновим скроллбар без вызова слушателя
if (Scrollbar != null && maxScrollSteps > 0)
{
float value = 1f - (float)scrollOffset / (float)maxScrollSteps;
Scrollbar.SetValueWithoutNotify(value);
}
}
// Обновляем позицию указателя: ставим его слева от текущего лейбла настройки
private void UpdatePointerPosition()
{
if (settingsLabels == null || currentSettingIndex < 0 || currentSettingIndex >= settingsLabels.Count)
return;
RectTransform labelRT = settingsLabels[currentSettingIndex].GetComponent<RectTransform>();
if (labelRT == null) return;
RectTransform pointerParent = PointerRect.parent as RectTransform;
if (pointerParent == null) return;
// Берём мировые координаты лейбла и конвертируем в локальное пространство родителя указателя
// Это работает вне зависимости от того, в каком контейнере находится PointerRect
Vector2 localPos = pointerParent.InverseTransformPoint(labelRT.position);
// Ставим указатель левее лейбла на pointerOffsetX пикселей
PointerRect.anchoredPosition = new Vector2(localPos.x - pointerOffsetX, localPos.y);
}
private void UpdateSettingsPositions()
{
if (settingsLabels == null) return;
// Плавающее смещение в строках
float scrollFloat = (offsety != 0) ? currentScrollY / offsety : 0f;
for (int i = 0; i < settingsLabels.Count; i++)
{
RectTransform rt = settingsLabels[i].GetComponent<RectTransform>();
// Вычисляем видимую позицию с учётом плавающего смещения
float visiblePosition = i - scrollFloat;
float targetY = starty - visiblePosition * offsety;
rt.anchoredPosition = new Vector2(startx + offsetx * i, targetY);
// Можно также добавить прозрачность для невидимых элементов
CanvasGroup canvasGroup = rt.GetComponent<CanvasGroup>();
if (canvasGroup == null) canvasGroup = rt.gameObject.AddComponent<CanvasGroup>();
// Элементы вне видимой области делаем невидимыми
if (i < scrollOffset || i >= scrollOffset + maxVisibleSettings)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
else
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
}
// Обновляем позицию указателя
UpdatePointerPosition();
}
// обрабтчик изменения значения Scrollbar (0..1). Значение 1 = верх (scrollOffset=0), 0 = низ (максимальное смещение)
private void OnScrollbarChanged(float value)
{
if (settingsAmount <= maxVisibleSettings) return;
maxScrollSteps = Mathf.Max(0, settingsAmount - maxVisibleSettings);
if (maxScrollSteps == 0) return;
// инвертируем значение: 1 -> 0, 0 -> maxScrollSteps
float t = 1f - value;
float desiredOffsetF = t * maxScrollSteps;
// Применяем дробную цель к targetScrollY (плавнее)
targetScrollY = desiredOffsetF * offsety;
// и обновляем целевой scrollOffset целым числом для маски видимости
scrollOffset = Mathf.Clamp(Mathf.RoundToInt(desiredOffsetF), 0, maxScrollSteps);
}
void Update()
{
// Плавная прокрутка
if (currentState == substate.SettingChoosing && Mathf.Abs(currentScrollY - targetScrollY) > 0.1f)
{
currentScrollY = Mathf.Lerp(currentScrollY, targetScrollY, Time.deltaTime * scrollSpeed);
UpdateSettingsPositions();
// при программной прокрутке обновляем скроллбар, чтобы отражать текущее положение
if (Scrollbar != null && maxScrollSteps > 0)
{
float scrollFloat = currentScrollY / offsety; // может быть дробным
float value = 1f - (scrollFloat / (float)maxScrollSteps);
// защита от NaN/Inf
if (float.IsNaN(value) || float.IsInfinity(value)) value = 1f;
Scrollbar.SetValueWithoutNotify(Mathf.Clamp01(value));
}
}
else if (currentScrollY != targetScrollY)
{
currentScrollY = targetScrollY;
UpdateSettingsPositions();
}
}
// Опции настроек
void createOptionsLabels(SettingOption option)
{
if (optionsLabels == null) optionsLabels = new List<GameObject>();
currentOptionIndex = 0;
// Убеждаемся что ValuesRect имеет актуальные размеры
Canvas.ForceUpdateCanvases();
// rect.rect.width/height — реальный размер (sizeDelta неправильный при стретч-якорях)
float containerW = ValuesRect.rect.width > 1f ? ValuesRect.rect.width : Mathf.Abs(ValuesRect.sizeDelta.x);
float containerH = ValuesRect.rect.height > 1f ? ValuesRect.rect.height : Mathf.Abs(ValuesRect.sizeDelta.y);
float labelWidth = (containerW - optionsPadding * 2) / option.options.Count;
float labelHeight = containerH - optionsPadding * 2;
foreach (string optionName in option.options)
{
GameObject label = Instantiate(OptionPrefab, ValuesRect);
optionsLabels.Add(label);
RectTransform rt = label.GetComponent<RectTransform>();
// Явно фиксируем якорь в левый верхний угол контейнера.
// Без этого Unity использует якорь из префаба (обычно center = 0.5,0.5),
// из-за чего X=0 оказывается в середине панели, а не у левого края.
rt.anchorMin = new Vector2(0f, 1f);
rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(0f, 1f);
rt.sizeDelta = new Vector2(labelWidth, labelHeight);
// X: отступ + шаг * индекс
// Y: отрицательный — в Unity UI движение вниз от верхнего края = отрицательный Y
rt.anchoredPosition = new Vector2(
optionsPadding + labelWidth * currentOptionIndex,
-optionsPadding
);
TextMeshProUGUI text = label.GetComponent<TextMeshProUGUI>();
text.text = optionName;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.gray;
currentOptionIndex++;
}
currentOptionIndex = option.chosenIndex;
}
void ClearSettingsPanel()
{
if (settingsLabels == null) return;
foreach (GameObject label in settingsLabels)
{
if (label != null) Object.Destroy(label);
}
settingsLabels.Clear();
}
void ClearOptionsPanel()
{
if (optionsLabels == null) return;
foreach (GameObject label in optionsLabels)
{
if (label != null) Object.Destroy(label);
}
optionsLabels.Clear();
}
IEnumerator DisplayChosenOption()
{
Color normalColor = Color.gray;
Color highlightedColor = Color.white;
int normalSize = 26;
int chosenSize = 40;
List<TextMeshProUGUI> options = new List<TextMeshProUGUI>();
if (optionsLabels != null)
{
foreach (GameObject label in optionsLabels)
{
options.Add(label.GetComponent<TextMeshProUGUI>());
}
}
while (true)
{
for (int i = 0; i < options.Count; i++)
{
if (i == currentOptionIndex)
{
options[i].color = Vector4.MoveTowards(options[i].color, highlightedColor, Time.deltaTime * 5);
options[i].fontSize = Mathf.MoveTowards(options[i].fontSize, chosenSize, Time.deltaTime * 140);
}
else
{
options[i].color = Vector4.MoveTowards(options[i].color, normalColor, Time.deltaTime * 5);
options[i].fontSize = Mathf.MoveTowards(options[i].fontSize, normalSize, Time.deltaTime * 140);
}
}
yield return null;
}
}
/// <summary>
/// Переключает слайдер текущей выбранной настройки (для типа Svitcher)
/// Вызывается из SettingsInput при нажатии кнопки
/// </summary>
public void ToggleSwitcher()
{
if (currentState != substate.SettingChoosing || !switcherSliders.TryGetValue(currentSettingIndex, out Slider slider)) return;
// ВАЖНО: Округляем текущее значение до 0 или 1, чтобы избежать промежуточных значений при быстрых нажатиях
int currentValue = Mathf.RoundToInt(slider.value);
float newValue = currentValue == 0 ? 1f : 0f;
string settingName = gameSettings[currentSettingIndex].settingName;
// Сохраняем сразу — не ждём конца анимации.
// ВАЖНО: slider.value инвертирован относительно chosenIndex (см. инициализацию: chosenIndex==0 → slider=1)
// newValue==1 → визуально "вкл" → chosenIndex=0; newValue==0 → визуально "выкл" → chosenIndex=1
int newChosenIndex = Mathf.RoundToInt(newValue) == 1 ? 0 : 1;
SettingsManager.instance.ChangeSetting(settingName, newChosenIndex);
// Анимируем только визуально, без callback сохранения
SmoothSlider smoothSlider = slider.GetComponent<SmoothSlider>();
if (smoothSlider != null)
smoothSlider.AnimateTo(newValue, null);
else
slider.value = newValue;
}
/// <summary>
/// Напрямую меняет опцию Normal-настройки на +1/-1 и сохраняет изменение.
/// Используется при управлении Left/Right в режиме SettingChoosing.
/// </summary>
public void SwitchOption(int direction)
{
if (currentState != substate.SettingChoosing) return;
if (currentSettingIndex < 0 || currentSettingIndex >= gameSettings.Count) return;
SettingOption option = gameSettings[currentSettingIndex];
if (option.type != SettingType.Normal) return;
if (option.options == null || option.options.Count == 0) return;
int newIndex = (option.chosenIndex + option.options.Count + direction) % option.options.Count;
SettingsManager.instance.ChangeSetting(option.settingName, newIndex);
// Перестраиваем лейблы чтобы значение обновилось на экране
ClearSettingsPanel();
CreateSettingsLabels();
}
enum substate { SettingChoosing, OptionChoosing }
}