---
title: "Коллизии: AABB-проверки"
description: "Коллизии AABB в играх на Go: пересечение прямоугольников и Overlaps, осевое разделение X и Y, стены, платформы и туннелирование пуль — код для Ebitengine."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/09-kollizii-aabb/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Коллизии: AABB-проверки

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

Коллизии AABB — самая честная математика в 2D-игре: каждый объект становится прямоугольником, и столкновения игры на go сводятся к четырём сравнениям за тик. Соберём стены и платформы, разберём осевое разделение X и Y и узнаем, почему быстрая пуля пролетает сквозь тонкую стену.

Герой шестого урока едет, герой восьмого знает свои прямоугольники — но пока мир полый: сквозь стены, платформы и врагов он проходит, как призрак. Игра заканчивается там, где объекты начинают чувствовать друг друга, и хорошо, что для подавляющего большинства 2D-игр это чувство считается школьной геометрией. Подход называется AABB — axis-aligned bounding box, прямоугольник, стороны которого параллельны осям. Вращений нет, кругов нет, и это не упрощение от бедности, а сознательный контракт: у геймдева девяностые годы прошли именно на таких прямоугольниках, и аркадные классики стоят на них до сих пор. В этом уроке мы напишем проверку пересечения, разложим движение по осям, заведём мир из стен и встретим первое по-настоящему коварное явление курса — туннелирование.

### Коллизии AABB: пересечение прямоугольников

Два прямоугольника пересекаются тогда и только тогда, когда каждый из них заходит на территорию другого по обеим осям. По горизонтали: левый край первого левее правого края второго и левый край второго левее правого края первого. По вертикали — то же самое. Итого четыре сравнения, и вся физика платформера в них помещается: a.Min.X < b.Max.X && b.Min.X < a.Max.X && a.Min.Y < b.Max.Y && b.Min.Y < a.Max.Y. Обратите внимание на строгость знаков: касание границами — когда правый край героя точно совпал с левым краем стены — не считается пересечением. Это не прихоть, а удобное свойство: вплотную вставший герой не «столкнулся», и проверки не дребезжат на границе.

Ничего из этого писать руками, впрочем, не обязательно: в прямоугольнике image.Rectangle из восьмого урока уже живёт метод Overlaps — «имеют непустое пересечение», — реализованный ровно этими четырьмя сравнениями плюс явная проверка на вырожденные прямоугольники нулевой толщины, которые не пересекаются ни с чем. Писать свою функцию стоит ради прозрачности учебника или если хочется дополнительных семантик, но рабочий код смело вешает на стандартную библиотеку. Рядом живёт и Intersect: он возвращает наибольший прямоугольник, лежащий в обоих сразу, а без пересечения — нулевой. Механикам, которым важна сила удара, размер этого общего куска даёт глубину проникновения: отбрасывание и урон удобно масштабировать его шириной или площадью вместо самодельной арифметики. Важная оговорка про слово «axis-aligned»: как только вы повернёте спрайт через GeoM.Rotate, его AABB остаётся невращающимся прямоугольником вокруг картинки — для большинства механик это норм, но честная физика повёрнутых объектов — уже другая математика и другая глава.

### Проверка пересечения и мир из стен

```go
// rectsOverlap — AABB-проверка в четыре сравнения;
// то же самое умеет стандартная библиотека: a.Overlaps(b)
func rectsOverlap(a, b image.Rectangle) bool {
        return a.Min.X < b.Max.X && b.Min.X < a.Max.X &&
                a.Min.Y < b.Max.Y && b.Min.Y < a.Max.Y
}

func (g *Game) hitWall(r image.Rectangle) bool {
        for _, wall := range g.walls {
                if rectsOverlap(r, wall) { // или r.Overlaps(wall)
                        return true
                }
        }
        return false
}
```

rectsOverlap повторяет канонические четыре сравнения со строгими знаками: касание — не пересечение. hitWall перебирает срез стен из поля структуры: мир — это []image.Rectangle, и сто стен на тик — копеечная цена, а return true останавливает перебор на первой же стене. Готовый Overlaps заменяет самописную функцию без единого изменения поведения.

**Вопрос:** Герой — прямоугольник (0, 0, 16, 16), стена вплотную — (16, 0, 32, 16). Что скажет AABB-проверка?

- Пересечение есть: границы совпадают, значит зашли друг на друга
- ✅ Пересечения нет: сравнения строгие, касание — не пересечение
- Зависит от скорости героя
- Overlaps вернёт true, а самописная проверка false

Первое сравнение: герой.Min.X (0) меньше стены.Max.X (32) — да; но стена.Min.X (16) меньше героя.Max.X (16) — нет, шестнадцать не строго меньше шестнадцати. Совпадение границ не пересечение, и проверки держатся стабильными у самой стены.

### Осевое разделение: сначала X, потом Y

Проверка пересечения отвечает «стукнулись или нет», но игре нужен следующий вопрос — «что делать». Ответ платформенных движков называется осевым разделением, и идея в том, чтобы никогда не проверять движение по двум осям одновременно. Тик разбивается на два полушага: сдвигаем героя по X, проверяем стены, при пересечении откатываем X; затем сдвигаем по Y, проверяем снова, откатываем Y. Почему нельзя одним шагом: герой летит по диагонали в угол — после совместного сдвига пересечение есть, но непонятно, какая ось виновата, а от катов страдает вся геометрия. Оси дают диагноз: откатился по X — блокировала стена сбоку, откатился по Y — пол или потолок, и от этого диагноза зависит всё поведение: гасить вертикальную скорость, разрешать прыжок, играть звук удара.

Откат — самое простое решение: запомнить координату до шага и вернуть её при пересечении. Мелочь, которая экономит строки у объектов без готовой функции сборки: прямоугольник можно не пересчитывать заново, а переносить тем же смещением — метод Add возвращает image.Rectangle, сдвинутый на image.Point. Более аккуратный вариант — прижим вплотную: вместо возврата старой координаты поставить героя вплотную к стене, например x = wall.Max.X при ударе слева. Прижим убирает микрощели между героем и полом и лечит дрожь у границ — об этом ниже, в живом месте. Пока запомните скелет: два полушага, две проверки, два отката — и платформер обретает стены.

### moveAndCollide: два полушага

```go
func (g *Game) moveAndCollide(dx, dy float64) {
        g.x += dx
        r := g.heroRect()
        for _, wall := range g.walls {
                if r.Overlaps(wall) {
                        g.x -= dx // откат по горизонтали
                        r = g.heroRect()
                }
        }
        g.y += dy
        r = g.heroRect()
        for _, wall := range g.walls {
                if r.Overlaps(wall) {
                        g.y -= dy // откат по вертикали
                        r = g.heroRect()
                        g.vy = 0 // стоим: гасим вертикальную скорость
                }
        }
}
```

Сдвиг по X, проверка всех стен, откат при пересечении — затем то же по Y. heroRect пересобирается после каждого изменения: прямоугольник обязан соответствовать актуальной координате. Гашение vy при вертикальном откате — заодно определение опоры: в девятом уроке курса на этом стоит прыжок.

**Вопрос:** Почему движение проверяется по осям по очереди, а не одним совместным сдвигом?

- Так быстрее: два маленьких прямоугольника проверять дешевле
- ✅ При диагональном подходе к углу совместный сдвиг не скажет, какая ось заблокировала
- Overlaps умеет работать только с одной осью за вызов
- Движок требует проверять оси в порядке X, затем Y

Совместный сдвиг даёт один факт «есть пересечение», а поведение нужно разное: сбоку — откат и стук, снизу — опора и прыжок. Осевое разделение превращает проверку в диагноз. Скорость здесь ни при чём: два прохода по десяткам стен стоят копейки.

### Куда упадёт image.Rect с перепутанными углами?

```go
r := image.Rect(10, 20, 30, 5)
fmt.Println(r.Min.Y, r.Max.Y, r.Dy())
```

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

