Содержание урока · 10 разделов
- ebitengine окно: три размера вместо одного
- Три размера одной игры
- ebiten layout: контракт логического холста
- Масштаб: как 320 на 240 занимает весь экран
- Изменяемое окно и полноэкранный режим
- DPI и Retina: device scale factor
- Правила холста одним списком
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Разберём типичную сцену растерянности. Вы написали игру под холст 320 на 240, запустили — и окно внезапно 640 на 480. Потом потянули за уголок, и игра либо растянулась, либо осталась прежней — в зависимости от того, что вы вернули в Layout. Что вообще рисует Draw: окно или холст? Если под тем же вопросом вы открывали GUI-библиотеки вроде Fyne, там логика похожая — но у игры, в отличие от формы с кнопками, размер холста часто осознанно фиксированный, и это порождает слой масштабирования, которого в GUI нет. Сегодня мы раскладываем всё по полочкам: три размера, которые живут одновременно, контракт Layout и практические рецепты — от изменяемого окна до полноэкранного режима и DPI.
ebitengine окно: три размера вместо одного#
Первый шаг к порядку в голове — разделить три величины, которые в разговоре все называют словом «размер». Первый — размер окна операционной системы в device-independent пикселях: именно его движок передаёт в Layout как outsideWidth и outsideHeight. Второй — логический размер холста вашей игры: то, что Layout возвращает, и то, в каких координатах живёт ваш Draw. Третий — физические пиксели монитора, которых на HiDPI-экране в два или полтора раза больше, чем логических точек; ими движок занимается сам. Документация формулирует контракт аккуратно: Layout принимает внешний размер в device-independent пикселях и возвращает логический размер экрана; масштаб от холста к итоговому окну движок подбирает автоматически.
Зачем нужен этот средний слой — логический холст? Затем же, зачем художник рисует кадр фиксированного формата: геймплей не должен зависеть от того, развернул ли игрок окно на весь 4K-монитор или запустил в уголке ноутбука. Вы проектируете игру в своих координатах, а проблему «как это вписать в произвольное окно» решает движок. Плата за удобство — необходимость понимать, что screen в Draw — это не окно ОС: когда вы захотите узнать «размер экрана», сначала решите, какой из трёх размеров вам на самом деле нужен.
Три размера одной игры#
| Размер | Где живёт | Кто им управляет |
|---|---|---|
| Окно ОС (outside) | device-independent пиксели окна или монитора | пользователь и SetWindowSize; движок передаёт в Layout |
| Логический холст | координаты Draw и всей игровой логики | ваш Layout — фикс или расчёт от outside |
| Физические пиксели | реальная матрица монитора | движок через device scale factor |
ebiten layout: контракт логического холста#
Layout — самый короткий метод интерфейса ebiten.Game, и в нём больше решений, чем кажется. Два полюса. Полюс первый — фиксированный холст: return 320, 240 в любом случае, что угодно творится с окном. Это выбор пиксель-арт игр: пропорции стабильны, масштаб предсказуем, координаты никогда не меняются. Полюс второй — резиновый холст: return outsideWidth, outsideHeight, и тогда ваша игра получает в Draw столько же места, сколько занимает окно. Так делают стратегии и головоломки с интерфейсом, который должен растягиваться вместе с окном.
Между полюсами есть и компромиссы: возвращать outside, поджатый под минимальную ширину, или фиксированную высоту с плавающей шириной. Два технических факта завершают контракт. Layout вызывается почти каждый кадр — это не одноразовая настройка, поэтому тяжёлые вычисления внутри него дурной тон. А возврат неположительных чисел недопустим: документация предупреждает, что вызывающий код может запаниковать, — холста размером ноль не бывает. Из тех же документов следует и гарантия первого кадра: Layout вызывается до первого Update, поэтому даже расчётный холст уже знает свои размеры, когда логика делает первый шаг. Обратите внимание и на сигнатуру: из трёх методов интерфейса только Layout не возвращает ошибку — ему нечего сообщать о сбое, он описывает мир, а не выполняет рискованную работу. Для тонких случаев с дробными размерами существует и float-версия интерфейса LayoutF: она подключается отдельным интерфейсом LayoutFer, о котором знать достаточно, пока вы не упёрлись в полупиксельные границы.
Два полюса Layout в одном файле
GOtype Game struct{}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
// Пиксель-арт: холст всегда один и тот же
return 320, 240
}
func (g *Editor) Layout(outsideWidth, outsideHeight int) (int, int) {
// Инструмент уровня: холст следует за окном
return outsideWidth, outsideHeight
}Обе реализации легальны: движку важен сам контракт, а не конкретная стратегия. Фиксированный возврат оставляет геймплей стабильным при любом окне, возврат outside делает холст равным окну в device-independent пикселях. В проекте вы выбираете одну стратегию для одной игры, а не держите две структуры — здесь они показаны рядом для контраста.
Что означает аргумент outsideWidth в Layout?
Масштаб: как 320 на 240 занимает весь экран#
Что происходит между холстом и окном? Движок считает масштаб: во сколько раз логические пиксели вырастают до размеров окна. Окно 640 на 480 при холсте 320 на 240 даёт масштаб ровно два — и это идеальный случай для пиксель-арта: каждый логический пиксель становится аккуратным квадратом два на два. Стремитесь к таким целым множителям: делайте окно кратным холсту через SetWindowSize или холст кратным ожидаемому окну. Когда пропорции не совпадают — скажем, окно 640 на 400 при холсте 320 на 240, — движок вписывает холст в окно, сохраняя пропорции картинки, и лишнее место окна остаётся незанятым.
Нецелый масштаб — тихий враг пиксель-арта. При множителе 1,5 один логический пиксель то занимает два физических, то один, и на экране появляются пиксели разной толщины — «грязная» картинка, в которой художник узнаёт свой арт с трудом. Движок при этом ничего не предупредит: он честно отрисует дробные пиксели, и заметить размытость можно только глазами на реальном мониторе — скриншоты, пересжатые браузером, артефакт легко скрывают. Отсюда практическое правило ретро-игр: выбирайте холст так, чтобы он делил типовые окна без остатка, 320 на 240 и 640 на 360 — проверенные классики. А если окно меняет размер, движок пересчитывает масштаб на лету: Layout вызывается почти каждый кадр именно поэтому.
Какой масштаб получится?
Прочитайте код и выберите, что он выведет:
func main() {
ebiten.SetWindowSize(960, 720)
ebiten.SetWindowTitle("Scale test")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
func (g *Game) Layout(w, h int) (int, int) {
return 320, 240
}Изменяемое окно и полноэкранный режим#
По умолчанию окно Ebitengine имеет фиксированный размер: пользователь не может его растянуть мышью. Разрешение даёт ebiten.SetWindowResizingMode: режим WindowResizingModeEnabled разрешает менять размер за рамку, режим по умолчанию WindowResizingModeDisabled запрещает, а WindowResizingModeOnlyFullscreenEnabled разрешает только переключение в полноэкранный режим — последний работает, как честно говорит документация, пока только на macOS. Включённый ресайз не отменяет ваш Layout: вы по-прежнему решаете, холст ли следует за окном, или окно вписывает фиксированный холст. Отключить рамку окна целиком умеет отдельная функция SetWindowDecorated, а лимиты min и max для ресайза ставит SetWindowSizeLimits.
Полноэкранный режим — пара IsFullscreen и SetFullscreen: вторая принимает true или false и переключает режим в любой момент, хоть из Update по нажатию F11. Здесь спрятана ловушка, о которой документация предупреждает прямо: WindowSize продолжает возвращать исходный размер окна даже в фулскрине — размеры монитора дают Monitor().Size(), а целевое разрешение для честного фулскрина считает ScreenSizeInFullscreen. Если игра должна заполнять весь экран без полей, спросите ScreenSizeInFullscreen и подстройте Layout под неё — это ровно тот случай, когда Layout стоит сделать расчётным, а не фиксированным.
Окно, которое слушается пользователя
GOfunc main() {
ebiten.SetWindowSize(960, 720)
ebiten.SetWindowTitle("Resizable game")
// разрешаем тянуть окно за рамку
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
func (g *Game) Update() error {
if ebiten.IsKeyPressed(ebiten.KeyF) {
ebiten.SetFullscreen(!ebiten.IsFullscreen())
}
return nil
}Пакетные Set-функции вызывают до RunGame — это настройка окружения; но SetFullscreen и IsFullscreen безопасно дёргать и в живом цикле, здесь — по клавише F. Каркас Layout из первого куска не изменился: холст 320 на 240 впишется и в растянутое окно, и в монитор фулскрина. Заметьте, что переключение фулскрина не требует перезапуска игры.
Игра в полноэкранном режиме. Что вернёт ebiten.WindowSize()?
DPI и Retina: device scale factor#
Последний слой — плотность пикселей. На Retina-экране одна логическая точка занимает два физических пикселя, и это отношение называется device scale factor. Движок применяет его автоматически: картинка рендерится в полную физическую плотность, и на 2x-экране выглядит резко. Спросить текущий множитель у кода можно через Monitor().DeviceScaleFactor(); историческая пакетная функция ebiten.DeviceScaleFactor считается устаревшей с версии 2.6 — в старых туториалах вы встретите именно её, так что знайте обе.
Зачем этот множитель вручную? Для режима повышенной чёткости пиксель-арт-игр. По умолчанию резкий пиксель-арт на Retina может выглядеть замыленным: движок масштабирует холст с учётом дробных множителей окна. Официальный рецепт из FAQ: пусть Layout вернёт outside, умноженный на device scale factor, — тогда холст совпадёт с физическими пикселями, и картинка станет пиксель-в-пиксель; готовый пример так и называется highdpi. На мониторах без HiDPI множитель просто равен единице — рецепт от этого не ломается, просто ничего не умножает. Плата за высшую чёткость — больше работы на кадр, поэтому в браузерных сборках есть и обратная опция DisableHiDPI, отключающая HiDPI-рендер ради производительности. Решайте по своей игре: арты с крупным пикселем обычно выигрышнее выглядят с честным масштабом и целыми множителями.
Совет «верните outsideWidth и outsideHeight» для пиксель-арт игры — ловушка: на Retina-ноутбуке холст вырастает в разы, спрайты усыхают до крошки, а координаты уровней, рассчитанные под 320 на 240, разъезжаются. Фиксированный Layout нужен не потому, что лень считать, а потому, что дизайн игры жёстко привязан к формату кадра. Резиновый холст оправдан, когда интерфейс векторный и адаптивный. Проверка простая: поставьте в Layout печать outsideWidth в лог и посмотрите, как часто она срабатывает при ресайзе — после этого вопрос «почему у меня тормозит Layout» решается сам.
Что произойдёт, если Layout вернёт 0 и 0?
Правила холста одним списком#
- Пиксель-арт — фиксированный Layout, кратный типовым окнам: 320 на 240, 640 на 360
- Интерфейсные и текстовые игры — возврат outsideWidth и outsideHeight
- Полноэкранные режимы — расчёт от ScreenSizeInFullscreen, а не от WindowSize
- Резкость на Retina — hi-DPI рецепт с device scale factor из примера highdpi
- Никогда не возвращайте ноль и отрицательные числа — вызывающий код может запаниковать
- Тяжёлых вычислений в Layout быть не должно: он вызывается почти каждый кадр
Разделы этого урока проверяются только локально: окно создаёт операционная система, а браузерной песочнице такие полномочия недоступны. Зато математика масштаба — деление окна на холст, целые и дробные множители — считается на чистом Go: тематическая программа в песочнице сайта прокручивает те же числа, что считает движок при вписывании холста в окно.
Соберите настройку окна
Восстановите три пропуска: режим ресайза и резиновый Layout.
Запуск с изменяемым окном
Расставьте строки программы с ресайзом в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Резюме слоя отображения такое: окно ОС живёт в device-independent пикселях, Layout превращает его в логический холст, движок масштабирует холст на монитор с учётом DPI, а вы выбираете между фиксированным и резиновым холстом сознательно, а не по привычке. Приятное следствие контракта: значения из Layout можно менять на ходу — захотите переключить пиксель-арт режим на интерфейсный, возвращайте другие числа, и движок подстроится без перезапуска. Проверьте рецепт на практике: локальный проект из урока про установку и цикл запустите трижды — с фиксированным холстом, с резиновым и с переключателем фулскрина по клавише. На следующем шаге пустой холст перестанет быть пустым: четвёртый урок загружает PNG с прозрачностью и рисует первый спрайт через DrawImage (04-sprajty), а там недалеко и до позиций с пивотами в уроке про прямоугольники (08-pryamougolniki). Если хотите параллельно сравнить, как тот же вопрос «кто кого масштабирует» решается в GUI-мире, у Fyne свой ответ через типовые размеры — курс Fyne хорош как противовес.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Размеров три: окно ОС в device-independent пикселях, логический холст из Layout, физические пиксели монитора.
- Layout принимает outside и возвращает логический размер; вызывается почти каждый кадр; неположительный возврат недопустим.
- Движок сам считает масштаб холста к окну и вписывает картинку с сохранением пропорций; целые множители — признак чистого пиксель-арта.
- Ресайз включает SetWindowResizingMode(Enabled); фулскрин переключают SetFullscreen и IsFullscreen в любой момент игры.
- WindowSize в фулскрине возвращает исходное окно; монитор даёт Monitor().Size(), разрешение фулскрина — ScreenSizeInFullscreen.
- HiDPI рендерится автоматически; рецепт пиксель-в-пиксель — Layout, умноженный на device scale factor, как в примере highdpi из FAQ.
Домашнее задание
Возьмите каркас из второго урока и соберите лабораторию масштаба: Layout с фиксированным 320 на 240, окно 960 на 720, ресайз включён. Потяните окно до нецелого множителя и посмотрите на края пикселей; затем добавьте переключатель фулскрина по клавише F через SetFullscreen. После этого реализуйте hi-DPI рецепт из FAQ: умножайте outside на Monitor().DeviceScaleFactor() в Layout и сравните резкость. Если под рукой есть Mac, проверьте и WindowResizingModeOnlyFullscreenEnabled: на других системах движок честно трактует его как Disabled. Выпишите в заметки, для какой из ваших будущих игр какой Layout подходит и почему.
Личные заметкиПусто
Вопросы и ответы по уроку
Какой холст выбрать для первой пиксель-арт игры?
Классика — 320 на 240 или 640 на 360: они дают целые множители на типовых окнах и мониторах. Начните с фиксированного Layout и окна, кратного холсту, а резиновый холст оставьте для интерфейсных игр без пиксельного стиля. Проверить себя просто: холст 640 на 360 при окне 960 на 720 даёт нецелый множитель 1,5 и неровные пиксели, а при 1280 на 720 — целые двойки.
Почему на Retina игра выглядит мыльной?
Движок рендерит в физическую плотность, но дробные множители окна размывают пиксели при масштабировании. Сработает hi-DPI рецепт: верните из Layout outside, умноженный на device scale factor, и добавьте целые множители для окна. Если арты нарисованы под крупный пиксель, лёгкое мыло иногда даже нравится — правило целых множителей не догма, а инструмент.
Можно ли менять размер окна из кода во время игры?
Да: SetWindowSize работает в любой момент и остаётся актуальным при выходе из фулскрина. Но делайте это осмысленно — игрок вправе держать окно своего размера, а резкое переключение размеров из логики обычно раздражает.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript