Как стать автором
Обновить

Как использовать функции столкновений в Unity: OnCollisionEnter/Stay/Exit, OnTriggerEnter…

Время на прочтение6 мин
Количество просмотров46K

Столкновения (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. Кто-нибудь знает, куда люди обычно прикрепляют ссылки, такие как у меня здесь -- на папку с игрой? А то на гугл диске не очень хочется место занимать, а онлайн-файлообменники позволяют расположить файлы только на конкретные срок...

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии12

Публикации

Истории

Работа

Ближайшие события

Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область