Содержание урока · 10 разделов
Игра без текста долго не живёт: счёт в углу, жизни, подсказки управления, названия экранов меню. Каждая из этих надписей — это шрифт, цвет, позиция и производительность, и в Ebitengine за них отвечают разные инструменты под разные задачи. Отладочный вывод уже мелькал в уроках курса — пришла пора разобрать его границы, подключить настоящий шрифт из TTF-файла и понять, какой из двух API для текста выбрать для игры, а какой — оставить истории. Механика урока по-движковому проста: текст — это те же спрайты глифов, нарисованные поверх мира, и к моменту конца урока счёт в углу перестанет быть загадкой.
DebugPrint: отладочный шрифт и его границы#
Самый быстрый текст в движке — ebitenutil.DebugPrint из пакета ebitenutil: одна строка кода, никакой настройки, шрифт вшит в движок картинкой. Уроки про клавиатуру и примеры движка пользуются им для отладочных строк неслучайно: вывод TPS и FPS в угол экрана не требует ни одного импорта шрифтов. Но у бесплатности есть цена, и она прописана в документации дословно: DebugPrint умеет только символы из диапазона U+0000–U+00FF — базовая латиница, цифры и Latin-1. Ни кириллицы, ни акцентов, ни символа валюты.
Вторая граница — позиция и внешний вид: шрифт растровый, восьмибитный по духу, масштабируется уродливо, а у способа рисования нет ни цвета, ни размера. Для отладки это неважно — важна скорость чтения лога на экране. Для игры это уже невозможно: HUD с таким шрифтом выглядит прототипом, а русская надпись превратится в пустые квадраты. Правило разделения такое: DebugPrint — для разработки и исключительно латиницей, игровой текст — всё, что дальше по уроку.
Ebitengine текст: классический text.Draw#
Первый настоящий API — пакет text из самого движка: Ebitengine текст рисует функцией text.Draw(dst, str, face, x, y, clr), где face — реализация интерфейса font.Face стандартной библиотеки. Готовую реализацию без файлов даёт пакет golang.org/x/image/font/basicfont: Face7x13 — моноширинный растровый шрифт семь на тринадцать пикселей, самый частый гость первых проектов. Цвет передаётся аргументом color.Color, а координаты x, y указывают не на левый верхний угол надписи, а на базовую линию первой буквы — начало отсчёта глифов, о котором легко споткнуться, выравнивая текст по краю экрана.
У пакета есть два свойства, которые важно знать до написания HUD. Перенос строки работает: символ перевода строки отправляет текст на следующую строку, высота которой берётся из метрик шрифта — Metrics().Height. И второй: вызывать Draw каждый кадр безопасно для производительности — документация обещает это прямо, потому что глифы кэшируются и переиспользуются, а не пересоздаются. Но у API есть статус: с версии 2.7 пакет помечен как Deprecated — движок предлагает text/v2, и классический text.Draw остаётся рабочим способом быстро вывести латиницу, однако новым кодам прямая дорога в следующий раздел.
Классическому API нужны не только растровые лица: font.Face можно собрать и из векторного TTF через пакет golang.org/x/image/font/opentype — Parse разбирает байты шрифта в opentype.Font (это псевдоним sfnt.Font), NewFace строит font.Face с опциями размера в пунктах, DPI и хинтинга. Это старший путь до text/v2: он рабочий, но кэш и метрики лица остаётся вести самому, и для новых проектов он проигрывает следующему разделу по всем пунктам, кроме независимости от движка.
Ещё нюанс, записанный в документации пакета прямо: переданный font.Face пакет держит у себя и никогда не выпускает — известная проблема #498. Для одного-двух шрифтов на игру это неважно, но создавать лица в цикле по числу надписей не стоит: они не освободятся никогда.
Счёт через basicfont и text.Draw
GOfunc (g *Game) Draw(screen *ebiten.Image) {
text.Draw(screen,
fmt.Sprintf("Score: %d", g.score),
basicfont.Face7x13,
8, 20,
color.White)
text.Draw(screen,
"Arrows: move Space: jump",
basicfont.Face7x13,
8, 40,
color.RGBA{0x90, 0x90, 0x90, 0xff})
}Два вызова — две надписи: счёт и подсказка управления, каждая со своим цветом. Аргументы после строки — лицо шрифта, базовая линия x и y, цвет; координата y двадцать означает, что верх букв окажется чуть выше, потому что базовая линия проходит по низу глифов. Форматирование счёта — обычный fmt.Sprintf, и вызовы каждый кадр кэш глифов переживает спокойно.
Почему text.Draw можно вызывать каждый кадр без просадок FPS?
Шрифты ebiten: TTF через text/v2#
Пакет text/v2 — рекомендуемый путь с версии 2.7, и его центральное понятие — источник шрифта: text.GoTextFaceSource. Конструктор text.NewGoTextFaceSource принимает io.Reader и разбирает файл OpenType или TrueType — те самые TTF, которые лежат в папке со шрифтами любой системы. Источник разбирается один раз при старте, кэширует глифы внутри себя и живёт до конца игры. Поверх источника создаётся лицо — text.GoTextFace: обязательные поля Source и Size в пикселях, плюс необязательные Direction и Language для направления письма и языковых подсказок; одно и то же лицо можно смело создавать на каждый вызов Draw — это дешёвая маленькая структура, а кэш глифов живёт в источнике, а не в лице.
Отрисовка — text.Draw(screen, str, face, op), где op — *text.DrawOptions, embed-структура: внутри ebiten.DrawImageOptions с GeoM и ColorScale плюс LayoutOptions с межстрочным расстоянием LineSpacing. Отличие в позиционировании стоит запомнить сразу: v2 кладёт в точку перевода левый верхний угол области текста, а не базовую линию, как классический пакет — перенесённый код со старыми координатами уедет вверх. Сам файл шрифта вшивается в бинарник через embed, как картинки четвёртого урока, и игра не зависит от шрифтов, установленных в системе игрока.
TTF-шрифт из embed через text/v2
GO//go:embed assets/fonts/game.ttf
var fontData []byte
var uiFont *text.GoTextFaceSource
func init() {
src, err := text.NewGoTextFaceSource(
bytes.NewReader(fontData))
if err != nil {
log.Fatal(err)
}
uiFont = src
}
func (g *Game) Draw(screen *ebiten.Image) {
op := &text.DrawOptions{}
op.GeoM.Translate(8, 8)
op.LineSpacing = 28 // расстояние между строками
text.Draw(screen, "Score: 1200
Lives: 3",
&text.GoTextFace{Source: uiFont, Size: 24}, op)
}Шрифт читается из embed один раз в init: источник парсит TTF и остаётся жить глобальной переменной. DrawOptions сочетает перенос, цвет через ColorScale и межстрочный интервал — без LineSpacing вторая строка ляжет вплотную, потому что по умолчанию интервал нулевой. Лицо собирается на месте вызова: источник плюс размер, и для другого размера достаточно поменять одно число.
Зачем в text/v2 источнику шрифта нужна обёртка GoTextFace, а не рисовать прямо им?
Классический text против text/v2#
| Свойство | text (v1) | text/v2 |
|---|---|---|
| Статус | Deprecated с версии 2.7 | рекомендуемый API |
| Источник шрифта | font.Face: basicfont, sfnt | GoTextFaceSource из TTF-файла |
| Позиция точки | базовая линия первой буквы | левый верх области текста |
| Цвет | аргумент clr в вызове | ColorScale в DrawOptions |
| Многострочность | перенос с высотой Metrics | перенос с LineSpacing |
| Размер | зашит в лицо шрифта | число в GoTextFace, любое |
| Кириллица | нет у растровых лиц | есть у TTF с нужными глифами |
Что произойдёт с переносом строки в v2?
Прочитайте код и выберите, что он выведет:
op := &text.DrawOptions{}
op.GeoM.Translate(8, 8)
// LineSpacing не задан — остался нулевым
text.Draw(screen, "HP: 3
Score: 900",
&text.GoTextFace{Source: src, Size: 24}, op)Самый обидный момент первого HUD: надпись «Очки: 10» через DebugPrint или basicfont вместо букв рисует пустые места — в растровых шрифтах просто нет кириллических глифов. Код при этом абсолютно корректен: движок честно рисует то, что нашёл. Что сломалось бы дальше: локализация невозможна в принципе, а замена шрифта на ходу ломает вёрстку HUD. Решение — TTF с кириллицей через text/v2; выбирая декоративный пиксельный шрифт, проверяйте таблицу символов до того, как вписали его в двадцать экранов.
Цвет, размер и Measure для аккуратного HUD#
Цвет в text/v2 задаётся ColorScale из опций отрисовки — тем же механизмом, что тонирует спрайты: op.ColorScale.ScaleWithColor(color.RGBA{...}) перекрашивает белые глифы шрифта. Размер — поле Size лица, и здесь у кэша есть характер: картинки глифов создаются для каждого размера отдельно, поэтому плавно растущий текст — дюжина размеров за секунду — завалит кэш пересозданиями. Размер меняйте дискретно: двадцать четыре, тридцать два, сорок восемь, как уровни заголовков в интерфейсе, а не интерполяцией каждый кадр.
Для выравнивания пакет даёт измерение: text.Measure(str, face, lineSpacing) возвращает ширину и высоту области, которую займёт текст, а text.Advance — ширину одной строки; с версии 2.10 Advance помечен как Deprecated в пользу AdvanceAt для позиционирования каретки, а Measure остаётся основным инструментом вёрстки. Центрированный заголовок меню — это Measure плюс пара делений: x := (viewW - w) / 2. Для прогрева кэша перед первым кадром есть text.CacheGlyphs: вызвать его на старте со всеми строками интерфейса — и первый кадр с текстом не заплатит за создание глифов. Только помните, что функция тяжёлая: она создаёт все подпиксельные варианты глифов сразу, и в обычной игре Draw создаёт и кэширует глифы сам — прогрев оправдан, когда первый кадр критичен. Measure работает по данным шрифта и не рисует ничего: три вызова в кадре дешевле одной пробной отрисовки ради ширины.
Правила текста одним списком#
- DebugPrint — отладка и латиница до U+00FF, игровой текст — не он
- Классический text.Draw Deprecated с 2.7: годится для быстрого прототипа
- text/v2: источник GoTextFaceSource разбирает TTF один раз, лица создаются поверх
- Позиция v2 — левый верх области текста, не базовая линия, как в v1
- LineSpacing обязателен для многострочности: по умолчанию он нулевой
- Цвет — ColorScale, размер — поле Size; размеры меняйте дискретно
- Measure и Advance выравнивают текст без пробных отрисовок
Соберите подключение TTF
Восстановите три пропуска: тип источника, тип опций и поле размера.
Рисовать глифы можно только в локальном Go с окном из уроков курса, но измерения — чистая математика шрифта. Тематическая программа в песочнице сайта печатает ширину строк по заданному размеру и считает центровку: подставьте свою ширину окна и длину заголовка, чтобы проверить арифметику Measure до запуска проекта.
Подключение шрифта по шагам
Соберите путь TTF-шрифта до вызова Draw.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
HUD: где текст живёт в игре#
С текстом на месте HUD перестаёт быть абстракцией: счёт и жизни — это text.Draw поверх мира, и рисуется он в Draw после всего остального. Камера из прошлого урока HUD не двигает: интерфейс живёт в экранных координатах и рисуется последним, поверх сдвинутого мира — порядок вызовов внутри Draw решает всё. Счёт и жизни — состояние игры, и их отображение лучше держать рядом с хранением: урок про игровое состояние покажет, как данные HUD устроены в структуре, не позволяя тексту и логике сплетаться.
Меню и пауза — следующий уровень применения: заголовок по центру через Measure, пункты выбора, подсказки клавиш — весь интерфейс меню строится тем же вызовом Draw, что и счёт. Сцены и их переходы разберёт ближайший урок, а пока зафиксируйте ещё одно правило: шрифт — ассет с лицензией. Шрифты из примеров движка — M+ 1p, Press Start 2P, Terminus TTF — поставляются на свободных лицензиях, и правила их использования соберёт урок про ассеты и лицензии: лицензия шрифта читается так же внимательно, как лицензия спрайтов.
Текст встал на место третьего слоя игры: мир рисует тайлмапу, спрайты — героев, а поверх всего — строки HUD. Три инструмента урока делят работу честно: DebugPrint — отладка, классический text.Draw — быстрый прототип с basicfont, text/v2 — всё настоящее с TTF и кириллицей. Домашнее задание проведёт вас по всем трём и закончится шрифтом в бинарнике: игра перестанет зависеть от шрифтов системы и получит собственный голос, одинаковый на всех платформах. Когда счёт в углу оживёт, останется одна ступень до настоящей игры — меню и пауза между сценами, и это тема следующего урока.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Отладочный DebugPrint умеет только латиницу до U+00FF и годится исключительно для разработки.
- Классический text.Draw с basicfont.Face7x13 Deprecated с версии 2.7, но остаётся быстрым путём к первой надписи.
- text/v2: источник GoTextFaceSource парсит TTF один раз, лица GoTextFace добавляют размер и создаются дёшево.
- Позиция text/v2 — левый верх области текста, а не базовая линия; LineSpacing для переносов обязателен.
- Цвет задаётся ColorScale, размеры меняются дискретно — плавный рост размера перебивает кэш глифов.
- HUD рисуется после мира в экранных координатах, и камере его двигать нельзя.
Домашнее задание
Выведите счёт тремя способами подряд: DebugPrint, text.Draw с basicfont и text/v2 с TTF — сравните вид и запишите, какая надпись выглядит игровой. Попробуйте написать по-русски во всех трёх и зафиксируйте, где появились пустые места. Подключите шрифт из примеров движка или любой свободный TTF с кириллицей через embed и NewGoTextFaceSource, выведите двухстрочный HUD с LineSpacing. Отцентрируйте заголовок через Measure по ширине окна и проверьте формулу на трёх длинах строки. В конце добавьте прогрев text.CacheGlyphs на старте и сравните первый кадр с текстом и без — разница покажет, зачем нужен прогрев.
Личные заметкиПусто
Вопросы и ответы по уроку
Какой API выбрать, если проект уже начат на классическом text.Draw?
Для прототипа — никакого срочного перехода: Deprecated не значит сломан, пакет работает. Но новый код выгоднее писать сразу на text/v2: кириллица, размеры числом, перенос с LineSpacing и позиция левого верхнего угла проще в вёрстке. При переносе помните про смену точки отсчёта — базовая линия станет верхом, и весь текст уедет на высоту шрифта вверх.
Откуда взять шрифт с кириллицей и пиксельной эстетикой?
В примерах движка лежат M+ 1p Regular, Press Start 2P и Terminus TTF — их можно посмотреть как ориентир и проверить лицензии в license.md рядом. Геймдев-шрифты ищите на площадках свободных шрифтов, и обязательно проверяйте таблицу символов: у пиксельных шрифтов кириллица бывает не во всех версиях. Лицензионные правила соберёт урок про ассеты.
Текст рисуется с рваными краями на масштабированном окне — что настроить?
Растровые шрифт и масштаб плохи в паре: масштаб окна из Layout растягивает пиксели глифов. Первое средство — TTF-шрифт через text/v2: он векторный и рендерится под текущий масштаб. Второе — рисовать текст в целых координатах: дробные позиции дают размытие даже у векторного шрифта.
Термины курса
Понятия, на которых строится этот курс, — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылок на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript