- name
- xgaida-x-nixi-uitk
- description
- UI Toolkit в Guildmaster: рантайм-экраны на UXML/USS, дизайн-система токенов, компоненты, MVVM и UI-тесты. Зови на любую работу с интерфейсом — экраны, стили, токены, custom controls, PanelSettings — и на всё под Assets/_Project/UI и Scripts/UI, даже если слова «UI Toolkit» в задаче нет. НЕ применять к: боевому uGUI-HUD (Image.Filled и родня), inspector-логике вне UITK, тех-доке об UI-слое (tech-scribe).
# UI Toolkit — рабочий контур Guildmaster
Этот скилл — процедура, а не справка. Он превращает разрозненные правила в чеклист,
который прогоняется на КАЖДОЙ UI-задаче. Цель — чтобы интерфейс собирался
единообразно, из дизайн-системы, и попадал к Максу уже проверенным.
## Прежде всего: карта проекта
Вся дизайн-система и экраны уже стоят. Ничего не изобретай — читай и продолжай.
| Что | Где |
|---|---|
| Токены — ярус 1 (примитивы) | `Assets/_Project/UI/Theme/tokens.primitives.uss` |
| Токены — ярус 2 (семантика) | `Assets/_Project/UI/Theme/tokens.semantic.uss` |
| **Перечень элементов и их состояний** | `Scripts/UI/Components/UiComponentRegistry.cs` — ИСТОЧНИК, читают гейт, лист и звук |
| Компоненты (`.gm-*`) | `Assets/_Project/UI/Theme/components/*.uss` — блок на файл |
| Правила экранов | `Assets/_Project/UI/Theme/screens/*.uss` — экран на файл |
| Агрегатор темы (импортит 3 яруса) | `Assets/_Project/UI/Theme/theme.uss` |
| Экраны (UXML) | `Assets/_Project/UI/Screens/*.uxml` |
| Гейт конвейера цвета | `Assets/_Project/Tests/EditMode/UI/UiColorPipelineTests.cs` |
| Гейт состояний и порядка ярусов | `Tests/EditMode/UI/UiStateGateTests.cs` |
| Гейт мёртвого (класс без стиля, стиль без класса) | `Tests/EditMode/UI/UssDeadCodeTests.cs` |
| **Контактный лист состояний** | `Alebardium → UI → Contact Sheet` (в play) → `Temp/ContactSheet` |
| Custom controls (C#) | `Assets/_Project/Scripts/UI/Components/*.cs` (неймспейс `Guildmaster.UI.Components`, префикс UXML `gm:`) |
| Views / ViewModels / роутер | `Assets/_Project/Scripts/UI/*.cs` (неймспейс `Guildmaster.UI`) |
| Рантайм-asmdef | `Guildmaster.UI.asmdef` |
**Работаем в живой игре, а не на стендах** (реш. Макса 05.08.2026). Превью-стенд
(`UI/Dev/UiPreviewRoot.uxml`, `PreviewPanelSettings.asset`) и витрина `Component Gallery` в проекте
остаются, но из процедуры выведены: они отвечали на вопрос «как это выглядит в изоляции», а решение
принимается по тому, как элемент выглядит В ИГРЕ — на живом фоне, в реальном соседстве, при своей
подложке. Заодно отпадает целый класс ложных выводов: offscreen-рендер с `ppp=1` показывал обманный
масштаб, а изолированный стенд — цвет без того, что под ним лежит.
**Канва: 1920×1080.** Один UI-пиксель = один пиксель токена; всё проектируется под это
разрешение. `PanelSettings`: `scaleMode = ScaleWithScreenSize`, `referenceResolution =
1920×1080`, `screenMatchMode = MatchWidthOrHeight`, `match = 1` (по высоте) — UI держит одну
долю экрана на любом разрешении. **Нецелый скейл разрешён**, и это правило пережило отмену
пиксель-арта: оно опирается не на стиль, а на то, что боевая камера ведёт зум непрерывно и в
ровное кратное на экране не попасть. Множитель `scale = 1` — шов под будущую настройку UI-scale (доступность),
в логике на `scale == 1` не закладываться. (Старые заметки про «640×360 ×3» / целочисленный
`IntegerPanelScaler` — pixel-perfect отменён, удалено в Ф0 UI-реворка.)
## Пять правил, нарушение которых = переделка (HARD)
Это не бюрократия — каждое правило закрывает конкретный способ, которым UI-код
незаметно загнивает. Понимай «почему», тогда не придётся заучивать «нельзя».
1. **Разметка и стиль — только UXML/USS, никогда инлайн в C#.**
Экран собирается как дерево `.gm-*`-классов в `.uxml`, вид задаётся в `.uss`.
*Почему:* как только разметка расползается по C#, «единый стиль» из структуры
превращается в устный договор, который никто не соблюдает.
*Граница:* внутренности атомарного **custom control** (конструктор `SliderRow`
создаёт свои `Label`/`Slider` кодом) — это НЕ нарушение. Правило про **композицию
экранов**, а не про кишки переиспользуемого компонента. См.
`references/component-model.md`.
2. **Значения — только через токены, не хардкод.**
Цвета, отступы, размеры шрифта, рамки — через `var(--gm-*)`. Никаких магических
`rgb(...)` или `24px` в `components.uss` и тем более в экранах.
*Почему:* сменить тему/масштаб = поправить один ярус токенов. Хардкод ломает это.
*Дисциплина ярусов:* экраны и `components.uss` консюмят ТОЛЬКО семантику; семантика
ссылается ТОЛЬКО на примитивы. Прыгать через ярус нельзя.
3. **Текст — только через ключи локализации, заполнять только RU.**
Любая видимая строка идёт через ключ (`ui.*`), русская локаль заполняется,
остальные — прочерк. Прямые строки в UXML/C# = откат.
*Почему:* ретрофит локализации по готовому UI — дорогая боль; ключ сразу стоит
почти ноль.
*Готча (добыто треком тултипов):* `StatValueFormatter` в настройках локализации обязан стоять
**перед `DefaultFormatter`** и иметь пустое имя в списке имён. Иначе `{dmg}` молча напечатает имя
структуры вместо числа; ловит `SmartStatStringTests`.
4. **⚠️ КАРТИНКА В ЧАТ НА КАЖДОЙ ИТЕРАЦИИ — ЖЕЛЕЗНО. Это правило Макс повторял в гневе.**
**Кадр отправляется через `SendUserFile`.** `Read` PNG показывает картинку ТЕБЕ и до Макса
не доходит — это не показ, а самопроверка.
- Правило: **НИ ОДНОГО ответа про визуал БЕЗ картинки в этом же ответе.** Меняла раскладку,
чинила баг, «вот стало лучше» — все три требуют свежего скрина В ЧАТЕ, не пересказа.
- Ритм: ПО ХОДУ работы (сделал/поправил → показал → дальше), не пачкой к концу — короткая
петля «показал → поправил» экономит время обоим.
- Кадр — **из живой игры** (`ScreenCapture.CaptureScreenshot` в play), в честном 1920×1080.
Крупный план (кроп зоны) помогает, когда обсуждается деталь, — но кроп это ДОБАВКА к кадру,
а не замена.
*Почему:* Макс делает визуальную приёмку по КАРТИНКЕ. Нет картинки в ответе — нет приёмки,
он вынужден переспрашивать «где скрин», и это его бесит (по делу). Это самый частый мой
провал — держи в голове ПЕРВЫМ.
5. **Тесты — под игру и ГДД, не наоборот.**
Если UI-тест краснеет — чини UI или тест под реальное поведение, не ослабляй
механику ради зелёного.
## Реф — ВХОД задачи, а не справка (06.08.2026)
Разбор по архиву сессий: неверно прочитанный реф — САМЫЙ частый мой провал по UI (девять эпизодов из
двадцати пяти) и самый дорогой: именно он даёт циклы по десять-двенадцать раундов на один элемент.
Механика провала всегда одна — я работала по памяти о рефе, а не по разбору.
**Порядок, от которого не отступать:**
1. **Открыть разбор ДО первой правки.** База — `Art_Dev/UI Refs/`, разборы — `_teardowns/*.md` на
КЛАСС экрана (главное меню, настройки, награда…). Нет разбора на этот класс — сначала разбор,
потом код. Правило самой базы: каждое утверждение со ссылкой на файл, размеры в долях экрана,
«цвет — замером, а не глазами».
2. **Мерить ТОТ файл, что приложил Макс.** 06.08.2026 я померила саму игру-первоисточник вместо
присланного им кадра и уверенно назвала числа не из того источника. Приложенный кадр — это
заказ; игра-источник — справка.
3. **Перед показом — сравнить пару.** Реф и результат рядом, письменный список расхождений, и только
потом «смотри». «Вот стало лучше» — не приёмка, а просьба сделать приёмку за меня.
**Что я решаю сама, а что нет.** Разделение из README базы рефов: Макс — вкус и приёмка, я — замер и
сведение. Значит «похоже ли» решает он, а «совпадает ли» обязана доказать я — числами и кадрами.
## Компонентная модель — ГИБРИД (как советует Unity)
Развилка «темплейт или C#-контрол» решается по наличию логики/состояния:
- **Нет логики — UXML-темплейт.** Чисто визуальный повторяющийся блок →
`.uxml`-файл, вставляется через `<ui:Template>`/`<ui:Instance>`. Ноль C#.
- **Есть логика/состояние — custom control.** `[UxmlElement] partial class : VisualElement`
с `[UxmlAttribute]`-пропсами, `AddToClassList("gm-...")` в конструкторе, экспонируется
в UXML под `gm:`. Эталон — `SliderRow`.
- **Дублируется третий раз — выноси в компонент.** Скопировал разметку/стиль дважды —
на третий делай переиспользуемый компонент. Инлайн-повтор в экранах не копи.
- **Компонент — «префаб» интерфейса.** Один контрол + его USS-классы обслуживают ВСЕ вхождения:
правишь `.gm-plate-button` — меняются все кнопки-пластины игры, правишь `.gm-mainmenu__btn` —
только пункты меню, правишь токен — всё, что берёт эту роль. Отсюда правило: изменение вида
вносится на том ярусе, где оно должно действовать, а не в ближайшем месте.
Детали, примеры, старый vs новый синтаксис (`UxmlFactory`/`UxmlTraits` — deprecated) —
в `references/component-model.md`. **Читай его перед созданием нового компонента.**
## USS-нейминг — BEM
`block__element--modifier`. У нас это уже де-факто (`.gm-panel__title`,
`.gm-button--primary`, `.gm-tab--active`). Держи единообразно; не используй имена типов
(`Button`) и id-селекторы в стилях. Подробности и анти-примеры — в
`references/component-model.md`.
## Данные ↔ UI — MVVM + runtime binding
- **ViewModel — POCO**, создаётся через DI, НЕ MonoBehaviour, тестируется без сцены.
Эталон — `SettingsViewModel` (baseline-снапшот, `event Changed`, `Begin/Save/Cancel`).
- **VM → UI без эха:** обновляя контрол из VM, шли `SetValueWithoutNotify`, иначе
словишь событие обратно и зациклишься.
- Новые экраны — на этом паттерне. Детали и проводка View↔VM↔Router —
в `references/screens-and-mvvm.md`.
## Размеры — против гигантизма (под 1080p)
Философия «панели не на пол-экрана, читаемая типографика, ритм по сетке» держится,
пороги — под 1920×1080. Конкретные значения — из токенов, не из головы. Быстрый чеклист
(шкала отступов `--gm-space-*` = 8→64, шрифты `--gm-font-*` = 22→48) — в
`references/live-session-and-screenshots.md`.
## Как я авторю UXML/USS
1. **Пишу файлы напрямую** (`Write`/`Edit`) — так я контролирую разметку и YAML.
2. **Проверяю в живой игре** через `execute_code`: обход дерева от `UIDocument.rootVisualElement`,
чтение `resolvedStyle`, кадр через `ScreenCapture`. Превью-стенд и offscreen-рендер для этого
больше не используются — см. следующий раздел.
3. **Namespace-префикс в UXML — всегда с `ui:` / `gm:`** (`<ui:Style>`, не `<Style>`),
иначе UI Builder не откроет файл.
## Числа подбираются В ЖИВОМ play, а не перезаходами (реш. Макса 04.08.2026)
Подбор вида — это петля «темнее / шире / мягче», и она обязана крутиться **не выходя из игры**.
Правка файла на каждый шаг стоит выхода из play, переимпорта, входа обратно и потери состояния —
за вечер это десятки минут его времени на ровном месте.
**Как надо.** Макс входит в play, встаёт на нужный экран и НЕ выходит. Числа я правлю прямо в живой
панели через `mcp__unityMCP__execute_code`: ищу элемент обходом дерева от `UIDocument.rootVisualElement`,
ставлю `ve.style.*` (а приватные поля custom control — рефлексией) и зову `MarkDirtyRepaint()`.
Он видит результат мгновенно. Сошлись — **тогда** записываю значения в USS/токены, и перезаход нужен
ровно один, на проверку.
**Готча приёма, на которой я села дважды подряд:** правка приватных полей рефлексией живёт
**только до следующего пересчёта стиля**. Пересчёт случается не в момент вызова `ve.style.*`, а на
следующем кадре, перечитывает USS (в живой сессии он ещё старый) и молча возвращает всё назад —
выглядит как «агент вернул старый цвет». Лечение: вешать `RegisterCallback<CustomStyleResolvedEvent>`,
который переустанавливает значения после каждого пересчёта, а не красить один раз. Инлайновый
`ve.style.*` этим не страдает — он часть каскада.
Границы приёма, обе важные:
- **Так меняются только ЧИСЛА.** Формула, новое поле, новый элемент — это код: компиляция и перезаход
неизбежны. Не делай вид, что подкрутил, если поменялась математика.
- **Инлайн-стиль живёт до конца сеанса и НЕ является правкой.** Найденное значение обязано уехать в
файл, иначе оно умрёт вместе с play mode. Правило 1 (никаких инлайнов) не нарушено: это
измерительный прибор, а не авторинг.
**Почему не Live Reload.** Штатный UI Toolkit Live Reload (тумблер в меню ⋮ Game view) для рантайма
включён по умолчанию и умеет подхватывать USS/UXML на лету — но ему нужно, чтобы Unity ЗАМЕТИЛ правку
файла, а **Auto Refresh у нас выключен** ради диеты компиляции. Оттого правка «не доезжает», и это
выглядит как «стиль не применился». Включать Auto Refresh насовсем нельзя (вернётся переимпорт на
каждое касание); включить его на сессию подбора UI — законный вариант, но гасить после.
**И главное про переимпорт: пока идёт play, Unity ассеты НЕ переимпортирует.** `AssetDatabase.Refresh`
отвечает успехом, а USS в живую панель не приезжает. Сказать «готово, смотри», не проверив
`EditorApplication.isPlaying`, — это отправить Макса смотреть на старую картинку (поймано 04.08.2026,
стоило трёх заходов).
## Цвет: одна дорога на экран, и она проверяется пипеткой
**Условие, без которого разговор о палитре бессмысленен: что стоит в токене — то и на экране.**
05.08.2026 оно у нас не выполнялось (ручная конверсия душила заливки втрое), и подбор цвета шёл
вслепую: любое названное Максом число превращалось на экране в другое. Сначала проверь равенство,
потом обсуждай оттенки.
**У цвета ДВЕ дороги, и вторая — источник дефектов:**
| Дорога | Как идёт | Конверсия |
|---|---|---|
| штатная | USS → `background-color` / `border-color` / `Painter2D` | делает движок, значение задаётся ПРЯМОЕ |
| ручная | USS → custom property → поле C# → `Vertex.tint` | делает шейдер UITK, поэтому наш код НЕ конвертирует |
Обе дороги хотят одного и того же: **прямое значение из токена**. Нигде в UI-коде не должно быть
`.linear`, `.gamma` или своей математики над цветом. Отказаться от ручной дороги нельзя: штатных
градиентов в USS Unity не даёт и не планирует, а градиент на кнопке нам нужен.
**Три правила, которые держат её чистой:**
1. **Ровно одна функция на весь UI — `PlateButton.VertexColor`, и она тождество.** Компонент,
рисующий мешем, зовёт её, а не колдует у себя.
2. **У компонента нет своего цвета.** Дефолт поля — `Color.clear`, значение приходит только из USS.
Иначе получается второй владелец, который однажды разойдётся с токеном (так было у `EdgeVeil`:
токен 0.88, хардкод 0.92).
3. **Инвариант держит ТЕСТ, а не докстринг.** Комментарий не падает — и тот самый докстринг,
который запрещал конверсию, сам же её и предписывал целые сутки.
**Процедура подбора цвета — четыре шага:**
1. Цвет называется РОЛЬЮ (`--gm-color-surface-accent`), а не оттенком; оттенки живут ступенями рамп.
2. Крутим в живом play (см. раздел выше).
3. **Перед фиксацией — замер пикселя**, а не взгляд на скрин: снять кадр, взять точку внутри
элемента, сравнить с заданным. Не сходится — это дефект конвейера, а не повод крутить число.
4. Найденное уходит в ТОКЕН, не в правило экрана.
**Пипетка по рефу — законный способ задать цвет.** Если Макс утвердил кадр («верни именно этот
цвет»), значения снимаются с кадра `getpixel`, а не подбираются на глаз, и в токен едут буквально.
**Фаска следует за размером пластины** (правило Макса 05.08.2026). Поменял ширину кнопки — поменяй
`--gm-plate-chamfer` в той же пропорции (360→320 ⇒ 10→9). Скол, оставленный крупным, читается как
более резкая огранка у меньшей вещи: пропорция фигуры меняется, хотя «фигуру никто не трогал». То же
касается любого размера, заданного в абсолютных единицах внутри компонента.
## Коммитить КАЖДОЕ изменение UI (правило Макса 05.08.2026)
Работа по виду — это серия «покрутили → посмотрели → не то → назад». Без коммита на каждый шаг
откатиться некуда: инлайновые пробы умирают с play-сессией, а файлы к тому моменту уже трижды
переписаны. Поэтому: **изменение вида = отдельный коммит**, даже маленькое (цвет каймы, кегль,
ширина). Дробить, а не копить пачкой — цена коммита секунды, цена потерянного варианта — вечер.
## Чеклист сдачи UI-задачи
Прогнать перед тем, как сказать «готово»:
- [ ] Разметка/стиль в UXML/USS, не в C# (кроме кишок custom control)
- [ ] Ноль хардкод-значений — всё через `var(--gm-*)`, ярусы не перепрыгнуты
- [ ] Весь текст через loc-ключи, RU заполнен
- [ ] BEM-классы единообразны; новый/изменённый компонент есть в витрине
- [ ] Новый экран — на MVVM (VM = POCO), VM→UI через `SetValueWithoutNotify`
- [ ] Проект компилируется (`read_console` после C#-правок)
- [ ] Числа подобраны в живом play, а не серией перезаходов; найденное уехало в USS
- [ ] Цвет сверен ЗАМЕРОМ пикселя с тем, что задано в токене
- [ ] Каждое изменение вида — своим коммитом, а не пачкой в конце
- [ ] Перед «готово, смотри» проверено `EditorApplication.isPlaying` — иначе он смотрит старое
- [ ] Тронул состояния или компонент — снят КОНТАКТНЫЙ ЛИСТ и показан
- [ ] Новый элемент есть в `UiComponentRegistry`, гейты `UiStateGateTests` и `UssDeadCodeTests` зелёные
- [ ] Работа по рефу — открыт разбор, сравнение пары «реф ↔ результат» со списком расхождений
- [ ] Скрин Максу в реальном масштабе (1920×1080)
## Справочные файлы (читать по надобности)
- `references/component-model.md` — темплейты vs custom controls, BEM, синтаксис Unity 6,
антипаттерны. Читать перед созданием компонента.
- `references/screens-and-mvvm.md` — MVVM-проводка, роутер, биндинг, DI.
- `references/live-session-and-screenshots.md` — петля работы в живом play, снятие кадра, замер
пипеткой, чеклист размеров под 1080p и самопроверка перед показом. **Основной файл процедуры.**
- `references/ui-testing.md` — `com.unity.ui.test-framework`, PanelSimulator, смоук-тесты
кликов.
GitHubで見る