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

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

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

Урок 9 из 25 ~20 мин +50 XP

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

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

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

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

Осевое разделение: сначала 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 при вертикальном откате — заодно определение опоры: в девятом уроке курса на этом стоит прыжок.

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

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

Прочитайте код и выберите, что он выведет:

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

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

Куда деваются сами стены? Поле 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-проверке после шага?

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

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

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

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

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

Соберите moveAndCollide

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

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 // то же для вертикали: сдвиг, проверка, откат }

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

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

Строки

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

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

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

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

Термин

AABB

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

Axis-aligned bounding box: прямоугольник, стороны параллельны осям. Пересечение — четыре сравнения; вращения не поддерживаются сознательно.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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