Столкновения (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Д-версии функций столкновения в следующей статье (если она будет...)
ДЗ:
В качестве практики предлагаю вам поработать с обеими сценами. Например, можно сделать следующее:
На первой сцене вместо скрипта для шарика напишите скрипт для кубика, который будет изменять цвет шарика на, скажем, зеленый, когда коснется его. Возьмите код изменения цвета из конца урока и убедитесь, что кубик случайно не красит еще и пол!
На второй сцене выньте камеру из персонажа в иерархии, размножьте их на сцене и полностью перепишите их скрипт, чтобы они бегали в случайные стороны. А потом создайте скрипт для огненной зоны, в котором реализуйте все, что мы делали в этом уроке.
Теперь у вас по карте бегает куча недоделанных миньонов, каждый из которых вырастает и мелькает, когда попадает в огненную зону!
На этом все!
P.S. Это моя первая статья. Буду рад увидеть комментарии с конструктивной критикой. Также готов ответить на вопросы в комментариях.