Единый источник правды о размерах и стиле текста в интерфейсе. Кегль в UI не выбирается «на глаз в каждом экране» — он выбирается по ЯРУСУ, к которому принадлежит элемент. Здесь — шкала, ярусы, правила и примеры.
Связано: Journal - The World Replicates, The View Does Not, токены —
Assets/_Project/UI/Theme/tokens.primitives.uss, применение —components.uss.
Правило номер один: font-size задаётся ТОЛЬКО токеном --gm-font-* и только в USS.
Инлайн-размеры в C# и голые пиксели в классах запрещены — иначе «единый источник» превращается
в тридцать источников, и ни один экран не согласуется с соседним.
1. Шкала
Все значения — под нативную канву 1920×1080 (PanelSettings.scale = 1, ScaleWithScreenSize).
| Токен | px | Голос | Где стоит по факту |
|---|---|---|---|
--gm-font-xxs | 13 | Служебная подпись: то, что читают глазами по диагонали | .gm-stat__label, .gm-arcana-card__title, .gm-chip--sm, .gm-tooltip__line-label |
--gm-font-xs | 17 | Плотный рабочий текст: значения, нарратив, поля ввода | .gm-stat__value, .gm-loadout__narrative-text, .gm-loadout__search, .gm-tooltip__desc |
--gm-font-sm | 22 | Заголовок блока внутри плотного экрана; вторичные подписи | .gm-loadout__section-label, .gm-card__name, .gm-tooltip__title, .gm-text-muted |
--gm-font-body | 26 | Основной текст полноэкранного/диалогового слоя | .gm-button, .gm-tab, .gm-event-body, .gm-slider-row__label |
--gm-font-title | 34 | Заголовок панели-диалога | .gm-panel__title, .gm-shop__gold |
--gm-font-lg | 48 | Одна вещь на экран: титул игры, глиф, тост | .gm-mainmenu__title, .gm-titlecard__title, .gm-chest__glyph |
2. Три яруса — как выбрать кегль
Кегль определяется НЕ важностью текста, а плотностью экрана, на котором он живёт.
| Ярус | Что это | Заголовок | Текст | Подпись |
|---|---|---|---|---|
| Диалоговый | pause, настройки, ивент, награда, главное меню — окно на пустом фоне | title (34) | body (26) | sm (22) |
| Рабочий (плотный) | инвентарь, магазин, лоадаут — много блоков одновременно | sm (22) | xs (17) | xxs (13) |
| Вторичный | тултип, тост, служебный оверлей — комментарий к тому, что уже на экране | sm (22) | xs (17) | xxs (13) |
Вторичный слой никогда не крупнее того, что объясняет. Это самая частая ошибка: подсказка, набранная кеглем диалога, читается как отдельный экран и выглядит громоздкой, даже если текста в ней две строки.
Пример-урок (2026-07-26). Тултип был набран
body(26) заголовок иsm(22) текст — крупнее панели деталей инвентаря, к которой он относился (xxs/xs), приmax-width: 220px. Итог: узкая колонка крупных букв, фраза рвётся на шесть строк, окно занимает четверть экрана. Починка — не «уменьшить окно», а вернуть его на свой ярус:sm/xs/xxsплюс ширина 220 → 320. Высокое узкое окно выглядит массивнее широкого и низкого при том же объёме текста.
3. Ширина строки
Комфортная строка — 45–75 знаков. При 17 px это примерно 300–380 px, поэтому:
| Класс | max-width | Почему |
|---|---|---|
.gm-tooltip | 320 | текстовая подсказка в 2–3 строки |
.gm-tooltip--wide | 380 | разбор со стат-строками: подпись и значение в одну строку |
Не сужать ради «компактности»: сужение не уменьшает окно, оно делает его выше.
4. Гарнитуры
| Роль | Шрифт | Где |
|---|---|---|
| Тело интерфейса | Fira Sans (TextCore SDF) | всё, кроме перечисленного ниже |
| Заголовки, нарратив | Cormorant Garamond Medium | .gm-panel__title, .gm-loadout__narrative-text (курсив) |
Гарнитура прибивается в components.uss (секция «ЯРУС ШРИФТОВ»), а не в UXML: тема Unity вешает
шрифт прямо на .unity-text-element, и прямое правило обязано бить унаследованное.
4.1 Шрифтовые ассеты — только Static-атлас
Шрифты в Assets/_Project/UI/Fonts/*SDF*.asset — всегда Static atlas population с заранее
запечённым набором EN+RU. Dynamic запрещён.
Почему. Dynamic дорисовывает глифы прямо в редакторе — при наборе текста, при входе в Play —
и пересохраняет весь .asset. Файл это гигантский бинароподобный блоб (FiraSans-Regular раздувался
до 26 МБ), и git складывает новую копию при каждой регенерации: сотни тысяч строк diff и
мерж-конфликты на общей ветке. Static замораживает атлас — ноль регенераций, фиксированный размер,
детерминизм между машинами, быстрее рантайм.
Набор символов (213 глифов): ASCII 0x20–0x7E + кириллица 0x0400–0x045F + экстра
0x00A0 00AB 00BB 00A9 00AE 00B0 00B7 00D7 2013 2014 2018 2019 201C 201D 2022 2026 2116 20BD 20AC 2212 2192 2190.
Появилась локаль с новыми глифами (укр. ҐґІі, дополнительная пунктуация) — расширяем набор и
делаем ре-бейк: один осознанный коммит вместо пассивного churn.
Рецепт бейка (через execute_code, на каждый шрифт):
SerializedObject→m_AtlasWidth/m_AtlasHeight= 2048,m_IsMultiAtlasTexturesEnabled= false,Apply().atlasPopulationMode = Dynamic, затемClearFontAssetData(false).TryAddCharacters(charset, out missing)— проверить, чтоmissing.Length == 0.atlasPopulationMode = Static.SetDirtyна сам ассет и на текстуры атласа,SaveAssets().
Point size / padding / render mode (90 / 9 / SDFAA) не трогать — это визуал, его принимает Макс.
Готчи.
- Это
UnityEngine.TextCore.Text.FontAsset(UI Toolkit / TextCore), неTMPro.TMP_FontAsset: грузитьLoadAssetAtPath<UnityEngine.TextCore.Text.FontAsset>(...), TMP-тип вернётnull. Enum тоже свой —UnityEngine.TextCore.Text.AtlasPopulationMode. - В
.gitattributesстоит*SDF*.asset -diff -merge— но это страховочная сеть, а не решение. Решение — Static. - Старые блобы в истории убираются только её переписыванием (BFG / filter-repo). Не затеваем: цена координации выше выгоды, цель — остановить рост, а не отыграть прошлое.
- Настоящий динамический текст (будущие имена игроков из Steam, чат) — отдельный контейнерный fallback-шрифт. Решать, когда дойдёт; в churn-путь не пускать.
5. Цвет текста — тоже часть стиля
| Токен | Смысл |
|---|---|
--gm-color-text | основной текст, значения |
--gm-color-text-muted | подписи, описания, всё вторичное |
--gm-color-disabled-text | выключенный элемент (гасим ЦВЕТОМ, не прозрачностью — сквозь неё виден фон) |
--gm-color-accent | акцент: теги, выделенное состояние |
--gm-color-positive / --gm-color-negative | прирост / падение величины |
6. Единый каркас подсказки
У ЛЮБОЙ подсказки — мементо, характеристики, пояснения к пустому слоту — есть шапка: заголовок слева, мета справа, под ними тонкий разделитель. Дальше идёт тело: теги, описание, строки «подпись — значение», определения терминов.
Правило держится API, а не дисциплиной: TooltipRequest.Plain(title, text) требует заголовок
и без него просто не создаёт запрос. Подсказка без заголовка читается как обрывок чужого текста —
игрок не понимает, к чему она относится, стоит увести взгляд от курсора.
Внизу окна живёт полоса ожидания (3 px) — у любой подсказки, без исключений. Она занимает своё место в раскладке, а не лежит поверх текста: иначе последняя строка читалась бы сквозь неё.
Ещё не сделано (ждёт контента): цвет, полужирность и подсветка внутри текста. UI Toolkit
поддерживает в рантайме <b>, <i>, <color>, <mark>, <size>, <sprite> — но rich text
не читает USS-переменные, поэтому цвета придётся отдавать из одного места как hex,
иначе палитра разъедется по текстам.
7. Термины в тексте
Ключевое слово глоссария пишется в квадратных скобках и с заглавной буквы: [Скрытность],
[Горение], [Физический урон]. Разметку расставляет KeywordMarkup из авторской записи
[kw:stealth:acc] — руками rich text в текстах не пишем.
Почему скобки, а не подчёркивание: скобки читаются как термин везде — в игре, в таблице локализации, в логе, в поиске по строкам. Подчёркивание жило только на экране и терялось всюду, где rich text не рисуется. (Решение Макса, 2026-07-26.)
8. Как менять
- Меняется вся игра → правь значение токена в
tokens.primitives.uss. - Меняется один компонент → правь его класс в
components.uss, но выбирай токен из шкалы, а не пиши пиксели. - Нужен новый кегль → сначала докажи, что шести не хватает. Седьмой размер в шкале — это седьмой способ выглядеть по-разному на соседних экранах.