---
title: "Прямоугольники и позиции спрайта"
description: "Позиции спрайта в Ebitengine: пивот спрайта — угол, центр или ноги, Bounds и image.Rect, прижим к границам холста и один источник правды — код и разбор."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/08-pryamougolniki/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Ebitengine: 2D-игры на Go», урок 8 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ebitengine/lessons/08-pryamougolniki/*

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

- Translate(-16, -24)
- ✅ Translate(-16, -48)
- Translate(16, 48)
- Translate(-32, -48)

Ноги — это нижняя грань и середина по горизонтали: сдвиг на минус половину ширины (32/2 = 16) и на минус всю высоту (48). После него точка (x, y) — точка стоп героя. Половина высоты дала бы пивот в центре, а он нужен пулям и эффектам.

### Спрайт координаты против прямоугольника: 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?

- ✅ image.Rect(84, 52, 116, 100)
- image.Rect(100, 100, 132, 148)
- image.Rect(84, 52, 116, 99)
- image.Rect(84, 100, 116, 148)

Прямоугольник повторяет пивот: минус половина ширины (84 = 100 - 16), минус вся высота (52 = 100 - 48), низ ровно в точке стоп. Вариант с 99 — ловушка «границы включительно»: Max не входит в прямоугольник, и единица там лишняя. Второй вариант собирает прямоугольник от угла, как будто пивота нет вовсе.

### Где окажется центр спрайта 16×16?

```go
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-8, -8)
op.GeoM.Translate(100, 100)
screen.DrawImage(img, op)
```

**Что выведет программа?**

- ✅ Центр картинки — в точке (100, 100)
- Левый верхний угол — в (92, 92), центр — в (108, 108)
- Левый верхний угол — в (100, 100), центр — в (108, 108)
- Центр — в (92, 92): минус-сдвиг перетянет картинку назад

Матрица копит сдвиги: минус-сдвиг (-8, -8) переносит пивот в центр, затем (100, 100) ставит центр в цель. Для угла суммарный сдвиг равен (92, 92) — и именно поэтому угол окажется там, а центр в (100, 100). Это и есть механика смены пивота одним вызовом.

### Границы холста: прижим и осмысленные вылеты

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

```go
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)
}
```

Подсказка: Ширина прямоугольника — Dx, конструктор прямоугольника — image.Rect, сдвиг пивота — GeoM.Translate.

Правильные ответы в пропусках: `Dx`, `Rect`, `Translate`

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

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

Правильный порядок строк:

1. `func drawHero(screen *ebiten.Image) {`
2. `w := hero.Bounds().Dx()`
3. `h := hero.Bounds().Dy()`
4. `op := &ebiten.DrawImageOptions{}`
5. `op.GeoM.Translate(-float64(w)/2, -float64(h))`
6. `op.GeoM.Translate(g.x, g.y)`
7. `screen.DrawImage(hero, op)`
8. `}`

Договорённость урока: у каждого спрайта есть пивот, пивот задаётся минус-сдвигом матрицы, а логика собирает прямоугольник той же арифметикой — от одной точки и одних размеров. Следующий шаг закономерен: урок про [коллизии AABB](/courses/ebitengine/lessons/09-kollizii-aabb/) берёт эти прямоугольники и учит их сталкиваться — стены, платформы и туннелирующие пули ждут именно их. Домашняя проверка на порядок матриц проста: поверните героя вокруг центра и убедитесь, что прицел и коллизии остались на своих местах. Техническая база осталась с четвёртого урока про [спрайты](/courses/ebitengine/lessons/04-sprajty/): DrawImage, GeoM и Bounds не изменились, изменился только взгляд на точку, которую задаёт Translate. А когда картинок станет много, размеры для heroRect начнут приходить из вырезанных кадров урока про [атласы](/courses/ebitengine/lessons/10-atlasy/) — пивоты при этом переживут смену источника пикселей без единой правки, потому что живут в арифметике, а не в файлах.

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

- **Пивот** — Точка спрайта, которая встанет в координаты Translate. Дефолт — левый верхний угол; центр и ноги задаются минус-сдвигом GeoM перед позицией.
- **Пивот «ноги»** — Нижний центр картинки: сдвиг (-w/2, -h). Стандарт платформеров — (x, y) становится точкой стоп, падение заканчивается ровно на земле.
- **Bounds** — Метод ebiten.Image, возвращающий image.Rectangle с размерами картинки; Size, Dx и Dy дают габариты для арифметики пивотов и границ.
- **image.Rect** — Конструктор прямоугольника из двух углов; перепутанные координаты упорядочивает сам. Из него собираются heroRect, стены и хит-зоны.
- **Min и Max** — Поля image.Rectangle: левый верхний и правый нижний углы. Прямоугольник в Go задан двумя точками, а не точкой и размерами.
- **Clamp к границам** — Ограничение координаты диапазоном через math.Min и math.Max в конце Update. Рамка мира для героя и отсечение курсора над полями масштаба.

## Коротко: что запомнить

- Пивот — точка спрайта, попадающая в координаты 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 нельзя: вернётся тихий замедлитель из пятого урока.

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

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