16 марта 2016 в 12:26

Is Grounded в 2D платформере: как узнать, стоит ли персонаж? из песочницы

Буду краток. Бился над этой задачей достаточно долгое время, посему решил поделиться решением. Движок — Unity3D.

Постановка задачи


Есть персонаж, есть платформы. Само собой, на персонаже коллайдер и ригидбоди, всё двадэ. На платформах тоже коллайдеры. На платформах — эффекторы, ибо платформы должны быть твердые только сверху. То есть, если перснаж упал на платформу, он на ней стоит. Но если персонаж идет мимо платформы или прыгает под ней — он свободно проходит через нее.

Персонаж может прыгать, только если стоит на платформе. Нужно знать, стоим ли мы.

Условия:

1) Форма платформы произвольная.
2) Персонаж может стоять на самом краю платформы.
3) Платформы могут быть наклонными.
4) Платформы могут двигаться.

Популярное решение и почему оно не подходит


В стандартном примере из ассет-стора предлагается использовать дополнительный геймобжект под персонажем, кидать из персонажа рэйкасты в геймобжект и следить, нет ли пересечения с землей.

1) Если мы «идем мимо» или «запрыгиваем на», то рэйкаст вполне поймает «землю», хотя персонаж не стоит.
2) Рэйкаст работает по конкретной точке, а не по площади. А значит, если платформа не будет находиться именно под точкой рэйкаста — например, мы стоим на краю платформы, или на маленьком камушке — проверка ее не заметит.

Решение


Решение нельзя получить логически, только опытным путем. Ибо оно проистекает из особенностей работы физики движка. Вам нужны:
using System.Linq — вообще удобная штука, рекомендую ознакомиться, если еще не. Использую для поиска по коллекциям, уничтожает 90% форычей с вложенными ифами.
GetComponent().bounds.min.y — низ вашего коллайдера, то бишь минимальная ордината
OnCollisionStay2D docs.unity3d.com/ScriptReference/Collider2D.OnCollisionStay2D.html
OnCollisionExit2D
CollisionContacts docs.unity3d.com/ScriptReference/Collision-contacts.html docs.unity3d.com/ScriptReference/ContactPoint-point.html

OnCollisionStay2D срабатывает, когда персонаж и платформа движутся друг относительно друга. При попадании персонажа на платформу т.е. при OnCollisionEnter2D вышеуказанное событие тоже сработает.

Так вот, оказывается, если персонаж стоит на платформе, то геометрически его коллайдер и коллайдер платформы не пересекаются. А значит, «низ персонажа» должен быть ВНЕЗАПНО выше точки контакта. ВСЁ!

1) Создаем список:

List<Collider2D> GroundColliders = new List<Collider2D>();

2) По событию OnCollisionStay2D проверяем и при необходимости добавляем коллайдеры:

void OnCollisionStay2D(Collision2D coll)
{     
     if (!GroundColliders.Contains(coll.collider))   
            foreach (var p in coll.contacts)
                   if (p.point.y < myCollider.bounds.min.y)
                   {
                          GroundColliders.Add(coll.collider);
                          break;
                   }
}

3) Если больше не пересекаемся с платформой — удаляем ее из списка:

void OnCollisionExit2D(Collision2D coll)
    {
        if (GroundColliders.Contains(coll.collider))
            GroundColliders.Remove(coll.collider);
    }

4) Угадайте, что проверяем:

bool IsGrounded
    {
        get
        {
            return GroundColliders.Count > 0;
        }
    }

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