- ✅ 5 20 15 — конструктор упорядочил углы сам
- 20 5 -15 — прямоугольник вывернут, Dy отрицательный
- 20 5 15 — Min и Max остались как передали
- Программа упадёт: Rect требует верхний левый угол первым

image.Rect канонизирует вход: если координаты перепутаны, он меняет их местами, поэтому Min.Y станет 5, Max.Y — 20, а Dy — 15. Свойство пригодится в коллизиях: сдвиги и прижимы можно считать без заботы о порядке углов.

### Стены и платформы: мир как срез прямоугольников

Куда деваются сами стены? Поле g.walls []image.Rectangle — и уровень в этой модели есть список прямоугольников. Земля — один длинный прямоугольник под всем уровнем; платформы — тонкие горизонтальные полосы; стены комнат — вертикальные. Когда в двенадцатом уроке появятся тайлмапы, источник списка сменится: тайл — клетка 16 на 16, и стены сгенерируются из непустых клеток карты автоматически, но потребитель — moveAndCollide — не изменится ни на символ. Здесь же живёт и любимая механика платформеров: односторонняя платформа, сквозь которую прыгают снизу и на которую падают сверху. Её AABB блокирует только вертикальный полушаг вниз и только когда ноги героя были выше платформы в прошлом тике; условие из трёх сравнений вешается на конкретные элементы среза, и мир становится тоньше без изменения движка. Список стен — это данные, а не код: отладочный вывод всех прямоугольников мира экономит час гаданий, где именно герой застрял.

Сколько прямоугольников выдержит перебор? Сотни — спокойно: четыре сравнения на стену при шести десятках тиков — ничто на любом железе. Тысячи и десятки тысяч требуют структур: сеток пространства, квадродеревьев — тема, к которой курс подойдёт после профилирования в двадцатом уроке, потому что premature-оптимизация коллизий — классический способ сломать работающий платформер. Правило здравого смысла: пока ActualTPS держится 60, ваш перебор — не проблема.

### Круги: когда AABB режет угол

Прямоугольник — не единственный дешёвый примитив, и у него есть честный недостаток: углы. Пуля пролетает мимо камня по диагонали, прямоугольники пересекаются в пустом углу — и регистрируется попадание, которого игрок не видит. Для круглых объектов — мячей, зон взрывов, магнитов — честнее круг: два круга пересекаются, когда расстояние между центрами меньше суммы радиусов, и это math.Hypot(dx, dy) против r1+r2 — одна строка без единого вызова движка.

Круг против прямоугольника собирается из той же операции: прижмите центр круга к прямоугольнику по осям — координата центра зажимается между Min и Max по каждой оси, ровно как clamp из восьмого урока, — и посчитайте расстояние от точки прижима до центра. Если оно меньше радиуса, пересечение есть. Clamp здесь сменил работу: в восьмом уроке он держал героя в границах холста, здесь находит ближайшую точку прямоугольника. Справочная таблица ниже сводит подходы рядом — от четырёх сравнений до пиксель-perfect.

### Виды коллизий: от AABB к пиксель-perfect

| Подход | Проверка | Цена | Когда хватает |
| --- | --- | --- | --- |
| AABB-прямоугольники | четыре сравнения | почти нулевая | платформеры, аркады, UI |
| Круги | расстояние центров против суммы радиусов | дешёвая | пули, зоны взрывов, магниты |
| Круг против AABB | прижать точку к прямоугольнику и сравнить радиус | дешёвая | мячи и шарики у стен |
| Пиксель-perfect | чтение пикселей в зоне пересечения | дорогая | финальная честность босс-файтов |

**Вопрос:** Пуля летит 30 пикселей за тик, толщина стены — 8 пикселей. Что произойдёт при AABB-проверке после шага?

- Пуля отскочит от стены, как положено
- ✅ Пуля пролетит насквозь: после шага прямоугольники уже не пересекаются
- Пуля остановится внутри стены и застрянет
- Движок сам разобьёт шаг на подшаги

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

### Туннелирование и разбивание шага

