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

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

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

Урок 8 из 25 ~18 мин +50 XP

Прямоугольники и позиции спрайта

Спрайт координаты подчиняются не позиции, а точке привязки: DrawImage кладёт картинку так, что её левый верхний угол оказывается в заданной точке, и от выбора этой точки зависит вся остальная игра. Разбираем пивот спрайта — угол, центр или ноги, прямоугольник из Bounds и image.Rect и прижим к границам холста.

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

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

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

  • DrawImage без сдвигов ставит в (x, y) левый верхний угол — угол тоже пивот, просто дефолтный
  • Пивот задаётся минус-сдвигом GeoM перед позицией: центр — (-w/2, -h/2), ноги — (-w/2, -h)
  • Платформенному герою — ноги: (x, y) становится точкой стоп, и падение честное
  • Прямоугольник логики строит image.Rect из тех же координат и размеров, что отрисовка
  • image.Rect упорядочивает перепутанные углы сам — минимум и максимум всегда верные
  • Прижим к границам — math.Min и math.Max в конце Update; вылетевшие пули — удалять

Соберите прямоугольник и центр-отрисовку

Восстановите три пропуска: размер из Bounds, прямоугольник и сдвиг пивота.

func (g *Game) heroRect() image.Rectangle { w := hero.Bounds().() h := hero.Bounds().Dy() x, y := int(g.x), int(g.y) return image.(x-w/2, y-h, x+w/2, y) } func drawAtCenter(screen, img *ebiten.Image, cx, cy float64) { op := &ebiten.DrawImageOptions{} op.GeoM.(-float64(img.Bounds().Dx())/2, -float64(img.Bounds().Dy())/2) op.GeoM.Translate(cx, cy) screen.DrawImage(img, op) }

Отрисовка героя с пивотом-ногами

Соберите функцию: размеры, опции, два сдвига и рисование.

Строки

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

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

Договорённость урока: у каждого спрайта есть пивот, пивот задаётся минус-сдвигом матрицы, а логика собирает прямоугольник той же арифметикой — от одной точки и одних размеров. Следующий шаг закономерен: урок про коллизии AABB берёт эти прямоугольники и учит их сталкиваться — стены, платформы и туннелирующие пули ждут именно их. Домашняя проверка на порядок матриц проста: поверните героя вокруг центра и убедитесь, что прицел и коллизии остались на своих местах. Техническая база осталась с четвёртого урока про спрайты: DrawImage, GeoM и Bounds не изменились, изменился только взгляд на точку, которую задаёт Translate. А когда картинок станет много, размеры для heroRect начнут приходить из вырезанных кадров урока про атласы — пивоты при этом переживут смену источника пикселей без единой правки, потому что живут в арифметике, а не в файлах.

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

Термин

Пивот

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

Точка спрайта, которая встанет в координаты Translate. Дефолт — левый верхний угол; центр и ноги задаются минус-сдвигом GeoM перед позицией.

1 / 6

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

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

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

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

После добавления Scale герой уползает — пивот тоже масштабируется?

Уползает, если порядок вызовов неверный: GeoM копит операции, и Scale, применённый после Translate, масштабирует сам сдвиг. Правильный порядок — сначала Scale, потом Translate: картинка растягивается вокруг своего угла, затем пивот ставится в позицию. Для масштабирования от центра к стандартной паре сдвигов добавляется ещё минус-половина размера перед Scale.

Зачем int внутри image.Rect, если координаты float64?

Потому что image.Rect принимает только целые координаты: стандартная библиотека хранит Min и Max как int. Усечение долей пикселя в момент сборки хит-прямоугольника безвредно — проверки работают на пиксельной сетке, — а накопление движения продолжается в дробном поле. Приводить позицию целиком к int нельзя: вернётся тихий замедлитель из пятого урока.

Кадры в атласе разного размера — куда девать пивот?

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

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

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

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

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

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

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

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