Комментарии (19)

  • +1
    А значит, «низ персонажа» должен быть ВНЕЗАПНО выше точки контакта. ВСЁ!

    ну я даже не знаю...
    • +1
      image
      Я серьезно думал, что точкки контакта двух коллайдеров ПРИНАДЛЕЖАТ обоим коллайдерам, т.е. находятся внутри или на границе. И соответственно, точка контакта не может быть ниже самой низкой точки коллайдера. А однако ж!
  • +3
    Я, может, не понял проблемы, которую решает автор, но почему нельзя отслеживать контакт с землей просто по коллайду персонажа и платформы?
    Раз столкнулся — значит встал на платформу — значит теперь стоит?
    Ну и флажок какой "можноПрыгнуть" — прыгнули, сняли флажок, приземлились — включили флажок ?
    • 0
      На платформах — эффекторы, ибо платформы должны быть твердые только сверху. То есть, если персонаж упал на платформу, он на ней стоит. Но если персонаж идет мимо платформы или прыгает под ней — он свободно проходит через нее.

      Пересечение с коллайдером ВООБЩЕ не означает, что персонаж стоит на платформе. В этом и сложность.
    • 0
      И да,
      приземлились — включили флажок ?
      — а как узнать, что приземлились? Нулевая вертикальная скорость может быть и в верхней токе прыжка, плюс платформа, на которой мы находимся, может двигаться. Мы можем прыгать из-под платформы и в верхней точке прыжка пересекаться с ней, но все-таки не "встать" на нее — и вот, у перса нулевая скорость, и он пересекается с платформой — НО НЕ СТОИТ.
      Также платформа по тем или иным причинам может исчезнуть из-под персонажа.
  • +2
    Game development? 2D? Платформер? И ни одной картинки в статье?
    Ну вы, блин, даете. (с)
    • –1
      Уговорили, нате картинку: о)

      image
  • 0
    Я может чего не понимаю, но: http://docs.unity3d.com/Manual/class-PlatformEffector2D.html
    Достаточно установить флажок — Use One Way.
    • 0
      Конечно, можно. Но и что с того? Как это поможет узнать, можно прыгать или нет? Есть ли у вас более простой способ написать код, чтобы персонаж прыгал только если стоит на поверхности?
      К тому же, если вы полагаете, что с этим флажком событие коллизии генерируется только "с нужной стороны" — вы ошибаетесь.
      • 0
        Вот вам видео, где пацан за 2 минуты сделал так, чтобы персонаж прыгал сквозь платформу: https://www.youtube.com/watch?v=acFYSKle6wY
        Или же я снова чего-то недопонял?
        • 0
          Надо, чтобы он прыгал толкьо когда стоит. Задача — в том, чтобы определить, что персонаж прямо сейчас стоит.
          Сложно не сделать прыжки и ходьбу сквозь платформу — сложно отследить, что персонаж стоит, с учетом того, что персонаж может прыгать-ходить сквозь платформы, двигатсья вместе с платформами, платформы могут быть наклонными и т. д.
          Т.е. ни вертикальная скорость персонажа, ни пересечение с платформой, ни положение платформы и персонажа друг относительно друга не являются показателями того, что персонаж стоит.
          Так что недопоняли, да.
  • 0
    Я правильно понял, две платформы могут быть друг над другом, но ближе, чем высота персонажа? можно сказать — "под ногами" и "в поясе"? И тогда, если я иду по нижней платформе, то я могу идти сквозь верхней платформы. А если прыгну — то попаду на верхнюю платформу.
    • 0
      Да. Это делается стандартными средствами юнити. А вот отличить в самом коде платформу, на которой ты стоишь от платформы, через которую ты идешь, отличить именно "коллизию стояния" — это то, что делает моё решение.
      • 0
        не думаете ассет сделать на основе вашего решения?
  • +1
    Интересное решение!
    Но по статье два "совета":
    1 — LINQ хорош для прототипирования, когда нужно быстро понять что куда, но не всегда пригоден для рантайма из-за усиленного выделения памяти. Kаждый запрос — новый объект в куче, причём каждый новый запрос после точки — новый объект. Привет immutability. Поэтому часто на основе используемых решений просто делают конкретную реализацию алгоритма (поиска, агрегации или фильтрации) без дополнительного выделения памяти.
    2 — Удалять элемент из списка можно без проверки Contains. Remove просто возвращает false если элемент в списке не найден (в самом Remove проверяется наличие элемента в списке с помощью IndexOf < 0)
  • +1
    Попробовал ради интереса реализовать решение, и вот что получилось в моём случае:
    В OnCollisionStay проверяется это хитрое условие на "выше платформы" и оно не выполняется. Более того, на Math.Approximately тоже не выполняется (отрисовал точки дебагом, они выглядели "на одной высоте"). Стал смотреть фактические значения — в моём случае точка контакта оказалась выше нижней точки коллайдера. Приблизил картинку — так и есть. На скриншоте видно, что красная линия (точка контакта) выше синей (нижняя точка коллайдера)

    разница высот

    Причём красная линия рисуется от точки контакта, а левая от нижней точки коллайдера. То есть (ещё раз) точка контакта выше нижней точки коллайдера. Проверить таким образом "приземление" не представляется возможным, ибо такой случай возникает чаще всего именно в описанных случаях, которые мы как раз пытаемся избежать (втыкаемся в платформу сбоку, пролетаем через платформу "телом"). Код несколько раз перепроверил, условие именно такое, которое описано в статье:

    if (collision.contacts[i].point.y < GroundCheckCollider.bounds.min.y)

    Видимо ваши звёзды были к вам более благосклонны и вы получили другой результат.
    • 0
      О да. мои звезды благосклонны. Загадка… Можно в ту формулу внести какую-то дельту
      if (point.y — bounds.min.y < 0.001) типа того
      • +1
        О, уже первый костыль! А почему именно 0.001? Может в каком-то случае будет больше. У вас на скриншоте как раз коллайдер заметно выше платформы (забегая вперёд — в моём случае эта разница получилась равной 0.0075). Эти волшебные числа до добра не доводят. Было бы интересно разобраться с самим принципом работы этого Box2D — почему в одних случаях коллайдер выше, а в других ниже, и тогда можно было бы с уверенностью использовать подобный способ.

        Просто ради интереса — попробуйте изменить нижний коллайдер вашего персонажа на круг, тоже будет немного вниз проваливаться?

        Поменял у себя коллайдер на BoxCollider2D и выполнился ваш случай, то есть коллайдер чуть выше платформы. Но в моём случае не получится использовать этот тип коллайдера.
        Немного потестил как это всё работает, и получилось, что bounds.min.y для BoxCollider2D находится в центре привязки спрайта (pivot point), а не внизу по умолчанию (только если сама точка привязки в нуле). Синяя линия проводится из точки минимума вправо. Красные линии — точки контакта, тоже вправо:




        С CircleCollider2D, как я уже скидывал выше, min.y всегда внизу. После изменения точки привязки картина не поменялась — точка контакта всё так же выше нижней точки коллайдера.
  • 0
    Конечно, я в Unity не профи, но по-моему это какое-то слишком запаренное решение. Во-первых, есть же намного проще и лаконичнее, а во-вторых, уже раза 2 точно видел про это isGrounded на хабре, которые сюда перетекли с просторов англоязычного ютуба.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.