Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~16 мин +50 XP

Текст и шрифты

Ebitengine текст для HUD, счёта и меню ставит перед вами выбор из трёх инструментов: отладочный DebugPrint, классический text.Draw с растровым basicfont и современный text/v2 с TTF-шрифтами. Разберём шрифты ebiten по всем трём: где какой уместен, чем они отличаются и почему кириллица не влезает в первые два.

Содержание урока · 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

GO
func (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, sfntGoTextFaceSource из 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)

Цвет, размер и 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

Восстановите три пропуска: тип источника, тип опций и поле размера.

var src *text. func init() { s, err := text.NewGoTextFaceSource(bytes.NewReader(fontData)) if err != nil { log.Fatal(err) } src = s } op := &text.{} text.Draw(screen, "Score: 900", &text.GoTextFace{Source: src, : 24}, op)

Подключение шрифта по шагам

Соберите путь 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 и кириллицей. Домашнее задание проведёт вас по всем трём и закончится шрифтом в бинарнике: игра перестанет зависеть от шрифтов системы и получит собственный голос, одинаковый на всех платформах. Когда счёт в углу оживёт, останется одна ступень до настоящей игры — меню и пауза между сценами, и это тема следующего урока.

Термины урока

Термин

ebitenutil.DebugPrint

нажмите, чтобы перевернуть
Определение

Отладочный вывод вшитым растровым шрифтом: только символы U+0000–U+00FF, без цвета и размера. Для разработки, не для игры.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 на старте и сравните первый кадр с текстом и без — разница покажет, зачем нужен прогрев.

Личные заметкиПусто
Хранятся только в вашем браузере (Ebitengine · Текст и шрифты)

Вопросы и ответы по уроку

Какой API выбрать, если проект уже начат на классическом text.Draw?

Для прототипа — никакого срочного перехода: Deprecated не значит сломан, пакет работает. Но новый код выгоднее писать сразу на text/v2: кириллица, размеры числом, перенос с LineSpacing и позиция левого верхнего угла проще в вёрстке. При переносе помните про смену точки отсчёта — базовая линия станет верхом, и весь текст уедет на высоту шрифта вверх.

Откуда взять шрифт с кириллицей и пиксельной эстетикой?

В примерах движка лежат M+ 1p Regular, Press Start 2P и Terminus TTF — их можно посмотреть как ориентир и проверить лицензии в license.md рядом. Геймдев-шрифты ищите на площадках свободных шрифтов, и обязательно проверяйте таблицу символов: у пиксельных шрифтов кириллица бывает не во всех версиях. Лицензионные правила соберёт урок про ассеты.

Текст рисуется с рваными краями на масштабированном окне — что настроить?

Растровые шрифт и масштаб плохи в паре: масштаб окна из Layout растягивает пиксели глифов. Первое средство — TTF-шрифт через text/v2: он векторный и рендерится под текущий масштаб. Второе — рисовать текст в целых координатах: дробные позиции дают размытие даже у векторного шрифта.

Термины курса

Понятия, на которых строится этот курс, — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылок на практику.

Следующая ступень учебного пути

Chromedp25 уроков~11 чпрактика25 уроков уже доступны

Chromedp в Go: автоматизация Chrome без Selenium

Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.

  • Запускать Chrome из Go и управлять страницами
  • Находить элементы селекторами и ждать их появления
  • Кликать, заполнять формы и извлекать данные
  • Делать скриншоты, PDF и выполнять JavaScript