411 lines
17 KiB
C#
411 lines
17 KiB
C#
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);
|
|
}
|
|
}
|