Unity3D – Camera Shake. Как сделать подвижную камеру, чтобы улучшить отдачу от вашей игры

Доброго времени суток. Представлюсь, для тех, кто меня не знает. Меня зовут Дима. Я работаю C++ разработчиком уже более 5-ти лет. На данный момент работаю в крупной Gamedev-студии, предыдущее место работы: SK hynix memory solutions Eastern Europe. Помимо работы увлекаюсь созданием образовательного контента для YouTube и Twitch каналов.

Сложно подобрать аналог данного понятия в русском языке, чтобы он не звучал странно. Тем не менее, в данной статье речь пойдёт о вибрации камеры(Camera Shake) в играх и её реализации на движке Unity.

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

Зачем нужны подобные эффекты? Чтобы лучше передать ощущение от игры, чтобы игрок чувствовал большее погружение. Особенно важно это делать в PC-играх, где отсутствуют возможности тактильно сообщать игроку о полученном уроне через вибрацию геймпада.

Сравним взрывы в разных компьютерных играх и то, как они ощущаются без вибрации камеры:

И с ней:

Особенно хорошо ощущается разница передачи, если нет звука: в первом случае больше похоже на лопнувшую банку оранжевой краски, нежели на взрыв робота. Во втором случае же однозначно чувствуется, что произошёл взрыв космического корабля страшной силы. Хотя не могу не признать, что тряска чересчур уж сильная.

Приведённые выше примеры касаются только вибрации камеры. Есть и другие эффекты. Рассмотрим еще один эффект. Это направленное вращение камеры, имитирующее удар. С помощью этого эффекта можно отлично передать сильную отдачу оружия, полученный от оружия урон, либо же в целом сделать более живыми и правдоподобными анимации персонажа:

Думаю, вступлений достаточно, перейдём к реализации. За основу взят небольшой проект, в котором я на стримах реализую геймплейные механики из игры Doom Eternal на движке Unity.

Вибрация Камеры

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

//С помощью данной функции будет запущена вибрация извне данного класса public void ShakeCamera(float duration, float magnitude, float noize) { //Запускаем корутину вибрации StartCoroutine(ShakeCameraCor(duration, magnitude, noize)); } //Преимущество корутин в данной реализации очевидно //Если реализовыывать классически образом, используя функцию Update //Слишком много полей пришлось бы сохранять в полях класса private IEnumerator ShakeCameraCor(float duration, float magnitude, float noize) { //Инициализируем счётчиков прошедшего времени float elapsed = 0f; //Сохраняем стартовую локальную позицию Vector3 startPosition = transform.localPosition; //Генерируем две точки на "текстуре" шума Перлина Vector2 noizeStartPoint0 = Random.insideUnitCircle * noize; Vector2 noizeStartPoint1 = Random.insideUnitCircle * noize; //Выполняем код до тех пор пока не иссякнет время while (elapsed < duration) { //Генерируем две очередные координаты на текстуре Перлина в зависимости от прошедшего времени Vector2 currentNoizePoint0 = Vector2.Lerp(noizeStartPoint0, Vector2.zero, elapsed / duration); Vector2 currentNoizePoint1 = Vector2.Lerp(noizeStartPoint1, Vector2.zero, elapsed / duration); //Создаём новую дельту для камеры и умножаем её на длину дабы учесть желаемый разброс Vector2 cameraPostionDelta = new Vector2(Mathf.PerlinNoise(currentNoizePoint0.x, currentNoizePoint0.y), Mathf.PerlinNoise(currentNoizePoint1.x, currentNoizePoint1.y)); cameraPostionDelta *= magnitude; //Перемещаем камеру в нувую координату transform.localPosition = startPosition + (Vector3)cameraPostionDelta; //Увеличиваем счётчик прошедшего времени elapsed += Time.deltaTime; //Приостанавливаем выполнение корутины, в следующем кадре она продолжит выполнение с данной точки yield return null; } //По завершении вибрации, возвращаем камеру в исходную позицию transform.localPosition = startPosition; }

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

Чем длиннее будут векторы, тем больше от переходов от чёрного к белому(0..1) попадёт в нашу генерации и тем более дёрганной будет наша вибрация.
Чем длиннее будут векторы, тем больше от переходов от чёрного к белому(0..1) попадёт в нашу генерации и тем более дёрганной будет наша вибрация.

