Содержание урока · 10 разделов
- Пивот спрайта: угол, центр или ноги
- Спрайт координаты против прямоугольника: Bounds и image.Rect
- Поворот вокруг пивота: порядок сдвигов
- Границы холста: прижим и осмысленные вылеты
- Какой пивот выбрать: справочная таблица
- Кадр целиком: слои, порядок и один источник правды
- Правила пивотов и границ одним списком
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
На четвёртом уроке появился первый спрайт, на пятом он поехал, но в одном вопросе мы всё это время жили на честном слове: что именно означает «позиция спрайта». Поставить героя в (100, 100) через Translate — и что окажется в этой точке: угол картинки, её центр, ноги? Пока игра — один спрайт на пустом холсте, ответ не важен. Но как только появляются пули из ствола, взрывы под ногами и падения на землю, размытость превращается в источник постоянных сдвигов на глаз. Дальше по курсу каждый новый объект — пуля, взрыв, кнопка интерфейса — будет спрашивать ту же точку, и договорённость окупится с каждым из них. В этом уроке мы фиксируем договорённость, которую знает любой 2D-движок, даже если не проговаривает: у каждого спрайта есть точка привязки — пивот, — и выбор пивота — это проектное решение, а не случайность дефолта. После него позиции героев, эффектов и интерфейса перестают спорить между собой.
Пивот спрайта: угол, центр или ноги#
Пивот — это точка спрайта, которая встанет в координаты, переданные Translate. Дефолт DrawImage с нулевыми опциями кладёт в (0, 0) левый верхний угол картинки — значит, без дополнительных сдвигов пивотом служит именно угол: Translate(100, 50) ставит в (100, 50) верхний левый пиксель героя, а всё его тело растёт вправо и вниз. Для фоновых слоёв и плиток это удобно и оставить стоит. Для всего остального пивот выбирается сознательно, и делается это не опцией движка, а вашим сдвигом матрицы: перед позицией вызывается второй Translate с минус-частью размеров. Сдвиг на минус половину ширины и высоты переносит пивот в центр картинки — так рисуют пули, взрывы и прицелы, у которых середина и есть смысловая точка.
Третий классический пивот — ноги, нижний центр картинки. Платформенный герой живёт точкой стоп: (x, y) — это место, где он стоит на земле, и тогда прыжок поднимает спрайт, падение опускает до земли, а сами ноги не утопают в платформе на половину роста. Смена пивота — решение одного разряда с выбором единиц скорости из пятого урока: принимается один раз на тип объекта и дальше держится всей игрой. Переменный пивот «иногда угол, иногда центр» — верный способ вернуть хаос обратно, и если в проекте хочется именно так, почти всегда правильнее завести две функции отрисовки с понятными именами, чем один вызов с флагом.
Рисуем от центра: сдвиг пивота матрицей
GO// drawAtCenter рисует img так, что центр картинки
// оказывается в точке (cx, cy)
func drawAtCenter(screen, img *ebiten.Image, cx, cy float64) {
w := float64(img.Bounds().Dx())
h := float64(img.Bounds().Dy())
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-w/2, -h/2) // пивот: из угла в центр
op.GeoM.Translate(cx, cy) // позиция центра на холсте
screen.DrawImage(img, op)
}Bounds даёт прямоугольник картинки, Dx и Dy — ширину и высоту из него. Первый Translate переносит пивот из угла в центр, второй ставит центр в целевую точку. Порядок важен: матрица накапливает сдвиги, как показывал predict четвёртого урока, — поменяйте вызовы местами, и смысл сдвигов изменится.
Герой — спрайт 32×48 с пивотом «ноги» (нижний центр). Какой первый сдвиг GeoM переносит пивот из угла в ноги?
Спрайт координаты против прямоугольника: Bounds и image.Rect#
Пивот решает вопрос отрисовки, но у логики прямоугольная природа: попадание пули, клик по кнопке, столкновение со стеной — всё это проверки области, а не точки. Прямоугольник спрайта собирается из стандартной библиотеки: у ebiten.Image есть Bounds, возвращающий image.Rectangle с полями Min и Max, а из Bounds().Size() выходят ширина и высота в image.Point с полями X и Y. Для героя прямоугольник строится из позиции и размеров вызовом image.Rect(x0, y0, x1, y1) — левого верхнего и правого нижнего углов. Функция прощает перепутанные углы: если передать координаты наоборот, она молча их упорядочит, и прямоугольник всё равно будет корректным — на этом основаны короткие вычисления вроде image.Rect(x-w/2, y-h, x+w/2, y).
Отдельное правило о типах: позиция героя живёт в float64 — урок про движение доказал, что целые координаты тихо едят шаги, — а image.Rect принимает только int. Значит, приведение int(g.x) случается в момент сборки прямоугольника для проверок, и только там: усечение долей пикселя безвредно для хит-теста, но не для накопления движения. Точка против области решается симметрично: попадание курсора в предмет — метод pt.In(rect) у image.Point, причём для точки на границе он вернёт true, если это левый или верхний край: In проверяет полуинтервал [Min, Max) по обеим осям, и левый верхний угол прямоугольника сам себе принадлежит. Пересечение двух областей — тема следующего урока про AABB-коллизии. И заметка на полях: у прямоугольника есть Size, Dx и Dy — держите их в привычке, они режут арифметику границ пополам.
heroRect: прямоугольник из пивота «ноги»
GO// heroRect собирает прямоугольник героя из позиции
// с пивотом «ноги» — точка (x, y) это точка стоп
func (g *Game) heroRect() image.Rectangle {
w := hero.Bounds().Dx()
h := hero.Bounds().Dy()
x, y := int(g.x), int(g.y)
return image.Rect(x-w/2, y-h, x+w/2, y)
}Прямоугольник повторяет тот же пивот, что и отрисовка: минус половина ширины по горизонтали, минус вся высота по вертикали, низ — в точке стоп. heroRect нужен логике: попаданиям, столкновениям, границам. Одна функция сборки на объект гарантирует, что картинка и её прямоугольник никогда не разъедутся.
Поворот вокруг пивота: порядок сдвигов#
Пивот — это не только про позицию: вокруг него же спрайт вращается и масштабируется. GeoM вращает вокруг начала координат, поэтому поворот вокруг центра собирается из трёх вызовов: минус-сдвиг переносит центр картинки в начало координат, Rotate крутит вокруг него, последний Translate ставит точку на место. Официальный пример rotate из репозитория движка делает ровно это — с комментарием, что при применении матриц начало координат для картинки это её левый верхний угол. Порядок вызовов менять нельзя: матрица применяется к точкам в порядке вызовов, и переставленные сдвиги дают вращение вокруг чужой точки — та же логика порядка, что в отрисовке от центра выше.
Тот же приём служит и масштабу: Scale после минус-сдвига растянет картинку вокруг её центра, а без него — вокруг левого верхнего угла, и герой «отползёт» от точки стоп. Заметьте, что у вращения есть граница применимости, о которой скоро напишет девятый урок: AABB коллизий не вращается, поэтому повороты — украшение отрисовки, а не физики. Пули можно крутить, стены — нет.
Соберите heroRect глазами
Герой 32×48 с пивотом-ногами стоит в точке (100, 100). Какой прямоугольник вернёт heroRect?
Где окажется центр спрайта 16×16?
Прочитайте код и выберите, что он выведет:
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-8, -8)
op.GeoM.Translate(100, 100)
screen.DrawImage(img, op)Границы холста: прижим и осмысленные вылеты#
Первое применение прямоугольников — рамки мира. Пока камера из тринадцатого урока не появилась, холст — весь мир, и герой обязан оставаться на нём: прижим к границам — ограничение координаты допустимым диапазоном, в геймдеве его зовут clamp. По горизонтали минимум — левая грань героя, максимум — ширина холста минус ширина героя; при пивоте-ногах рамка считается от прямоугольника, а не от точки, поэтому heroRect здесь окупается сразу. В стандарте прижим собирается из math.Min и math.Max в одну строку, и размещать его нужно в конце Update, после шага движения: сначала шаг, потом рамка — иначе прижим съест движение у самых границ.
Второе применение — обратное: осмысленные вылеты. Пули и частицы за границами холста не clamp-ятся, а удаляются: как только прямоугольник объекта целиком ушёл за screen.Bounds(), он мёртв, и экономия в том, что мёртвые объекты перестают участвовать в проверках. Третья история — курсор из седьмого урока: координаты указателя над полями масштабирования уходят за холст, и та же проверка границ отсекает их от хит-тестов. Одна и та же арифметика прямоугольника обслуживает рамку героя, жизнь пуль и честность прицела — это и есть признак хорошего примитива.
Какой пивот выбрать: справочная таблица#
| Объект | Пивот | Первый сдвиг GeoM | Почему |
|---|---|---|---|
| Платформенный герой | ноги (низ-центр) | (-w/2, -h) | (x, y) — точка стоп: земля и прыжки честные |
| Пули и снаряды | центр | (-w/2, -h/2) | вылетает из середины ствола, повороты вокруг себя |
| Взрывы и эффекты | центр | (-w/2, -h/2) | круговой эффект расходится ровно от эпицентра |
| Прицел курсора | центр | (-w/2, -h/2) | перекрестие показывает именно точку клика |
| Фоны и тайлы | угол (дефолт) | (0, 0) | грань слоя совпадает с гранью холста без поправок |
| Кнопки интерфейса | угол или центр | по выравниванию | центр — для текста поверх кнопки |
Кадр целиком: слои, порядок и один источник правды#
Соберём кадр игры как конвейер из пивотов. Фон — угол, ложится в (0, 0) без сдвигов. Мир — герой с пивотом-ногами, враги с тем же правилом, платформы углом. Эффекты — центром: вспышки выстрелов и взрывы обязаны расходиться от эпицентра. Интерфейс — углом или центром в зависимости от выравнивания текста. Порядок отрисовки остаётся из четвёртого урока: каждый DrawImage кладёт пиксели поверх предыдущих, поэтому фон первый, интерфейс последний. Ни один из этих слоёв не спрашивает у движка, где у картинки «настоящая» точка, — договорённость живёт в ваших функциях отрисовки, и в этом сила: поведение кадра читается по коду целиком.
Теперь главный инженерный вывод урока. Позиция одна, но ею пользуются двое: отрисовка через GeoM и логика через image.Rect — и рассинхрон между ними рождает призрачные попадания, которых «нет на экране», но «есть в логике». Лекарство простое и одно на все случаи: одна функция сборки прямоугольника на тип объекта, та же арифметика сдвига, что в отрисовке, и никакие другие расчёты границ не допускаются. Хит-тесты из следующего урока будут вызывать именно её. Когда картинок станет много и каждая окажется кадром большого листа, эта функция переживёт переход на атласы — только источник размеров сменится с отдельного файла на вырезанный кадр из урока про атласы.
Классический баг середины курса: прямоугольник героя собирает heroRect с пивотом-ногами, а хит-тест пули написан «на глаз» от угла. Картинка целая, но попадания регистрируются в пустоте рядом — пивоты разъехались. Что сломалось бы дальше: каждый объект начал бы считать свою геометрию, и отладка превратилась бы в археологию. Правило: одна функция сборки прямоугольника, и отрисовка, и логика держатся её пивота.
Спрайты из этого урока видны только в локальном проекте с Go — окно и картинка нужны для DrawImage. Но арифметика пивотов — чистая математика сдвигов и прямоугольников: тематическая программа в песочнице печатает для одной точки (x, y) прямоугольники героя при пивотах угол, центр и ноги — разница видна в числах на странице.
Правила пивотов и границ одним списком#
- DrawImage без сдвигов ставит в (x, y) левый верхний угол — угол тоже пивот, просто дефолтный
- Пивот задаётся минус-сдвигом GeoM перед позицией: центр — (-w/2, -h/2), ноги — (-w/2, -h)
- Платформенному герою — ноги: (x, y) становится точкой стоп, и падение честное
- Прямоугольник логики строит image.Rect из тех же координат и размеров, что отрисовка
- image.Rect упорядочивает перепутанные углы сам — минимум и максимум всегда верные
- Прижим к границам — math.Min и math.Max в конце Update; вылетевшие пули — удалять
Соберите прямоугольник и центр-отрисовку
Восстановите три пропуска: размер из Bounds, прямоугольник и сдвиг пивота.
Отрисовка героя с пивотом-ногами
Соберите функцию: размеры, опции, два сдвига и рисование.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Договорённость урока: у каждого спрайта есть пивот, пивот задаётся минус-сдвигом матрицы, а логика собирает прямоугольник той же арифметикой — от одной точки и одних размеров. Следующий шаг закономерен: урок про коллизии AABB берёт эти прямоугольники и учит их сталкиваться — стены, платформы и туннелирующие пули ждут именно их. Домашняя проверка на порядок матриц проста: поверните героя вокруг центра и убедитесь, что прицел и коллизии остались на своих местах. Техническая база осталась с четвёртого урока про спрайты: DrawImage, GeoM и Bounds не изменились, изменился только взгляд на точку, которую задаёт Translate. А когда картинок станет много, размеры для heroRect начнут приходить из вырезанных кадров урока про атласы — пивоты при этом переживут смену источника пикселей без единой правки, потому что живут в арифметике, а не в файлах.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Пивот — точка спрайта, попадающая в координаты Translate; дефолт DrawImage — левый верхний угол.
- Смена пивота — минус-сдвиг GeoM перед позицией: центр — (-w/2, -h/2), ноги платформенного героя — (-w/2, -h).
- Прямоугольник логики собирает image.Rect из позиции и размеров Bounds — той же арифметикой, что отрисовка.
- Одна функция сборки прямоугольника на объект защищает от призрачных попаданий и рассинхрона пивотов.
- Приведение к int живёт только в момент сборки хит-прямоугольника: позиция остаётся float64.
- Границы холста — clamp в конце Update; вылетевшие за Bounds объекты удаляются, а не прижимаются.
Домашнее задание
Заведите в проекте из четвёртого урока drawAtCenter и drawHero с пивотом-ногами и проверьте predict: центр спрайта 16×16 должен лечь ровно в точку клика из седьмого урока. Напишите heroRect и добавьте clamp по границам холста: герой с пивотом-ногами не должен уезжать за экран ни одной гранью прямоугольника, включая верх прыжка. Добавьте пулю с пивотом-центром, вылетающую из головы героя, — убедитесь, что вспышка выстрела расходится от правильной точки, а не от угла спрайта. Затем нарочно сломайте синхронизацию: соберите хит-прямоугольник от угла, а рисуйте от ног, и посмотрите на призрачные попадания — после чего верните одну функцию сборки на место.
Личные заметкиПусто
Вопросы и ответы по уроку
После добавления Scale герой уползает — пивот тоже масштабируется?
Уползает, если порядок вызовов неверный: GeoM копит операции, и Scale, применённый после Translate, масштабирует сам сдвиг. Правильный порядок — сначала Scale, потом Translate: картинка растягивается вокруг своего угла, затем пивот ставится в позицию. Для масштабирования от центра к стандартной паре сдвигов добавляется ещё минус-половина размера перед Scale.
Зачем int внутри image.Rect, если координаты float64?
Потому что image.Rect принимает только целые координаты: стандартная библиотека хранит Min и Max как int. Усечение долей пикселя в момент сборки хит-прямоугольника безвредно — проверки работают на пиксельной сетке, — а накопление движения продолжается в дробном поле. Приводить позицию целиком к int нельзя: вернётся тихий замедлитель из пятого урока.
Кадры в атласе разного размера — куда девать пивот?
Пивот считается от вырезанного кадра, а не от атласа, поэтому у кадров разного размера он разный по величине, но одинаковый по смыслу. Два рабочих пути: выравнивать кадры анимации по нижней границе при рисовании листа — тогда пивот-ноги у всех считается одинаково, — или хранить рядом с кадрами таблицу офсетов. Точные прямоугольники кадров даёт урок про атласы.
Термины курса
Понятия, на которых строится этот курс, — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылок на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript