Столкновения (Collisions) играют важную роль в компьютерных играх. Это, пожалуй, не конкретная механика, а объемный пласт взаимодействия между игровыми объектами.

В этой статье (потом, возможно, серии статей) мы разберем, как работать со столкновениями в Unity, как ловить и обрабатывать их в коде, глубже погрузимся в тему и постараемся ответить на часто возникающие вопросы.

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

Как работает система столкновений? Что такое коллайдеры?

Система столкновений Unity работает за счет коллайдеров (Colliders).

Коллайдер - это компонент, который представляет собой невидимые "границы" объекта.

Часто они совпадают с формой самого объекта (как в реальном мире), хотя это и не обязательно.

Unity поддерживает разнообразные формы коллайдеров:

  • BoxCollider - форма прямоугольного параллелепипеда

  • SphereCollider - сфера

  • CapsuleCollider - капсула (математически, сфероид или эллипсоид вращения)

  • MeshCollider - кастомная форма 3Д-меша, соответствующая форме самого меша.

  • BoxCollider2D - прямоугольник

  • CircleCollider2D - круг

  • PolygonCollider2D - кастомная форма 2Д-спрайта, повторяющая форму самого спрайта.

  • ...

Повторюсь, коллайдеры задают границы объектов, которые используются при расчете физики. Например:

Кубик лежит на столе: и куб, и стол имеют границы, поэтому не проходят сквозь друг друга.

В героя попала шальная пуля: мы зафиксируем это и уменьшим его здоровье.

Герой бежит и упирается в стену: он не может пробежать сквозь стену, потому что они оба имеют границы.

Триггеры

Триггеры -- это те же коллайдеры. Серьезно, всего одна галочка в любом компоненте коллайдера превращает его в триггер!

Но триггеры "физически прозрачны". Другими словами, объекты, помеченные как триггеры*, не являются твердыми телами и пропускают любое другое тело сквозь себя.

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

Комната в ядовитым газом - это один большой триггер: герой проходит сквозь него без физического взаимодействия, но все время, пока герой внутри, он получает пассивный урон от яда.

Зона видимости врага - тоже триггер: когда герой находится в этой зоне, враг видит его и стреляет по нему.

Зона открытия двери - тоже может быть триггером: расположим эту зону рядом с дверью. Если игрок нажимает кнопку E находясь внутри этой зоны (т.е. достаточно близко к двери), то она открывается, иначе -- нет.

Обработка столкновений

Самое интересное и важное: как правильно из кода узнать, что столкновение произошло и обработать это?

Для этого разработчики Unity подкатили нам целую набор функций. Предлагаю на практике подробно рассмотреть как работает одна из них, а потом на ее примере обсудим другие.

По ссылке можно скачать проект, чтобы следовать за статьей: https://drive.google.com/file/d/1mh3OJd3VtdKA7BG7X9bTuwA5PxGyn6x4/view?usp=sharing

Итак, перед вами сцена с большой платформой, кубиком и шариком (а также небольшим освещением). Давайте сделаем так, чтобы при падении шарика на кубик последний уничтожался.

Давайте повесим коллайдеры на объекты: на шарик - SphereCollider, на кубик - BoxCollider. Помимо этого, обоим объектам добавим компонент Rigidbody.

Скорее всего, вы уже знаете, что Rigidbody - это компонент, который добавляет объектом физику. Именно благодаря ему шарик будет падать, а при соприкосновении с кубиком - отскочит от него.

Создадим скрипт Ball.cs в папке Scripts. Сразу повесим его на шарик, чтобы потом не забыть. В скрипт нужно добавить следующий код:

public class Ball : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        print("Collision detected");
    }
}

Функция OnCollisionEnter будет вызвана автоматически, когда шарик соприкоснется с чем-либо. Самим где-либо вызывать ее не надо.

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

Единственный аргумент collisionхранит информацию о столкновении. В частности он содержит переменную gameObject, в которой хранится объект, с которым произошло столкновение.

Попробуйте заменить print("Collision detected") на print(collision.gameObject) и увидите, что при столкновении в консоль выводится информация о нашем кубике:

Cube (UnityEngine.GameObject)

Теперь вместо принта будем удалять этот самый кубик:

private void OnCollisionEnter(Collision collision)
{
    Destroy(collision.gameObject);
}

Функция Destroy() позволяет уничтожить какой-либо объект. Первым аргументом она принимает сам объект (в нашем случае кубик), а вторым - опционально - через сколько секунд должно произойти уничтожение.

Сохраните скрипт и запустите игру, чтобы увидеть, как шарик уничтожает сначала кубик, а затем - неожиданно - и пол! Да, ведь пол - это такой же объект, который имеет коллайдер (можете проверить:)

Как сделать чтобы пол не удалялся?

Самым правильным будет повесить на пол специальный тег, по которому его можно будет отличить от других объектов. Для этого выберите пол, создайте новый тег в инспекторе, нажав "Add Tag..." и назовите его Floor. После этого вновь выберите пол и прикрепите к нему этот тег (он появится в списке).

Теперь добавим в код проверку, чтобы уничтожать только те объекты, которые НЕ имеют тега "Floor":

private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.tag != "Floor")
    {
        Destroy(collision.gameObject);
    }
}

Profit!

(Ну, или можно было все оставить как есть и сказать, что это не баг, а фича)

Перейдем к другим функциям

Не закрывайте проект. Замените написанную функцию на такую:

private void OnCollisionStay(Collision collision)
{
    print("Objects are colliding");
}

Запустив проект, вы увидите, что строка выводится в консоль каждый божий кадр.

Функция OnCollisionStay срабатывает каждый* кадр, когда объекты соприкасаются хоть чуть-чуть.

А функция OnCollisionExit срабатывает всего один кадр -- когда касание прекратилось.

* На самом деле, не совсем: это физическая функция и она срабатывает каждый физический кадр. Детское объяснение в одну строку: Unity обрабатывает физику отдельно от не-физики: физика обрабатывается только в "физических кадрах", коими являются не все.

Опять к триггерам

Обсудим теперь триггеры. Вы можете попробовать отметить галочку "Is Trigger" в компоненте SphereCollider у шара. Запустив игру вы моментально поймете, что такое триггеры, если у вас пока не сложилось представление:)

Перейдите теперь на другую сцену, TriggersLesson, в папке "Scenes". Запустив игру вы увидите, что играете за капсулу, превращать которую в нормального героя мне было лень:)

Давайте сделаем так, чтобы при входе в горящую зону наш герой увеличивался, при нахождении в ней -- мигал, а при выходе - вновь уменьшался.

Помним, что эта зона -- триггер (можете в этом убедиться, в компоненте коллайдере отмечена галочка IsTrigger), а потому будем использовать триггерные функции. Найдем в папке Scripts файл Player.cs. Он уже содержит некоторые функции, отвечающие за перемещение игрока. Добавьте в конце еще несколько функций:

private void OnTriggerEnter(Collider other)
{
    if (other.gameObject.tag == "FireZone")
        transform.localScale *= 2;
}

Обратите внимание, что триггерные функции НЕ принимают в качестве аргумента объект типа Collision, т.е. информацию о столкновении, так как самого столкновения не было. Вместо этого они просто хранят ссылку на компонент-коллайдер того объекта, с которым столкнулись. В нашем случае, так как скрипт висит на герое, переменная otherссылаться на саму зону.

Мы проверяем, является ли она нашим триггером и увеличиваем объект.

Если оставить все как есть, то в круг можно несколько раз входить-выходить и достигнуть гигантских размеров. Давайте это исправим:)

private void OnTriggerExit(Collider other)
{
    if (other.gameObject.tag == "FireZone")
        transform.localScale /= 2;
}

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

private void OnTriggerStay(Collider other)
{
    if (other.gameObject.tag == "FireZone")
    {
        Color color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));
        GetComponent<MeshRenderer>().material.color = color;
    }
}

Эта функция создаст разноцветное случайное мерцание. Мы просто меняем цвет материала, примененного к мешу. Для этого мы обращаемся к компоненту MeshRenderer.

Кстати, каждый кадр использовать GetComponent() - плохая практика. Это сильно бьет по производительности. Поэтому компоненты стоит кэшировать. Я уже сделал это в 9-й и 14-й строках: объявил переменную и инициализировал ее в функции Awake.Теперь просто замените вызов GetComponent<MeshRenderer>() на переменную _mesh.

Кстати, закэшировал я и компонентRigidbody, который мне тоже нужен каждый кадр для перемещения персонажа.

Мне кажется, эта статья итак уже выходит довольно длинной, поэтому я опишу как использовать 2Д-версии функций столкновения в следующей статье (если она будет...)

ДЗ:

В качестве практики предлагаю вам поработать с обеими сценами. Например, можно сделать следующее:

  1. На первой сцене вместо скрипта для шарика напишите скрипт для кубика, который будет изменять цвет шарика на, скажем, зеленый, когда коснется его. Возьмите код изменения цвета из конца урока и убедитесь, что кубик случайно не красит еще и пол!

  2. На второй сцене выньте камеру из персонажа в иерархии, размножьте их на сцене и полностью перепишите их скрипт, чтобы они бегали в случайные стороны. А потом создайте скрипт для огненной зоны, в котором реализуйте все, что мы делали в этом уроке.

    Теперь у вас по карте бегает куча недоделанных миньонов, каждый из которых вырастает и мелькает, когда попадает в огненную зону!

На этом все!

P.S. Это моя первая статья. Буду рад увидеть комментарии с конструктивной критикой. Также готов ответить на вопросы в комментариях.

P.P.S. Кто-нибудь знает, куда люди обычно прикрепляют ссылки, такие как у меня здесь -- на папку с игрой? А то на гугл диске не очень хочется место занимать, а онлайн-файлообменники позволяют расположить файлы только на конкретные срок...

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


  1. Myxach
    11.01.2023 02:02

    Часто они совпадают с формой самого объекта (как в реальном мире), хотя это и не обязательно.

    Чаще всего они наоборот не совпадают, ибо объекты обычно сложнее. Цилиндрическая фигура мало похоже на гуманоидную, ну именно она используется для упрощеной колизии людей


    1. FismanMaxim Автор
      11.01.2023 20:59

      Согласен, неточная формулировка. Полный повтор сложных форм сильно бил бы по производительности. Хотя, пожалуй, в наработках начинающих используются довольно простые модели/спрайты и коллайдеры на них довольно простые:)


  1. maxmydoc
    11.01.2023 14:19

    Вопрос к автору.

    В свое время тоже баловался с Юнити и была проблема что при больших скоростях один объект вчеравно проникал в другой.

    Вопрос к вам. Что надо сделать чтобы объект с большой скоростью не проникал в другой.

    П.с. большая скорость например падающий объект под силой гравитации от компонента юнити с высоты 10м. Если честно я вспомнил что в игре КСП не решили толком эту проблему поэтому у себя я сам прописывал многие взаимодействия


    1. Tosha4389
      11.01.2023 20:23

      Решения этой проблемы нет ) Все упирается в производительность, особенно на мобильных устройствах.


      1. scuko-chaos
        13.01.2023 14:19

        Ну можно поиграться с частотой вызова FixedUpdate. Приблизить ее к частоте Update, например. Но производительность, да


    1. FismanMaxim Автор
      11.01.2023 21:06

      Проблема распространенная и может иметь несколько причин. Часто новички пытаются реализовать движение не через физическое перемещение (Rigidbody), а напрямую изменяя transform.position. Однако этот способ приемлем, только если нужно "телепортировать" один объект в другое место. При попытке использовать его в физическом перемещении, логично, что объекты могут проходить друг сквозь друга, особенно если их коллайдеры малы (узки).

      Если же движение реализовано правильно, может помочь установка поля Collision Detection на Continuous вместо Discrete. С другой стороны, это ударит по производительности в больших проектах. Проблема распространенная, о ней не раз спрашивали и писали.(https://forum.unity.com/threads/what-are-the-necessary-settings-to-prevent-objects-passing-through-each-other-at-high-speeds.384519/)


  1. Tosha4389
    11.01.2023 14:21
    +1

    Я не побоюсь показаться занудой, к сожалению... Мне кажется, что эта статья написана лишь для того, чтобы выйти из песочницы... ибо описаны настолько банальные вещи, что не понятно кому это будет полезно, учитывая сколько подобных туторов во всех мыслимых и не мыслимых формах можно нагуглить.

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

    К примеру, популярная проблема пролета коллайдеров через друг друга...я в свое время пару сотен раз сломал свои мозги в поисках решения....

    Хотел написать статью даже, да ленивый слишком и боюсь засмеют ))

    ЗЫ: Для хранения проекта пользуйтесь гитом


    1. maxmydoc
      11.01.2023 19:06

      Так напишите. Я в юнити просто ушёл от многих компонентов и писал все под себя


      1. Tosha4389
        11.01.2023 20:24

        Например? Какие компоненты Вы заменили своими фичами? Можно на код взглянуть ради интереса?


    1. FismanMaxim Автор
      11.01.2023 21:14

      Спасибо за отзыв и информацию.

      Увы, не похоже, что эта тема подробно разъясняется в курсах и туториалах, ибо на том же StackOverflow (как минимум на русском) с завидной регулярностью всплывают вопросы: "Почему коллайдеры не взаимодействуют", "Почему столкновение не обрабатывается", "Почему коллайдеры проходят свозь друг друга". Я даже могу с уверенностью заявить, что помню вопрос "Где и как вызвать функцию OnCollisionEnter?" Сейчас не могу найти - предполагаю, что вопрос закрыли.

      Пару лет назад я даже написал там вопрос-ответ с очень краткой информативной справкой и по просмотрам можно понять, что новички сталкиваются с проблемами по теме часто.


  1. GaricT
    11.01.2023 21:14

    Я бы в обязательном порядке добавил, что есть еще Collision action matrix (в самом низу документации): Introduction to collision. Столкновение не всякого коллайдера с другим коллайдером будет обработано, это не совсем очевидно.