Содержание урока · 10 разделов
- Коллизии AABB: пересечение прямоугольников
- Осевое разделение: сначала X, потом Y
- Стены и платформы: мир как срез прямоугольников
- Круги: когда AABB режет угол
- Виды коллизий: от AABB к пиксель-perfect
- Туннелирование и разбивание шага
- Правила AABB одним списком
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Герой шестого урока едет, герой восьмого знает свои прямоугольники — но пока мир полый: сквозь стены, платформы и врагов он проходит, как призрак. Игра заканчивается там, где объекты начинают чувствовать друг друга, и хорошо, что для подавляющего большинства 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: два полушага
GOfunc (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 известен и фиксирован, максимум скорости — проектное решение, а не загадка рантайма.
Полный откат при пересечении даёт странный эффект у границ: герой прислоняется к стене и мелко дрожит. Причина в стыке float и int: позиция дробная, а heroRect усекает её до целой, и на границе пересечение то ловится, то нет. Лечение — прижим вплотную: вместо возврата старой координаты ставить героя точно к грани стены, тогда усечённый прямоугольник стабильно вне стены, и дрожь исчезает.
AABB — чистая математика без окна и картинок, и это лучший урок курса для браузера: тематическая программа в песочнице тикает героя по прямоугольникам, печатает пересечения и откаты по осям — та же функция, что в вашей локальной игре. Локальный Go понадобится, чтобы увидеть стены глазами, а не числами.
Правила AABB одним списком#
- AABB — прямоугольник, параллельный осям: проверка пересечения — четыре сравнения
- Сравнения строгие: касание границами — не пересечение, и это удобно
- Стандартная библиотека умеет сама: image.Rectangle.Overlaps
- Оси проверяются по очереди: сдвиг X, откат, сдвиг Y, откат — так видно, что блокировало
- Односторонняя платформа — ограничение проверки: только вниз и только если ноги были выше
- Шаг за тик меньше толщины тончайшей стены — иначе туннелирование; лечится подшагами
Соберите moveAndCollide
Восстановите три пропуска: проверка пересечения, величина отката и пересборка прямоугольника.
Тик платформера от ввода до отрисовки
Расставьте порядок: ввод, шаги по осям с коллизиями, затем кадр.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Мир приобрёл твёрдость: прямоугольники из восьмого урока столкнулись четырьмя сравнениями, оси разделили движение на диагнозы, стены стали срезом, а туннелирование превратилось из сюрприза в проектное ограничение скорости. Дальше мир получит лицо: урок про атласы нарисует стены и героев из одного файла-листа, и прямоугольники коллизий переживут это без правок, потому что живут в координатах, а не в картинках. Стены сгенерирует из клеток урок про тайлмапы, а арифметику шага за тик напомнит урок про движение. Проверка на понимание простая: если ваш герой дрожит у стены — вы откатываете вместо прижима; если пули проходят сквозь тонкие колонны — вы проверяете один раз после полного шага. Оба симптома лечатся текстом этого урока. Третий симптом — призрачные попадания в пустых углах: у вашего снаряда круглый смысл, а проверка прямоугольная; круг из этого урока закрывает и его.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Коллизии AABB — четыре сравнения по Min и Max; касание границами при строгих знаках не считается пересечением.
- image.Rectangle.Overlaps из стандартной библиотеки заменяет самописную проверку без изменения поведения.
- Осевое разделение — сдвиг X, откат, затем Y: совместный шаг не говорит, какая ось заблокировала.
- Мир — это []image.Rectangle: земля, платформы и стены; тайлмапы позже только сменят источник списка.
- Туннелирование лечится проектным пределом скорости за тик и разбиванием шага на подшаги.
- Дрожь у стены лечится прижимом вплотную к грани вместо полного отката — float и int перестают спорить.
Домашнее задание
Постройте комнату из пяти стен: земля, потолок и две вертикали — и заведите героя с moveAndCollide. Проверьте диагональный подход к углу: откат должен работать по каждой оси отдельно, без зависания в углу. Добавьте одностороннюю платформу: снизу герой пролетает, сверху встаёт, спрыгнуть вниз — через временное игнорирование платформы. В момент столкновения выведите Intersect героя и стены и посмотрите, как меняется общий прямоугольник при подходе с разных сторон — глубина проникновения станет видимой в числах. Затем заведите пулю со скоростью 600 пикселей в секунду и стену толщиной 8 пикселей: убедитесь в туннелировании и почините его подшагами — делите шаг тика на столько частей, чтобы каждый был тоньше стены. В конце замените прижимом полный откат по вертикали и посмотрите, как исчезает дрожь на полу.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему бы не подключить готовый физический движок?
Для платформера и аркады AABB закрывает девяносто процентов потребностей за четыре сравнения, а физический движок приносит дерево тел, ограничения, настройки стабильности и свой слой отладки — цена не оправдана, пока у вас нет честной физики твёрдых тел. Правило курса: сначала собственные прямоугольники, движок — когда появится задача, которую они не решают.
Как правильно проверить пули против врагов — с-hero-коллизиями или отдельно?
Порядок проверки не влияет на корректность — только на стоимость и читаемость. Разумная схема тика: герой против стен, враги против стен, затем снаряды против целей. Пары пуль и врагов удобно вести отдельным срезом снарядов, чтобы мёртвые объекты не участвовали в переборе — экономия на сотнях объектов заметнее всех микрооптимизаций.
Повернутый спрайт — что с его AABB?
AABB невращающийся по определению: вокруг повернутой картинки останется прямоугольник по осям, и коллизии будут считаться с ним. Для сносимых по дуге стрелок и медленно вращающихся объектов это обычно нормально. Когда же поворот — суть механики, переходите к кругам: расстояние центров дешевле честного повёрнутого прямоугольника и часто точнее по ощущениям.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript