409 lines
15 KiB
C#
409 lines
15 KiB
C#
using System;
|
||
using System.Collections;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
public class TextInput : SettingsPanelBase
|
||
{
|
||
[Header("Ключ prefs")]
|
||
[SerializeField] private string key;
|
||
[SerializeField] private bool loadData;
|
||
[SerializeField] private bool saveDataOnClose;
|
||
|
||
[Header("Описание страницы")]
|
||
[SerializeField] private TextMeshProUGUI title;
|
||
[SerializeField] private TextMeshProUGUI description;
|
||
|
||
[Header("Пресеты настроек")]
|
||
[SerializeField] private TextInputPreset[] presets;
|
||
|
||
|
||
[Header("Настройки генерации поля")]
|
||
[Tooltip("Якорь лево-низ")][SerializeField] private GameObject SlotPrefab;
|
||
[SerializeField] private RectTransform slotsRoot;
|
||
[SerializeField] private int slotsCount = 10;
|
||
|
||
[SerializeField] private float rootPaddingX = 10;
|
||
[SerializeField] private float rootPaddingY = 10;
|
||
[SerializeField] private float gapSize = 10;
|
||
|
||
[Header("Размер слота")]
|
||
[Tooltip("Предпочитаемый размер стороны квадрата слота. Если слотов много, размер уменьшится, чтобы вместиться.")]
|
||
[SerializeField] private float preferredSlotWidth = 250f;
|
||
[SerializeField] private bool centerWhenSmaller = true;
|
||
// Высота контейнера всегда рассчитывается по числу строк (rows)
|
||
|
||
|
||
[Header("Настройки анимации")]
|
||
|
||
[SerializeField] private float animationSpeed;
|
||
|
||
[SerializeField] private float fontSizeNormal = 20;
|
||
[SerializeField] private float fontSizeHighlighted = 25;
|
||
[SerializeField] private Color fontColorNormal;
|
||
[SerializeField] private Color fontColorHighlighted;
|
||
|
||
[SerializeField] private float slotSizeMultiplier = 1.1f;
|
||
[SerializeField] private Color slotColorNormal;
|
||
[SerializeField] private Color slotColorHighlighted;
|
||
|
||
private Vector2 slotSizeNormal; //расчет при генерации
|
||
// Сохраняем дефолтный размер контейнера, чтобы все перерасчеты основывались на исходном размере
|
||
private Vector2 defaultSlotsRootSize = Vector2.zero;
|
||
|
||
[Header("Алфавит")]
|
||
[SerializeField] private string alphabetString = "0123456789"; //из него генерирует массив символов
|
||
|
||
[Header("Скорость переключения букв")]
|
||
[SerializeField] private float MaxSpeedLetterPerSecond = 8;
|
||
[SerializeField] private float MinSpeedLetterPerSecond = 3;
|
||
[SerializeField] private float MaxSpeedTime = 3;
|
||
[SerializeField] private float MinSpeedTime = 0.5f;
|
||
|
||
private float switchButtonPressedTime = 0;
|
||
private float letterSwitchSpeed = 0;
|
||
private float switchDelayRest = 1;
|
||
|
||
public override GameState State => GameState.Input;
|
||
public override bool UsesContinuousInput => true;
|
||
|
||
|
||
//тут массивы для доступа
|
||
private string[] alphabet; // не забыь добавить пустую строку при генерации (пробел лучше)
|
||
private RectTransform[] slotRects;
|
||
private Graphic[] slotGraphics;
|
||
private TextMeshProUGUI[] slotLabels;
|
||
|
||
private Coroutine animationCoroutine;
|
||
|
||
private int currentSlotIndex = 0;
|
||
private int currentLetterindex = 0;
|
||
|
||
private TextMeshProUGUI currentLabel => slotLabels[currentSlotIndex];
|
||
|
||
[System.Serializable]
|
||
public class TextInputPreset
|
||
{
|
||
public string key;
|
||
public int slotsCount = 10;
|
||
public string alphabetString = "0123456789";
|
||
public string title = "Введите номер";
|
||
public string description = "Введите номер";
|
||
|
||
}
|
||
|
||
public static event Action OnTextChanged;
|
||
|
||
|
||
|
||
public override void Initialize(int prisetIndex)
|
||
{
|
||
if (prisetIndex < 0 ||prisetIndex >= presets.Length)
|
||
{
|
||
Debug.Log("Неверный индекс пресета");
|
||
return;
|
||
}
|
||
this.key = presets[prisetIndex].key;
|
||
this.slotsCount = presets[prisetIndex].slotsCount;
|
||
this.alphabetString = presets[prisetIndex].alphabetString;
|
||
this.title.text = presets[prisetIndex].title;
|
||
this.description.text = presets[prisetIndex].description;
|
||
|
||
// Сохраняем дефолтный размер slotsRoot при первом открытии
|
||
if (slotsRoot != null && defaultSlotsRootSize == Vector2.zero)
|
||
{
|
||
defaultSlotsRootSize = slotsRoot.rect.size;
|
||
}
|
||
|
||
Generate();
|
||
LoadSavedData();
|
||
|
||
if (animationCoroutine != null)
|
||
StopCoroutine(animationCoroutine);
|
||
animationCoroutine = StartCoroutine(SlotsAnimation());
|
||
}
|
||
|
||
public override void Accept()
|
||
{
|
||
string result = GetText();
|
||
|
||
if (!string.IsNullOrEmpty(key) && saveDataOnClose)
|
||
{
|
||
PlayerPrefs.SetString(key, result);
|
||
}
|
||
|
||
StopCoroutine(animationCoroutine);
|
||
|
||
Clear();
|
||
OnTextChanged?.Invoke();
|
||
|
||
|
||
}
|
||
private void Generate()
|
||
{
|
||
if (slotsRoot == null || SlotPrefab == null)
|
||
{
|
||
Debug.LogError("TextInput.Generate: missing slotsRoot or SlotPrefab");
|
||
return;
|
||
}
|
||
|
||
slotsCount = Mathf.Max(1, slotsCount);
|
||
ClearSlotsRootChildren();
|
||
|
||
// Базовый размер контейнера (из дефолта, если он сохранён)
|
||
Vector2 baseRootSize = defaultSlotsRootSize == Vector2.zero ? slotsRoot.rect.size : defaultSlotsRootSize;
|
||
Vector2 contentArea = baseRootSize - new Vector2(rootPaddingX * 2f, rootPaddingY * 2f);
|
||
|
||
float preferred = preferredSlotWidth > 0f ? preferredSlotWidth : 250f;
|
||
|
||
// Сначала рассчитываем размер стороны квадрата при равномерном распределении
|
||
float totalGaps = Mathf.Max(0, slotsCount - 1) * gapSize;
|
||
float availablePerSlotWidth = Mathf.Max(0f, (contentArea.x - totalGaps) / slotsCount);
|
||
float slotSide = Mathf.Max(1f, Mathf.Min(preferred, availablePerSlotWidth, contentArea.y));
|
||
slotSizeNormal = new Vector2(slotSide, slotSide);
|
||
|
||
// Считаем колонки/строки (wrap) на основе полученного slotSide
|
||
int columns = Mathf.Max(1, Mathf.FloorToInt((contentArea.x + gapSize) / (slotSide + gapSize)));
|
||
columns = Mathf.Clamp(columns, 1, slotsCount);
|
||
int rows = Mathf.CeilToInt((float)slotsCount / columns);
|
||
|
||
float contentWidth = columns * slotSide + (columns - 1) * gapSize;
|
||
float contentHeight = rows * slotSide + (rows - 1) * gapSize;
|
||
|
||
float newRootW = contentWidth + rootPaddingX * 2f;
|
||
float newRootH = contentHeight + rootPaddingY * 2f;
|
||
|
||
slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newRootW);
|
||
slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, newRootH);
|
||
|
||
var rootLayout = GetOrAdd<LayoutElement>(slotsRoot.gameObject);
|
||
SetupLayoutElement(rootLayout, newRootW, newRootH, false);
|
||
|
||
Canvas.ForceUpdateCanvases();
|
||
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(slotsRoot);
|
||
|
||
Vector2 start = new Vector2(rootPaddingX, rootPaddingY);
|
||
if (centerWhenSmaller && contentWidth < (slotsRoot.rect.width - rootPaddingX * 2f))
|
||
start.x += ((slotsRoot.rect.width - rootPaddingX * 2f) - contentWidth) * 0.5f;
|
||
|
||
slotRects = new RectTransform[slotsCount];
|
||
slotGraphics = new Graphic[slotsCount];
|
||
slotLabels = new TextMeshProUGUI[slotsCount];
|
||
|
||
for (int i = 0; i < slotsCount; i++)
|
||
{
|
||
int col = i % columns;
|
||
int row = i / columns;
|
||
Vector2 pos = new Vector2(start.x + col * (slotSide + gapSize), -(start.y + row * (slotSide + gapSize)));
|
||
CreateSlot(i, pos, slotSizeNormal);
|
||
}
|
||
|
||
// Алфавит
|
||
alphabet = new string[alphabetString.Length + 1];
|
||
for (int i = 0; i < alphabetString.Length; i++) alphabet[i] = alphabetString[i].ToString();
|
||
alphabet[alphabet.Length - 1] = " ";
|
||
currentSlotIndex = 0;
|
||
}
|
||
|
||
// --- вспомогательные методы ---
|
||
private void ClearSlotsRootChildren()
|
||
{
|
||
for (int i = slotsRoot.childCount - 1; i >= 0; i--)
|
||
{
|
||
var c = slotsRoot.GetChild(i);
|
||
if (Application.isPlaying) Destroy(c.gameObject); else DestroyImmediate(c.gameObject);
|
||
}
|
||
}
|
||
|
||
private T GetOrAdd<T>(GameObject go) where T : Component
|
||
{
|
||
var comp = go.GetComponent<T>();
|
||
if (comp == null) comp = go.AddComponent<T>();
|
||
return comp;
|
||
}
|
||
|
||
private void SetupLayoutElement(LayoutElement l, float w, float h, bool ignoreLayout)
|
||
{
|
||
if (l == null) return;
|
||
l.preferredWidth = w; l.minWidth = w; l.preferredHeight = h; l.minHeight = h;
|
||
l.flexibleWidth = 0f; l.flexibleHeight = 0f; l.ignoreLayout = ignoreLayout;
|
||
}
|
||
|
||
private void CreateSlot(int index, Vector2 anchoredPos, Vector2 size)
|
||
{
|
||
GameObject slot = Instantiate(SlotPrefab, slotsRoot);
|
||
var rt = slot.GetComponent<RectTransform>();
|
||
slotRects[index] = rt;
|
||
slotGraphics[index] = slot.GetComponent<Graphic>();
|
||
slotLabels[index] = slot.transform.GetComponentInChildren<TextMeshProUGUI>();
|
||
|
||
rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
|
||
rt.pivot = new Vector2(0f, 1f);
|
||
rt.localScale = Vector3.one;
|
||
rt.sizeDelta = size;
|
||
rt.anchoredPosition = anchoredPos;
|
||
|
||
var le = GetOrAdd<LayoutElement>(slot);
|
||
SetupLayoutElement(le, size.x, size.y, true);
|
||
|
||
if (slotGraphics[index] != null) slotGraphics[index].color = slotColorNormal;
|
||
if (slotLabels[index] != null) { slotLabels[index].color = fontColorNormal; slotLabels[index].text = ""; slotLabels[index].fontSize = fontSizeNormal; }
|
||
}
|
||
|
||
private void LoadSavedData()
|
||
{
|
||
string data = "";
|
||
|
||
if (loadData)
|
||
data = PlayerPrefs.GetString(key, "");
|
||
|
||
// Очищаем все слоты
|
||
for (int i = 0; i < slotsCount; i++)
|
||
{
|
||
slotLabels[i].text = " ";
|
||
}
|
||
|
||
// Заполняем сохраненными данными
|
||
if (!string.IsNullOrEmpty(data))
|
||
{
|
||
int minLength = Mathf.Min(data.Length, slotsCount);
|
||
for (int i = 0; i < minLength; i++)
|
||
{
|
||
slotLabels[i].text = data[i].ToString();
|
||
}
|
||
}
|
||
}
|
||
|
||
public override void SwitchVertical(int continiousDirection)
|
||
{
|
||
|
||
continiousDirection = Mathf.Clamp(continiousDirection, -1, 1);
|
||
if (continiousDirection == 0)//сброс переключения
|
||
{
|
||
switchDelayRest = 0;
|
||
switchButtonPressedTime = 0;
|
||
return;
|
||
}
|
||
|
||
Debug.Log("Залет");
|
||
|
||
if (switchDelayRest < 0)
|
||
{
|
||
//переключение
|
||
currentLetterindex = (currentLetterindex + continiousDirection + alphabet.Length) % alphabet.Length;
|
||
currentLabel.text = alphabet[currentLetterindex];
|
||
|
||
recalculateRest();
|
||
Debug.Log($"Переключение остаток: {switchDelayRest}");
|
||
}
|
||
switchDelayRest -= Time.deltaTime;
|
||
switchButtonPressedTime += Time.deltaTime;
|
||
|
||
|
||
void recalculateRest()
|
||
{
|
||
float k = (MaxSpeedLetterPerSecond - MinSpeedLetterPerSecond)
|
||
/ (MaxSpeedTime - MinSpeedTime);
|
||
float b = MinSpeedLetterPerSecond - k * MinSpeedTime;
|
||
letterSwitchSpeed = k * switchButtonPressedTime + b;
|
||
letterSwitchSpeed = Mathf.Clamp(letterSwitchSpeed,
|
||
MinSpeedLetterPerSecond,
|
||
MaxSpeedLetterPerSecond);
|
||
switchDelayRest = 1 / letterSwitchSpeed;
|
||
}
|
||
}
|
||
|
||
public override void SwitchHorizontal(int discreteDirection)
|
||
{
|
||
if (discreteDirection == 0) return;
|
||
|
||
currentSlotIndex = (currentSlotIndex + discreteDirection + slotsCount) % slotsCount;
|
||
|
||
string letter = currentLabel.text;
|
||
currentLetterindex = Array.IndexOf(alphabet, letter);
|
||
|
||
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
foreach (RectTransform rect in slotRects)
|
||
{
|
||
Destroy(rect.gameObject);
|
||
}
|
||
}
|
||
|
||
public string GetText()
|
||
{
|
||
System.Text.StringBuilder sb = new();
|
||
|
||
foreach (TextMeshProUGUI label in slotLabels)
|
||
{
|
||
// Если текст пустой строка, считаем это пробелом
|
||
if (!string.IsNullOrEmpty(label.text))
|
||
{
|
||
sb.Append(label.text);
|
||
}
|
||
}
|
||
return sb.ToString().Trim();
|
||
}
|
||
|
||
|
||
IEnumerator SlotsAnimation()
|
||
{
|
||
float scaleSpeed = (slotSizeMultiplier - 1) / animationSpeed;
|
||
float slotColorSpeed = Vector4.Distance(slotColorNormal, slotColorHighlighted) / animationSpeed;
|
||
float fontColorSpeed = Vector4.Distance(fontColorNormal, fontColorHighlighted) / animationSpeed;
|
||
float fontSizeSpeed = (fontSizeHighlighted - fontSizeNormal) / animationSpeed;
|
||
|
||
while (true)
|
||
{
|
||
for (int i = 0; i < slotsCount; i++)
|
||
{
|
||
if (i == currentSlotIndex) //подсвечиваем
|
||
{
|
||
slotRects[i].localScale = Vector3.MoveTowards(slotRects[i].localScale, Vector3.one * slotSizeMultiplier, scaleSpeed);
|
||
slotGraphics[i].color = Vector4.MoveTowards(slotGraphics[i].color, slotColorHighlighted, slotColorSpeed);
|
||
|
||
slotLabels[i].color = Vector4.MoveTowards(slotLabels[i].color, fontColorHighlighted, fontColorSpeed);
|
||
slotLabels[i].fontSize = Mathf.MoveTowards(slotLabels[i].fontSize, fontSizeHighlighted, fontSizeSpeed);
|
||
|
||
}
|
||
else //убираем подсветку
|
||
{
|
||
slotRects[i].localScale = Vector3.MoveTowards(slotRects[i].localScale, Vector3.one, scaleSpeed);
|
||
slotGraphics[i].color = Vector4.MoveTowards(slotGraphics[i].color, slotColorNormal, slotColorSpeed);
|
||
|
||
slotLabels[i].color = Vector4.MoveTowards(slotLabels[i].color, fontColorNormal, fontColorSpeed);
|
||
slotLabels[i].fontSize = Mathf.MoveTowards(slotLabels[i].fontSize, fontSizeNormal, fontSizeSpeed);
|
||
}
|
||
}
|
||
|
||
|
||
yield return null;
|
||
}
|
||
|
||
}
|
||
|
||
// Восстановить slotsRoot к сохранённому дефолтному размеру
|
||
public void ResetSlotsRootToDefault()
|
||
{
|
||
if (slotsRoot == null) return;
|
||
if (defaultSlotsRootSize == Vector2.zero) return;
|
||
slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, defaultSlotsRootSize.x);
|
||
slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, defaultSlotsRootSize.y);
|
||
var rootLayout = slotsRoot.GetComponent<LayoutElement>();
|
||
if (rootLayout != null)
|
||
{
|
||
rootLayout.preferredWidth = defaultSlotsRootSize.x;
|
||
rootLayout.preferredHeight = defaultSlotsRootSize.y;
|
||
rootLayout.minWidth = defaultSlotsRootSize.x;
|
||
rootLayout.minHeight = defaultSlotsRootSize.y;
|
||
}
|
||
Canvas.ForceUpdateCanvases();
|
||
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(slotsRoot);
|
||
}
|
||
|
||
}
|