King, Witch and Dragon. Отчёт 1

Пару дней работал над процедурной анимацией паучьих лап для способности "карабканье по стенам и потолку".

Описание фичи

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

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

​Концепт арт сделанный несколько месяцев назад, на старте проекта
​Концепт арт сделанный несколько месяцев назад, на старте проекта

Итого, повежение паучих лап можно тезисно описать так:

  • Используют IK для постановки в нужное место
  • Появляются при "приземлении" стену
  • Пропадают при прыжке от стены или при выходе на горизонтальную поверхность
  • Должны уметь шагать по стенам и потолку разного наклона
  • Должны уметь огибать внешние и внутренние углы
  • Не должны сильно рябить и визуально "шуметь" и просто смотреться странно

Поехали! (с)

Посмотрев на список требований, я составил список параметров, который должна иметь каждая лапа. Эти параметры я вынес в Scriptable Object, чуть дальше объясню зачем я это сделал. В скрипте на локе указывается референс на этот Scriptable Object и все параметры берутся с него.

King, Witch and Dragon. Отчёт 1

В свойствах задаётся минимальное и максимальное допустимое расстояние IK-таргета (конца лапы) от персонажа. Если таргет выходит за этот диапазон, лапа делает шаг. Также задаётся положение таргета по умолчанию, если лапа не может найти "землю". Задаются свойства для поиска точки на поверхности, а также время на совершение шага, плюс кривые для интерполяции или твинига шага (ускорение, замедление и т.д.) и высота шага.

Определяем момент когда нужно сделать шаг

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

Упрощённый код для этого выглядит так (на самом деле всё немного сложнее, но тем не мене):

private void UpdateLeg() { // Считаем расстояние до IK-таргета float dist = (_currentTargetPosition - transform.position).magnitude; // Сравниваем расстояние с пороговыми значениями if (dist > _profile.maxDistanceFromRoot || dist < _profile.minDistanceFromRoot) { // Если вышли из диапазона, ищем новую точку на поверхности делаем шаг GetNewTargetPosition(_profile.defaultRaycastDistanceFromRoot); StartCoroutine(MovetargetToNewPosition()); } }

Находим новую точку на поверхности

Для этого мы также используем окружность, потому что, также как и в прошлом параграфе, мы не знаем заранее под каким углом будет находиться поверхность, на которую лапа попытается встать. Вот только стандартный CircleCast или SphereCast тут не помогут, т.к. нам нужна именно точка на окружности. Для этого я использовал последовательный LineCast по касательной к окружности заданного радиуса, а если быть совсем точным, то это LineCast по секторам с поворотом на заданный угол:

Желтая линия - отступ от основания лапы. Красная линия - RayCast​

Функция для поиска точки на поверхности с использованием этого метода выглядит так:

private bool GetNewTargetPosition(float distanceFromRoot) { // По умолчанию берем дефолтное положение в качестве новой позиции таргета _newTargetPosition = transform.TransformPoint(_profile._targetDefaultPosition); //Определяем начальную и конечную точку лайнкаста Vector3 linecastStartPos = transform.up * distanceFromRoot; Vector3 linecastEndPos = linecastStartPos; // Поворачиваем конечную точку на заданный угол Quaternion rotation = Quaternion.AngleAxis(_profile.raycastAngularStep, transform.forward); int steps = Mathf.CeilToInt(180 / Mathf.Abs(_profile.raycastAngularStep)); // Кидаем лайнкаст с заданным шагом пока не найдем точку // или пока не пройдём дугу 180 градусов for (int i = 0; i < steps; i++) { linecastEndPos = rotation * linecastStartPos; if (Physics.Linecast(transform.position + linecastStartPos, transform.position + linecastEndPos, out RaycastHit hit, _character.Profile.climableWallsLayerMask)) { // Точка найдена, выходим из функции _newTargetPosition = hit.point; return true; } linecastStartPos = linecastEndPos; } // Точка не найдена, используем дефолтную позицию return false; }

Ну и в нагрузку код для перемещения таргета из предыдущего полгожения в новое:

private IEnumerator MovetargetToNewPosition() { // Определяем направление верткального смещения // для "поднятия" лапы над поверхностью Vector3 offsetDirection = Vector3.Cross((_newTargetPosition - _previousTargetPosition).normalized, transform.forward); // Начинаем корутин float accumulatedTime = 0f; while (accumulatedTime < _profile.transitionDuration) { accumulatedTime += Time.deltaTime; // Получаем занчение текущего прогресса в диапазоне [0,1] float progress = Mathf.InverseLerp(0f, _profile.transitionDuration, accumulatedTime); // Ремапим его в соответствии с заданной кривой твининга float remappedProgress = _profile.transitionTweening.Evaluate(progress); // Положение таргета на прямой линии Vector3 interpolatedPosition = Vector3.Lerp(_previousTargetPosition, _newTargetPosition, remappedProgress); // Получаем значение вертикального смещения в зависимости от прогресса float verticalOffset = _profile.transitionVerticalOffset.Evaluate(progress) * _profile.maxVerticalOffset; // Положение таргета с учетом смещения над поверхностью _currentTargetPosition = interpolatedPosition + offsetDirection * verticalOffset; yield return 0f; } }

Синхронизация лап

С одной лапой всё выглядело и работало круто, до тех пор, пока я не прицепил персонажу 4 лапы, по 2 с каждой стороны...

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

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

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

После этого я создал 4 Scriptable Object'a с параметрами лап и в каждом из них немного поменал значения, так что каждая лапа движется немного по разному (разное время на совершение шага, разная высота над поверхностью, разные минимальные и максимальные расстояния). Это мало заметно в движении, но при остановке видно, что лапы стоят не симметрично.

Конечный результат:

ДИСКЛЕЙМЕР: поза персонажа - заглушка. Я сделаю специальную анимацию для этого состояния, но сразу скажу, что персонаж так и будет всегда в вертикальном положении. Это выглядит немного странно и не то, что сходу ожидаешь, но зато это позволит в будущем сделать атаки в 4 направлениях (влево, право, вверх, вниз) пока персонаж ползёт по стенам и потолку. Мне кажется, что такая геймплейная свобода важнее, чем персонаж "красиво" прилегающий к стене.

Что дальше?

На этом с паучьими лапами пока что всё. Следующая на очереди змея с её процедурной анимацией хвоста при рывке.

Ну а пока, если вам интересен проект, подписывайтесь на меня в Instagram и Twitter, а также вступайте в группу VK, в которой я публикую апдейты и материалы по проекту до того как они попадают сюда.

Спасибо за внимание!

3030
4 комментария

Это так странно и круто одновременно о.О

1
Ответить

Круто, но крипово. Мы точно за героя играем?

Ответить

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

Ну и изначальная задумка была сделать визуально все способности такими, чтобы они так или иначе ассоциировались с Ведьмой (меч с перьями вороны, крылья летучей мыши, паучьи лапы и вот это всё).

1
Ответить

Интересный матан. И выглядит очень круто!

Ответить