Преимущество использования функции Mathf.PerlinNoise, в отличие от обычной генерации случайных чисел, состоит в том, что близкие друг к другу координаты дадут близкие результаты и дельты получатся плавными.

Вращение камеры

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

public void ShakeRotateCamera(float duration, float angleDeg, Vector2 direction)

Где duration - время в секундах, продолжительность вращения камеры, angleDeg - угол в градусах на который камера будет отклонена от направления взгляда игрока, к которому она в итоге должна вернуться, direction - направление, в котором будет происходить наклон.
На примере это всё выглядит так, с помощью угла, а точнее тангенса угла наклона, мы вычисляем длину направляющего вектора, суммируем его с вектором нашего взгляда и получаем результирующий вектора направления камеры:

Зелёный отрезок - то, куда будет смотреть игрок в крайней точке своего вращения.
Зелёный отрезок - то, куда будет смотреть игрок в крайней точке своего вращения.

Перейдём к реализации функционала:

public void ShakeRotateCamera(float duration, float angleDeg, Vector2 direction) { //Запускаем корутину вращения камеры StartCoroutine(ShakeRotateCor(duration, angleDeg, direction)); } private IEnumerator ShakeRotateCor(float duration, float angleDeg, Vector2 direction) { //Счетчик прошедшего времени float elapsed = 0f; //Запоминаем начальное вращение камеры по аналогии с вибрацией камеры Quaternion startRotation = transform.localRotation; //Для удобства добавляем переменную середину нашего таймера //Ибо сначала отклонение будет идти на увеличение, а затем на уменьшение float halfDuration = duration / 2; //Приводим направляющий вектор к единичному вектору, дабы не портить вычисления direction = direction.normalized; while (elapsed < duration) { //Сохраняем текущее направление ибо мы будем менять данный вектор Vector2 currentDirection = direction; //Подсчёт процентного коэффициента для функции Lerp[0..1] //До середины таймера процент увеличивается, затем уменьшается float t = elapsed < halfDuration ? elapsed / halfDuration : (duration - elapsed) / halfDuration; //Текущий угол отклонения float currentAngle = Mathf.Lerp(0f, angleDeg, t); //Вычисляем длину направляющего вектора из тангенса угла. //Недостатком данного решения будет являться то //Что угол отклонения должен находится в следующем диапазоне (0..90) currentDirection *= Mathf.Tan(currentAngle * Mathf.Deg2Rad); //Сумма векторов - получаем направление взгляда на текущей итерации Vector3 resDirection = ((Vector3)currentDirection + Vector3.forward).normalized; //С помощью Quaternion.FromToRotation получаем новое вращение //Изменяем локальное вращение, дабы во время вращения, если игрок будет управлять камерой //Все работало корректно transform.localRotation = Quaternion.FromToRotation(Vector3.forward, resDirection); elapsed += Time.deltaTime; yield return null; } //Восстанавливаем вращение transform.localRotation = startRotation; }

Для составления полной картины: реализации, иерархии объектов, демонстрации работы, настройки параметров, - рекомендую ознакомиться с записью моего стрима, где всё это реализовано:

Запись довольно-таки объёмная, постараюсь в ближайшее время добавить таймкоды на самые интересные места.

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

Полезные ссылки:

3232
26 комментариев

Добавлю что тряска камеры через position лучше подходит для 2д игр, где камера всегда смотрит сбоку или сверху, вдали от объектов игрового мира. Для 3д игр, особенно для игр от третьего лица лучше всегда использовать тряску камеры через rotation, чтобы камера всегда оставалась на месте и не улетала, например, в стену, если персонаж находится в узком коридоре.

5
Ответить

Соглашусь. Думаю идеальным вариантом будет улучшить тряску камеры, добавив туда тряску через вращение. И сделать два параметра разброса настраиваемыми. То есть разброс позиции и разброс углов вращения.

Ответить

Комментарий недоступен

4
Ответить

А потом просто берём и накатываем Cinemachine, где всё это уже реализовано, удобно и бесплатно) По крайней мере для простых задач подходит отлично

2
Ответить

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

1
Ответить

Комментарий недоступен

2
Ответить

Вы Правы. Исправил в тексте статьи. Только в названии оставил.

1
Ответить