Как научить робота ходить или процедурная анимация в Unity

Как научить робота ходить или процедурная анимация в Unity

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

Инверсная кинематика(IK)

В основне процедурных анимаций лежит инверсная кинематика. Задачей инверсной кинематики является поиск такого набора конфигураций сочленений, который обеспечил бы максимально мягкое, быстрое и точное движение к заданным точкам. Проще говоря инверсионная кинематика решает вопрос, как передвинуть цепь связанных костей, чтобы это было плавно и красиво. В конце статьи я оставлю ссылки для более подробного ознакомления с этой темой.
В Unity есть дополнительный пакет Animation Rigging, в котором есть реализация IK.

Window->Package Manager->Выбирайте пакеты, созданные Unity и устанавливаете Animation Rigging.
Window->Package Manager->Выбирайте пакеты, созданные Unity и устанавливаете Animation Rigging.

Когда мы установили этот пакет, начинаем подключать инверсионную кинематику к нашему роботу.

Наш подопытный :3
Наш подопытный :3

Теперь мы должны добавить компоненты Bone Renderer и Rig Builder к контейнеру, содержащему кости робота.

У нас добавился объект Rig 1, в нем мы будем добавлять инверсионную кинематику для наших костей, переименую его в FootRig.

Далее в нем создаем пустые объекты на каждую ногу робота.

Аналогично для каждой ноги.

Когда мы это сделали, добавим к новосозданным объектам компонент Two Bone IK Constraint.

Аналогично для каждой ноги. 

С описанием каждого компонента Animation Rigging можно ознакомиться в официальной документации Unity, но если коротко, с помощью Two Bone IK Constraint мы создаем конечность, которой мы задаем цель и ограничители, чтобы при перемещении к заданной точке она вела себя как реальная кость, не деформируясь.

В поля Root/Mid/Top нужно перенести кости нашего робота:

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

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

Также, для инверсионной кинематики лапок нашего робота можно добавить Hints. Они используются для указания направления, в котором конечность должна быть повернута. Этот компонент необязателен, но для робота я лучше их задам. Так как они лишь задают, как конечность должна быть повернута или наклонена, хинты можно добавить в контейнер FootRig. Далее перекидываем их в соответствующее поле на Two Bone IK Constraint, как мы это сделали с таргетами.

Хинты ставлю чуть выше колена

После всех этих несложных операций, вы запустите сцену и увидите результат:

Ножки дрыгаются. Но он всё еще не ходит. Что же заставит его перебирать лапки?

Учим робота ходить

Чтобы робот ходил, придется кодить. Сначала разберемся, что нам нужно сделать.

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

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

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

