Разрешение коллизий по осям

Иногда нужно не просто узнать о столкновении, а правильно остановить героя у стены. Разрешение коллизий по осям — приём, который спасает платформеры.
Суть: чтобы герой не проходил сквозь стены, движение разбивают по осям: сначала двигаем по X и гасим столкновения, потом по Y. Это убирает «застревания в углах».

Обнаружить столкновение — половина дела. Вторая половина — что с ним делать. Если герой въехал в стену, мало знать «есть коллизия», нужно аккуратно выдвинуть его обратно, чтобы он встал вплотную, а не застрял внутри или не проскочил насквозь. Это называется разрешением коллизий, и для платформеров есть проверенный приём.

Хитрость в том, чтобы двигать героя по одной оси за раз. Сначала сдвигаем по горизонтали (X) и проверяем столкновения со стенами: если въехали — выравниваем героя по краю стены (правым боком к левой грани стены или наоборот). Потом отдельно двигаем по вертикали (Y) и так же гасим. Почему по очереди? Потому что так мы точно знаем, с какой стороны произошёл контакт, и можем выдвинуть героя именно по этой оси, не ломая другую.

Если двигать сразу по обеим осям и пытаться разрулить, герой будет «цепляться» за углы и дёргаться. Раздельное разрешение — стандартное и надёжное решение, его используют в большинстве 2D-платформеров.

Как работает под капотом

Двигаем по X, проверяем стены, выдвигаем по X. Затем то же по Y. При движении вправо и столкновении — правый край героя становится левым краем стены:

   шаг 1: двигаем по X
      [герой]-->||стена||
      столкновение -> herой.right = стена.left
      [герой]||стена||   (встал вплотную)

   шаг 2: двигаем по Y
      [герой]
         |
         v падение
      ===пол===
      столкновение -> герой.bottom = пол.top

В pygame (читаем):

# движение по X
player.x += vel.x * dt
for wall in walls:
    if player.colliderect(wall):
        if vel.x > 0:  player.right = wall.left   # ехал вправо
        if vel.x < 0:  player.left = wall.right    # ехал влево

# движение по Y (отдельно!)
player.y += vel.y * dt
for wall in walls:
    if player.colliderect(wall):
        if vel.y > 0:  player.bottom = wall.top    # падал
        if vel.y < 0:  player.top = wall.bottom     # летел вверх

Логику разрешения по одной оси можно проверить без графики. Подвинем героя в стену по X и выдвинем обратно. Попробуй сам:

def resolve_x(player, wall, vx):
    # player и wall: списки [left, right]
    if player[1] > wall[0] and player[0] < wall[1]:  # пересеклись
        if vx > 0:                 # ехал вправо
            width = player[1] - player[0]
            player = [wall[0]-width, wall[0]]   # right = wall.left
            print("упёрся в стену слева от неё")
    return player

player = [90, 130]    # left=90, right=130, ширина 40
wall   = [120, 200]   # стена слева 120
player = resolve_x(player, wall, vx=200)
print("позиция героя [left, right]:", player)

Туннелирование на больших скоростях

У пошагового разрешения есть враг — туннелирование. Если объект движется очень быстро, за один кадр он может перепрыгнуть тонкую стену целиком: в начале кадра он перед стеной, в конце — уже за ней, а в момент проверки пересечения не было. Особенно страдают быстрые пули и падение с большой высоты. Поэтому тонкие стены и быстрые объекты — опасное сочетание, о котором стоит помнить заранее.

Лечат это по-разному. Простой способ — делать стены потолще, чтобы за кадр их было не перескочить. Надёжнее — разбивать большой шаг движения на несколько маленьких подшагов и проверять коллизию после каждого, как будто кадр стал «дробнее». Для самых быстрых объектов применяют непрерывную проверку — луч (raycast) по траектории движения, ищущий первое пересечение. Начинающим достаточно знать о проблеме и держать стены разумной толщины; продвинутые решения пригодятся, когда в игре появятся по-настоящему быстрые объекты.

Частые ошибки

  • Двигать сразу по двум осям и разруливать — герой цепляется за углы и дёргается.
  • Выдвигать не по той оси — герой «телепортируется» вбок при падении.
  • Не учитывать направление скорости — непонятно, к какой грани прижимать.

Best practices

  • Всегда разрешай коллизии по осям раздельно: сначала X, потом Y.
  • Прижимай героя к грани стены по направлению его движения.
  • После Y-разрешения при падении ставь on_ground = True для прыжка.

Итог: двигай по одной оси, проверяй, выдвигай — потом вторая ось. Этот приём убирает застревания и делает столкновения со стенами надёжными.

Проверьте себя
1. Почему движение и разрешение коллизий делают по осям раздельно?
AТак быстрее рисуется
BЧтобы точно знать ось контакта и не застревать в углах
CТак требует pygame
DЧтобы экономить память
2. Куда выдвинуть героя, если он ехал ВПРАВО и врезался в стену?
Aherой.left = стена.right
Bherой.right = стена.left
Cherой.top = стена.bottom
Dникуда не двигать