Туннелирование заслуживает отдельного блока, потому что оно не прощает интуиции. Быстрый объект за тик перепрыгивает тонкое препятствие: до шага он был слева, после — справа, и ни одна проверка «пересекаются ли прямоугольники сейчас» не увидит стену, которую он пересёк посередине шага. Математика простая: шаг за тик — это скорость, делённая на TPS, и при 60 тиках 600 пикселей в секунду — это 10 пикселей за тик; стена тоньше десяти пикселей для такой пули прозрачна. Лечится по нарастающей. Первая защита — договориться о пределе: шаг за тик не должен превышать толщины самой тонкой стены в игре, и так как тики фиксированы, предел — константа, известная на этапе дизайна. Вторая — разбивание шага: делить перемещение тика на N подшагов и проверять коллизии на каждом; движение через десять подшагов по пикселю остановится на тонкой стене честно. Третья — сегментные проверки по линии движения — за пределами курса, упоминание честности ради.

Практический вывод для игр курса: скорости героев и большинства снарядов на порядок меньше толщины любых стен, и туннелирование им не грозит; угрожают только явным пулям-молниям. Им — подшаги. Подшаг — это цикл внутри тика: dx и dy делятся на N, и на каждой итерации двигается доля с проверкой стен; N выбирается так, чтобы одна доля была тоньше самой тонкой стены. И держите связь с пятым уроком: раз TPS известен и фиксирован, максимум скорости — проектное решение, а не загадка рантайма.

> **Живое место: герой дрожит на краю стены.** Полный откат при пересечении даёт странный эффект у границ: герой прислоняется к стене и мелко дрожит. Причина в стыке float и int: позиция дробная, а heroRect усекает её до целой, и на границе пересечение то ловится, то нет. Лечение — прижим вплотную: вместо возврата старой координаты ставить героя точно к грани стены, тогда усечённый прямоугольник стабильно вне стены, и дрожь исчезает.

> **Коллизии считаются прямо в песочнице.** AABB — чистая математика без окна и картинок, и это лучший урок курса для браузера: тематическая программа в песочнице тикает героя по прямоугольникам, печатает пересечения и откаты по осям — та же функция, что в вашей локальной игре. Локальный Go понадобится, чтобы увидеть стены глазами, а не числами.

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

- AABB — прямоугольник, параллельный осям: проверка пересечения — четыре сравнения
- Сравнения строгие: касание границами — не пересечение, и это удобно
- Стандартная библиотека умеет сама: image.Rectangle.Overlaps
- Оси проверяются по очереди: сдвиг X, откат, сдвиг Y, откат — так видно, что блокировало
- Односторонняя платформа — ограничение проверки: только вниз и только если ноги были выше
- Шаг за тик меньше толщины тончайшей стены — иначе туннелирование; лечится подшагами

### Соберите moveAndCollide

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

```go
func (g *Game) moveAndCollide(dx, dy float64) {
        g.x += dx
        r := g.heroRect()
        for _, wall := range g.walls {
                if r.___(wall) {
                        g.x -= ___ // откат по горизонтали
                        r = g.___()
                }
        }
        g.y += dy
        // то же для вертикали: сдвиг, проверка, откат
}
```

Подсказка: Пересечение проверяет Overlaps, откат возвращает dx, а прямоугольник пересобирает heroRect после каждой правки координаты.

Правильные ответы в пропусках: `Overlaps`, `dx`, `heroRect`

### Тик платформера от ввода до отрисовки

Расставьте порядок: ввод, шаги по осям с коллизиями, затем кадр.

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

1. `dx, dy := readDirection()`
2. `dx *= g.speed / float64(ebiten.TPS())`
3. `dy *= g.speed / float64(ebiten.TPS())`
4. `g.moveAndCollide(dx, dy) // оси: X, откат, Y, откат`
5. `if g.onGround && inpututil.IsKeyJustPressed(ebiten.KeySpace) { g.jump() }`
6. `g.updateEnemies() // враги ходят по тем же стенам`
7. `op.GeoM.Translate(g.x, g.y)`
8. `screen.DrawImage(hero, op)`

Мир приобрёл твёрдость: прямоугольники из восьмого урока столкнулись четырьмя сравнениями, оси разделили движение на диагнозы, стены стали срезом, а туннелирование превратилось из сюрприза в проектное ограничение скорости. Дальше мир получит лицо: урок про [атласы](/courses/ebitengine/lessons/10-atlasy/) нарисует стены и героев из одного файла-листа, и прямоугольники коллизий переживут это без правок, потому что живут в координатах, а не в картинках. Стены сгенерирует из клеток урок про [тайлмапы](/courses/ebitengine/lessons/12-tajlmapy/), а арифметику шага за тик напомнит [урок про движение](/courses/ebitengine/lessons/05-geometriya-dvizheniya/). Проверка на понимание простая: если ваш герой дрожит у стены — вы откатываете вместо прижима; если пули проходят сквозь тонкие колонны — вы проверяете один раз после полного шага. Оба симптома лечатся текстом этого урока. Третий симптом — призрачные попадания в пустых углах: у вашего снаряда круглый смысл, а проверка прямоугольная; круг из этого урока закрывает и его.

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

- **AABB** — Axis-aligned bounding box: прямоугольник, стороны параллельны осям. Пересечение — четыре сравнения; вращения не поддерживаются сознательно.
- **Overlaps** — Метод image.Rectangle: «непустое пересечение». Реализация — строгие сравнения плюс защита от вырожденных прямоугольников, которые не пересекаются ни с чем.
- **Осевое разделение** — Проверка движения по осям по очереди: сдвиг X, откат, сдвиг Y, откат. Даёт диагноз, какая ось блокировала: стена сбоку или опора снизу.
- **Прижим** — Постановка героя вплотную к грани стены вместо полного отката. Убирает микрощели и дрожь на границе, вызванную усечением float в int.
- **Туннелирование** — Пролёт быстрого объекта сквозь тонкую стену за тик: до шага пересечения нет, после — тоже. Лечится пределом скорости и подшагами.
- **Односторонняя платформа** — AABB, блокирующий только падение сверху: проверка активна при движении вниз и если ноги были выше платформы в прошлом тике. Прыжок снизу проходит.

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

- Коллизии AABB — четыре сравнения по Min и Max; касание границами при строгих знаках не считается пересечением.
- image.Rectangle.Overlaps из стандартной библиотеки заменяет самописную проверку без изменения поведения.
- Осевое разделение — сдвиг X, откат, затем Y: совместный шаг не говорит, какая ось заблокировала.
- Мир — это []image.Rectangle: земля, платформы и стены; тайлмапы позже только сменят источник списка.
- Туннелирование лечится проектным пределом скорости за тик и разбиванием шага на подшаги.
- Дрожь у стены лечится прижимом вплотную к грани вместо полного отката — float и int перестают спорить.

## Домашнее задание

Постройте комнату из пяти стен: земля, потолок и две вертикали — и заведите героя с moveAndCollide. Проверьте диагональный подход к углу: откат должен работать по каждой оси отдельно, без зависания в углу. Добавьте одностороннюю платформу: снизу герой пролетает, сверху встаёт, спрыгнуть вниз — через временное игнорирование платформы. В момент столкновения выведите Intersect героя и стены и посмотрите, как меняется общий прямоугольник при подходе с разных сторон — глубина проникновения станет видимой в числах. Затем заведите пулю со скоростью 600 пикселей в секунду и стену толщиной 8 пикселей: убедитесь в туннелировании и почините его подшагами — делите шаг тика на столько частей, чтобы каждый был тоньше стены. В конце замените прижимом полный откат по вертикали и посмотрите, как исчезает дрожь на полу.

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

### Почему бы не подключить готовый физический движок?

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

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

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

### Повернутый спрайт — что с его AABB?

AABB невращающийся по определению: вокруг повернутой картинки останется прямоугольник по осям, и коллизии будут считаться с ним. Для сносимых по дуге стрелок и медленно вращающихся объектов это обычно нормально. Когда же поворот — суть механики, переходите к кругам: расстояние центров дешевле честного повёрнутого прямоугольника и часто точнее по ощущениям.