Код FootMover, с комментариями:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class FootMover: MonoBehaviour { public Vector3 NewTarget { get; set; } [SerializeField] private Transform _targetPoint; [SerializeField] private float _distance; [SerializeField] private float _maxHeightDistance; [SerializeField] private float _countLerpPosition = 0.4f; [SerializeField] private float _countLerpHeight = 0.5f; [SerializeField] private float _speed = 5; [SerializeField] private float _amplutide = 0.4f; private float currentTime = 1f; private void Start() { NewTarget = _targetPoint.position; } private void Update() { RaycastHit hit; // Переменная, в которой храним информацию о попадании луча. if (Physics.Raycast(transform.position, -transform.up, out hit)) { if ((Vector3.Distance(hit.point, _targetPoint.position) > _distance)) // Проверяем расстояние между попаданием рейкаста и текущим таргетом. { currentTime = 0; NewTarget = hit.point; // Задаем в свойство NewTarget координаты новой точки (Это нужно будет нам далее) } if (currentTime < 1) { Vector3 footPosition = Vector3.Lerp(_targetPoint.position, hit.point, _countLerpPosition); // С помощью линейной интерполяции плавненько переходим из текущей точки, в новую. footPosition.y = Mathf.Lerp(footPosition.y, hit.point.y, _countLerpHeight) + (Mathf.Sin(currentTime * Mathf.PI) * _amplutide); // С помощью линейной интерполяции и синуса задаем новую высоту лапки. _targetPoint.position = footPosition; // Меняем позицию таргета на новую. currentTime += Time.deltaTime * _speed; } } } }

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

Советую ставить дистанцию на каждую лапку разную, чтобы робот не перебирал ими одновременно.

Теперь, смотрим, научился ли ходить наш робот.

Он и правда ходит!!!

ОН ХОДИТ!!!

Изменение высоты тела

Добавим ему немного препятствий:

Проблема в том, что лапки-то он перебирает, а высоту тела не меняет. Нам придется объяснить ему, как это делать. Для этого напишем скрипт BodyHeight:

using System.Collections.Generic; using UnityEngine; public class BodyHeight: MonoBehaviour { [SerializeField] private List<FootMover> _targetFootPoints; private void Update() { CalculateHeight(); } public void CalculateHeight() { // Мы определяем новую высоту для тела через среднее значение высоты ног. float sum = 0f; for (int i = 0; i < _targetFootPoints.Count; i++) { sum += _targetFootPoints[i].NewTarget.y; // Для этого нам и понадобилось свойство в FootMover'е } float newHeight = sum / _targetFootPoints.Count; transform.position = new Vector3(transform.position.x, newHeight, transform.position.z); } }

ВАЖНО

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

Идём дальше

Добавляем этот скрипт на самого робота и вносим FootMover'ы в поле.

Теперь робот точно должен себя правильно вести:

Поворот тела

Он ходит, поднимается и опускается, но не наклоняется, когда нужно. Исправим это, написав скрипт BodyRotator:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class BodyRotator : MonoBehaviour { [SerializeField] private Transform _leftForwardFoot; [SerializeField] private Transform _leftBackFoot; [SerializeField] private Transform _rightForwardFoot; [SerializeField] private Transform _rightBackFoot; [SerializeField] private Transform _centralBackFoot; [SerializeField] private float _rotateSpeed; [SerializeField] private float _koef; private float forwardRotation; private float lateralRotation; private void Update() { Rotate(); } private void Rotate() { // Изменяем наклон тела с помощью разницы между лапками: Передний наклон - разница между передними и задними лапками; Боковой - разница между, очевидно, боковыми лапками forwardRotation = (_leftForwardFoot.transform.position.y + _rightForwardFoot.transform.position.y) - (_leftBackFoot.transform.position.y + _rightBackFoot.transform.position.y); lateralRotation = ((_leftForwardFoot.transform.position.y + _leftBackFoot.transform.position.y) - (_rightForwardFoot.transform.position.y + _rightBackFoot.transform.position.y)); if (Mathf.Abs(forwardRotation) < 1.5f) // Слишком уж маленькие повороты не учитываем { forwardRotation = 0.0f; } if (Mathf.Abs(lateralRotation) < 1.5f) { lateralRotation = 0.0f; } Vector3 newRotation = new Vector3(forwardRotation * -_koef, transform.eulerAngles.y, lateralRotation * -_koef); //Умножаем наклоны на коэффицент. float currentLateralRotationMoving = Mathf.LerpAngle(transform.eulerAngles.x, newRotation.x, _rotateSpeed); //Добиваемся плавного наклона с помощью линейной интерполяции float currentForwardRotationMoving = Mathf.LerpAngle(transform.eulerAngles.z, newRotation.z, _rotateSpeed); transform.rotation = Quaternion.Euler(new Vector3(currentLateralRotationMoving, transform.eulerAngles.y, currentForwardRotationMoving)); } }

Добавим этот скрипт на контейнер, хранящий кости и инверсионную кинематику нашего робота:

Теперь запускаем сцену и смотрим на нашего робота:

Посмотрим, как себя ведёт наш робот на полосе препятствий.

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

Как научить робота ходить или процедурная анимация в Unity

Ссылки на источники

Итог

У этой системы есть несколько недостатков, которые я попытаюсь в скором времени решить. А именно:

  • Роботу не совсем понятно, как себя вести на краю поверхности. Он начинает закручиваться и Вселенная схлопывается.
  • Иногда его подёргивает на наклонной поверхности(как видно в финальном испытании).

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

Надеюсь, вам понравилось. Спасибо, что дочитали до конца.

143143
11
9 комментариев
5000 ₽

Как научить робота донатить?

(победил в конкурсе, красаучик)

7
Ответить

Благодарю за статью. Уже месяц какдый день говорю себе, что нужно начать учить IK в unity, но все руки не дотягиваются.

3
Ответить

Рад, что оказался полезен)

1
Ответить

Робот збс! Лонг збс!

1
Ответить
1
Ответить

Респект за статью. Сижу на другом движке, но это пока, надеюсь освоить Унити.

1
Ответить