Files
MVP1/Assets/Scripts/Meta/UI/UIKit.cs
T

411 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
// Конструктор интерфейса из кода: текст, кнопки, строки, прокрутка, полоски прогресса,
// всплывающие сообщения (Toast) и окна (подтверждение, ввод имени, результат боя).
//
// Почему из кода: списки (задания, письма, товары) меняются во время игры, их всё равно
// надо создавать на лету. Цвета и размеры -- в Theme ниже, дизайнер может поменять их тут.
public static class UIKit
{
// ---------------- ТЕМА ----------------
public static class Theme
{
public static readonly Color Card = new Color(0.17f, 0.19f, 0.24f, 1f);
public static readonly Color CardDark = new Color(0.12f, 0.13f, 0.17f, 1f);
public static readonly Color CardDone = new Color(0.16f, 0.26f, 0.18f, 1f);
public static readonly Color Gold = new Color(0.95f, 0.75f, 0.25f, 1f);
public static readonly Color Green = new Color(0.25f, 0.62f, 0.3f, 1f);
public static readonly Color Red = new Color(0.72f, 0.24f, 0.22f, 1f);
public static readonly Color Blue = new Color(0.25f, 0.45f, 0.75f, 1f);
public static readonly Color Gray = new Color(0.32f, 0.33f, 0.37f, 1f);
public static readonly Color Text = new Color(0.95f, 0.95f, 0.95f, 1f);
public static readonly Color TextDim = new Color(0.7f, 0.72f, 0.78f, 1f);
public static readonly Color BarBg = new Color(0.08f, 0.08f, 0.1f, 1f);
}
// ---------------- БАЗА ----------------
public static RectTransform Make(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
return (RectTransform)go.transform;
}
public static RectTransform Stretch(RectTransform rt, float left = 0, float top = 0, float right = 0, float bottom = 0)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = new Vector2(left, bottom);
rt.offsetMax = new Vector2(-right, -top);
return rt;
}
public static LayoutElement Size(Component c, float width = -1, float height = -1, float flexW = -1)
{
var le = c.GetComponent<LayoutElement>();
if (le == null) le = c.gameObject.AddComponent<LayoutElement>();
if (width >= 0) { le.preferredWidth = width; le.minWidth = width; }
if (height >= 0) { le.preferredHeight = height; le.minHeight = height; }
if (flexW >= 0) le.flexibleWidth = flexW;
return le;
}
public static void Clear(Transform t)
{
for (int i = t.childCount - 1; i >= 0; i--)
{
var c = t.GetChild(i).gameObject;
c.SetActive(false);
UnityEngine.Object.Destroy(c);
}
}
// ---------------- ЭЛЕМЕНТЫ ----------------
public static Image Box(Transform parent, Color color, float height = -1)
{
var rt = Make("Box", parent);
var img = rt.gameObject.AddComponent<Image>();
img.color = color;
if (height >= 0) Size(img, -1, height);
return img;
}
public static TextMeshProUGUI Text(Transform parent, string text, float size = 30, Color? color = null,
TextAlignmentOptions align = TextAlignmentOptions.Left, float height = -1)
{
var rt = Make("Text", parent);
var t = rt.gameObject.AddComponent<TextMeshProUGUI>();
t.text = text;
t.fontSize = size;
t.color = color ?? Theme.Text;
t.alignment = align;
t.raycastTarget = false;
t.richText = true;
if (height >= 0) Size(t, -1, height);
return t;
}
public static Button Button(Transform parent, string label, Color color, Action onClick,
float width = -1, float height = 86, float fontSize = 28)
{
var rt = Make("Btn_" + label, parent);
var img = rt.gameObject.AddComponent<Image>();
img.color = color;
var b = rt.gameObject.AddComponent<Button>();
var colors = b.colors;
colors.disabledColor = new Color(0.55f, 0.55f, 0.55f, 0.6f);
b.colors = colors;
if (onClick != null) b.onClick.AddListener(() => onClick());
var t = Text(rt, label, fontSize, Theme.Text, TextAlignmentOptions.Center);
Stretch(t.rectTransform, 8, 4, 8, 4);
t.enableAutoSizing = true; t.fontSizeMin = 16; t.fontSizeMax = fontSize;
Size(b, width, height);
return b;
}
public static void SetButton(Button b, string label, bool interactable)
{
if (b == null) return;
var t = b.GetComponentInChildren<TextMeshProUGUI>();
if (t != null && label != null) t.text = label;
b.interactable = interactable;
}
// Горизонтальная строка: элементы слева направо
public static RectTransform Row(Transform parent, float height = 90, float spacing = 12, Color? bg = null, int padding = 0)
{
var rt = Make("Row", parent);
if (bg.HasValue) rt.gameObject.AddComponent<Image>().color = bg.Value;
var h = rt.gameObject.AddComponent<HorizontalLayoutGroup>();
h.spacing = spacing;
h.padding = new RectOffset(padding, padding, padding, padding);
h.childAlignment = TextAnchor.MiddleLeft;
h.childControlWidth = true; h.childControlHeight = true;
h.childForceExpandWidth = false; h.childForceExpandHeight = true;
if (height >= 0) Size(rt, -1, height);
return rt;
}
// Вертикальная "карточка": элементы сверху вниз, высота по содержимому
public static RectTransform Card(Transform parent, Color? bg = null, int padding = 18, float spacing = 10)
{
var rt = Make("Card", parent);
rt.gameObject.AddComponent<Image>().color = bg ?? Theme.Card;
var v = rt.gameObject.AddComponent<VerticalLayoutGroup>();
v.padding = new RectOffset(padding, padding, padding, padding);
v.spacing = spacing;
v.childControlWidth = true; v.childControlHeight = true;
v.childForceExpandWidth = true; v.childForceExpandHeight = false;
return rt;
}
public static RectTransform Spacer(Transform parent, float height)
{
var rt = Make("Spacer", parent);
Size(rt, -1, height);
return rt;
}
// Прокручиваемый список на всю область parent. Возвращает контейнер для элементов.
public static RectTransform ScrollList(Transform parent, out ScrollRect scroll, float spacing = 14)
{
var root = Stretch(Make("Scroll", parent));
var bg = root.gameObject.AddComponent<Image>();
bg.color = new Color(0, 0, 0, 0); // прозрачный, но ловит свайпы
scroll = root.gameObject.AddComponent<ScrollRect>();
scroll.horizontal = false;
scroll.movementType = ScrollRect.MovementType.Elastic;
scroll.scrollSensitivity = 40f;
var viewport = Stretch(Make("Viewport", root));
viewport.gameObject.AddComponent<RectMask2D>();
var content = Make("Content", viewport);
content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1);
content.pivot = new Vector2(0.5f, 1);
content.offsetMin = Vector2.zero; content.offsetMax = Vector2.zero;
var v = content.gameObject.AddComponent<VerticalLayoutGroup>();
v.spacing = spacing;
v.padding = new RectOffset(6, 6, 6, 24);
v.childControlWidth = true; v.childControlHeight = true;
v.childForceExpandWidth = true; v.childForceExpandHeight = false;
var fit = content.gameObject.AddComponent<ContentSizeFitter>();
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scroll.viewport = viewport;
scroll.content = content;
return content;
}
// Полоска прогресса. Возвращает картинку заливки (для обновления через SetBar)
public static Image Bar(Transform parent, float fraction, Color fill, float height = 26, string label = null)
{
var bg = Box(parent, Theme.BarBg, height);
var f = Make("Fill", bg.transform);
var img = f.gameObject.AddComponent<Image>();
img.color = fill;
img.raycastTarget = false;
f.anchorMin = Vector2.zero; f.offsetMin = Vector2.zero; f.offsetMax = Vector2.zero;
SetBar(img, fraction);
if (label != null)
{
var t = Text(bg.transform, label, height * 0.7f, Theme.Text, TextAlignmentOptions.Center);
Stretch(t.rectTransform);
t.name = "Label";
}
return img;
}
public static void SetBar(Image fill, float fraction, string label = null)
{
if (fill == null) return;
var rt = fill.rectTransform;
rt.anchorMax = new Vector2(Mathf.Clamp01(fraction), 1);
if (label != null)
{
var t = fill.transform.parent.Find("Label");
if (t != null) t.GetComponent<TextMeshProUGUI>().text = label;
}
}
// Цветной квадрат-иконка (заглушка, пока нет спрайтов) с буквой
public static Image Icon(Transform parent, Color color, string letter, float size = 80)
{
var img = Box(parent, color);
Size(img, size, size);
if (!string.IsNullOrEmpty(letter))
{
var t = Text(img.transform, letter, size * 0.5f, Color.white, TextAlignmentOptions.Center);
Stretch(t.rectTransform);
t.fontStyle = FontStyles.Bold;
}
return img;
}
public static Slider Slider(Transform parent, float value, Action<float> onChange)
{
var go = DefaultControls.CreateSlider(new DefaultControls.Resources());
go.transform.SetParent(parent, false);
var s = go.GetComponent<Slider>();
s.minValue = 0; s.maxValue = 1; s.value = value;
foreach (var img in go.GetComponentsInChildren<Image>())
{
if (img.name == "Background") img.color = Theme.BarBg;
else if (img.name == "Fill") img.color = Theme.Gold;
else if (img.name == "Handle") img.color = Color.white;
}
if (onChange != null) s.onValueChanged.AddListener(v => onChange(v));
Size(s, -1, 50, 1);
return s;
}
// Кнопка-переключатель ВКЛ/ВЫКЛ
public static Button Switch(Transform parent, bool on, Action<bool> onChange)
{
Button b = null;
bool state = on;
b = Button(parent, on ? "ВКЛ" : "ВЫКЛ", on ? Theme.Green : Theme.Gray, () =>
{
state = !state;
b.GetComponent<Image>().color = state ? Theme.Green : Theme.Gray;
b.GetComponentInChildren<TextMeshProUGUI>().text = state ? "ВКЛ" : "ВЫКЛ";
if (onChange != null) onChange(state);
}, 160, 70);
return b;
}
// Вкладки: ряд кнопок, текущая подсвечена
public static void Tabs(Transform parent, string[] names, int current, Action<int> onSelect)
{
var row = Row(parent, 80, 8);
for (int i = 0; i < names.Length; i++)
{
int idx = i;
var b = Button(row, names[i], i == current ? Theme.Gold : Theme.CardDark, () => onSelect(idx), -1, 80, 28);
Size(b, -1, 80, 1);
if (i == current) b.GetComponentInChildren<TextMeshProUGUI>().color = new Color(0.15f, 0.1f, 0.05f);
}
}
// ---------------- ВЕРХНИЙ СЛОЙ (тосты и окна) ----------------
static RectTransform overlay;
public static RectTransform Overlay
{
get
{
if (overlay == null)
{
var go = new GameObject("UI_Overlay", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
var canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 1000;
var scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1080, 1920);
scaler.matchWidthOrHeight = 0.5f;
overlay = (RectTransform)go.transform;
}
return overlay;
}
}
static ToastRunner runner;
static ToastRunner Runner
{
get
{
if (runner == null) runner = Overlay.gameObject.AddComponent<ToastRunner>();
return runner;
}
}
// Всплывающее сообщение внизу экрана на 2.5 секунды
public static void Toast(string message)
{
if (string.IsNullOrEmpty(message)) return;
if (!Application.isPlaying) { Debug.Log(message); return; }
Runner.Show(message);
}
class ToastRunner : MonoBehaviour
{
int active;
public void Show(string message) { StartCoroutine(Run(message)); }
IEnumerator Run(string message)
{
var rt = Make("Toast", Overlay);
rt.anchorMin = new Vector2(0.08f, 0); rt.anchorMax = new Vector2(0.92f, 0);
rt.pivot = new Vector2(0.5f, 0);
rt.anchoredPosition = new Vector2(0, 300 + active * 110);
rt.sizeDelta = new Vector2(0, 96);
var img = rt.gameObject.AddComponent<Image>();
img.color = new Color(0.05f, 0.05f, 0.07f, 0.92f);
img.raycastTarget = false;
var t = Text(rt, message, 28, Theme.Text, TextAlignmentOptions.Center);
Stretch(t.rectTransform, 20, 6, 20, 6);
t.enableAutoSizing = true; t.fontSizeMin = 16; t.fontSizeMax = 28;
var group = rt.gameObject.AddComponent<CanvasGroup>();
group.blocksRaycasts = false;
active++;
yield return new WaitForSecondsRealtime(2.2f);
for (float a = 1; a > 0; a -= Time.unscaledDeltaTime * 3f) { group.alpha = a; yield return null; }
active--;
Destroy(rt.gameObject);
}
}
// Окно поверх всего: затемнение + карточка по центру. Возвращает карточку для наполнения.
public static RectTransform Modal(string title, out GameObject root)
{
var dim = Stretch(Make("Modal", Overlay));
dim.gameObject.AddComponent<Image>().color = new Color(0, 0, 0, 0.65f);
root = dim.gameObject;
var card = Card(dim, Theme.CardDark, 32, 20);
card.anchorMin = new Vector2(0.06f, 0.5f); card.anchorMax = new Vector2(0.94f, 0.5f);
card.pivot = new Vector2(0.5f, 0.5f);
card.anchoredPosition = Vector2.zero;
var fit = card.gameObject.AddComponent<ContentSizeFitter>();
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var t = Text(card, title, 42, Theme.Gold, TextAlignmentOptions.Center);
t.fontStyle = FontStyles.Bold;
return card;
}
public static void Message(string title, string text, string button = "ОК", Action onClose = null)
{
GameObject root;
var card = Modal(title, out root);
Text(card, text, 30, Theme.Text, TextAlignmentOptions.Center);
Spacer(card, 10);
Button(card, button, Theme.Green, () => { UnityEngine.Object.Destroy(root); if (onClose != null) onClose(); }, -1, 96, 32);
}
public static void Confirm(string title, string text, string yes, Action onYes)
{
GameObject root;
var card = Modal(title, out root);
Text(card, text, 30, Theme.Text, TextAlignmentOptions.Center);
var row = Row(card, 96, 20);
var no = Button(row, "Отмена", Theme.Gray, () => UnityEngine.Object.Destroy(root));
Size(no, -1, 96, 1);
var ok = Button(row, yes, Theme.Red, () => { UnityEngine.Object.Destroy(root); if (onYes != null) onYes(); });
Size(ok, -1, 96, 1);
}
public static void InputDialog(string title, string initial, int maxLength, Action<string> onOk)
{
GameObject root;
var card = Modal(title, out root);
var go = TMP_DefaultControls.CreateInputField(new TMP_DefaultControls.Resources());
go.transform.SetParent(card, false);
var input = go.GetComponent<TMP_InputField>();
input.text = initial;
input.characterLimit = maxLength;
input.pointSize = 34;
go.GetComponent<Image>().color = new Color(0.9f, 0.9f, 0.92f);
Size(input, -1, 90);
var row = Row(card, 96, 20);
var no = Button(row, "Отмена", Theme.Gray, () => UnityEngine.Object.Destroy(root));
Size(no, -1, 96, 1);
var ok = Button(row, "Сохранить", Theme.Green, () =>
{
string v = input.text.Trim();
if (v.Length == 0) { Toast("Имя не может быть пустым"); return; }
UnityEngine.Object.Destroy(root);
if (onOk != null) onOk(v);
});
Size(ok, -1, 96, 1);
}
}