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

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

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

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

Окно, холст и Layout

У ebitengine окно и холст — не одно и то же, и смешивать их начинают почти все новички. В этом уроке разберём ebiten layout: как движок превращает ваши 320 на 240 в любое окно, что происходит при растягивании, полноэкранном режиме и на Retina-экранах, где физические пиксели не совпадают с логическими.

Содержание урока · 10 разделов

Разберём типичную сцену растерянности. Вы написали игру под холст 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 в одном файле

GO
type 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 стоит сделать расчётным, а не фиксированным.

Окно, которое слушается пользователя

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

Что произойдёт, если Layout вернёт 0 и 0?

Правила холста одним списком#

  • Пиксель-арт — фиксированный Layout, кратный типовым окнам: 320 на 240, 640 на 360
  • Интерфейсные и текстовые игры — возврат outsideWidth и outsideHeight
  • Полноэкранные режимы — расчёт от ScreenSizeInFullscreen, а не от WindowSize
  • Резкость на Retina — hi-DPI рецепт с device scale factor из примера highdpi
  • Никогда не возвращайте ноль и отрицательные числа — вызывающий код может запаниковать
  • Тяжёлых вычислений в Layout быть не должно: он вызывается почти каждый кадр

Соберите настройку окна

Восстановите три пропуска: режим ресайза и резиновый Layout.

func main() { ebiten.SetWindowSize(960, 720) ebiten.SetWindowTitle("Resizable game") ebiten.SetWindowResizingMode(ebiten.WindowResizingMode) if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return , }

Запуск с изменяемым окном

Расставьте строки программы с ресайзом в правильном порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Резюме слоя отображения такое: окно ОС живёт в device-independent пикселях, Layout превращает его в логический холст, движок масштабирует холст на монитор с учётом DPI, а вы выбираете между фиксированным и резиновым холстом сознательно, а не по привычке. Приятное следствие контракта: значения из Layout можно менять на ходу — захотите переключить пиксель-арт режим на интерфейсный, возвращайте другие числа, и движок подстроится без перезапуска. Проверьте рецепт на практике: локальный проект из урока про установку и цикл запустите трижды — с фиксированным холстом, с резиновым и с переключателем фулскрина по клавише. На следующем шаге пустой холст перестанет быть пустым: четвёртый урок загружает PNG с прозрачностью и рисует первый спрайт через DrawImage (04-sprajty), а там недалеко и до позиций с пивотами в уроке про прямоугольники (08-pryamougolniki). Если хотите параллельно сравнить, как тот же вопрос «кто кого масштабирует» решается в GUI-мире, у Fyne свой ответ через типовые размеры — курс Fyne хорош как противовес.

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

Термин

Логический холст

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

Размер, который возвращает Layout: в его координатах живёт Draw и вся игровая логика, независимо от окна.

1 / 6

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

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

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

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

Какой холст выбрать для первой пиксель-арт игры?

Классика — 320 на 240 или 640 на 360: они дают целые множители на типовых окнах и мониторах. Начните с фиксированного Layout и окна, кратного холсту, а резиновый холст оставьте для интерфейсных игр без пиксельного стиля. Проверить себя просто: холст 640 на 360 при окне 960 на 720 даёт нецелый множитель 1,5 и неровные пиксели, а при 1280 на 720 — целые двойки.

Почему на Retina игра выглядит мыльной?

Движок рендерит в физическую плотность, но дробные множители окна размывают пиксели при масштабировании. Сработает hi-DPI рецепт: верните из Layout outside, умноженный на device scale factor, и добавьте целые множители для окна. Если арты нарисованы под крупный пиксель, лёгкое мыло иногда даже нравится — правило целых множителей не догма, а инструмент.

Можно ли менять размер окна из кода во время игры?

Да: SetWindowSize работает в любой момент и остаётся актуальным при выходе из фулскрина. Но делайте это осмысленно — игрок вправе держать окно своего размера, а резкое переключение размеров из логики обычно раздражает.

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

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

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

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

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