King, Witch and Dragon. Devlog #5

В прошлом девлоге я рассказывал об анимациях персонажа, которые я делал в Blender'е и экспортировал для настройки в Unity. В этом девлоге я расскажу о процедурных анимациях способностей персонажа, которые полностью или частично создаются и управляются из кода внутри движка. Будет много текста и много gif'ок.

Паучьи лапы

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

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

Концепт​
Концепт​

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

King, Witch and Dragon. Devlog #5

В свойствах задаётся минимальное и максимальное допустимое расстояние 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 тут не помогут, т.к. нам нужна именно точка на окружности. Для этого я использовал последовательный RayCast по касательной к окружности заданного радиуса, а если быть совсем точным, то это LineCast по секторам с поворотом на заданный угол:

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

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

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; }

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

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

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

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

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

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

Таким образом можно менять свойства для каждой отдельной лапы, создавая её профиль как Scriptable Object, при этом не создавая дополнительных префабов.

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

Многие из вас в комментариях, а также в социальных сетях, говорили, что было бы хорошо, если бы персонаж менял положение тела в зависимости от того, по какой поверхности ползёт (сгибался, поворачивался к стене, касался стены или перебирал руками по стене вместе с лапами и т.д.). Я согласен, что это звучит здорово, но я придерживаюсь идеологии "gameplay first".

С точки зрения геймплея, я хочу дать персонажу возможность сражаться, когда он ползёт по стене или потолку - как минимум, атаковать в 4 направлениях (вверх, вниз, влево, вправо). Если делать несколько адаптивных анимаций со сгибанием или поворотом к стене, а поверх этого еще делать несколько анимаций атаки, то объем работ и потраченного времени начинает увеличиваться в геометрической прогрессии.

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

Анимация персонажа

Для анимации персонажа я использовал подход со смешиванием анимаций, в зависимости от скорости движения персонажа.

Я сделал 5 зацикленных анимаций:

  • без движения (айдл на стене)
  • движение по стене вверх
  • движение по стене вниз
  • движение по потолку влево
  • движение по потолку вправо
King, Witch and Dragon. Devlog #5

В качестве основной позы я выбрал нейтральное положение, в котором ноги персонажа расслаблены и просто свисают вниз, а меч он держит двумя руками перед собой. Из такого положения легко сделать переход в анимацию атаки и при этом персонаж может перехватывать меч в нужную руку, в зависимости от направления атаки (сложно из такого положения атковать влево, держа меч в правой руке).

Находясь на стене или потолке, персонаж может двигаться не только влево-вправо, но и вверх-вниз. По этому я использовал двухмерный Blend Tree с двумя параметрами - горизонтальная и верткальная скорость персонажа.

​Красная точка показывает текущие значения скорости. Голубые круги показывают степень влияния каждой анимации на финальную анимацию персонажа.
​Красная точка показывает текущие значения скорости. Голубые круги показывают степень влияния каждой анимации на финальную анимацию персонажа.

Вот что получилось в итоге:

Я всё ещё не удовлетворен этой анимацией, так что я вернусь к ней когда будет больше контекста (как минимум анимации атаки).

Дополнительные возможности

Также, для паучьих лап я сделал возможность ползать по двигающимся из вращающимся платформам:

Бросок гадюки

По сути это рывок на короткую дистанцию, во время которого персонаж принимает форму змеи.

Концепт​
Концепт​

На открытой местности персонаж может делать рывок в 8 направлениях: вверх, вниз, влево, вправо и по диагоналям.

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

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

Комбинированная анимация

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

Риг и Анимация

Для начала я создал "ползучую" анимацию в Blender'е. Забегая вперед скажу, что для такого комбинированного подхода пришлось сделать 2 набора костей в хвосте. Один набор костей для волнообразного движения. На эти кости заскинен меш змеи. Второй набор костей нужен чтобы манипулировать ими в движке. При этом каждая отдельня кость первого набора является child'ом соответствующей кости второго набора и анимируется отностельно неё.

Анимация выглядит так:

Да, я знаю, что змеи так не ползают. Но так как в игре камера сбоку, такая анимация лучше читается (при этом она хорошо читается со всех ракурсов, хоть сверху, хоть сбоку, хоть спереди). И по факту при рывке змея не ползёт, а летит.

После интеграции её в игру получился вот такой простой рывок (пока что без FX'ов и анимаций перехода из одного состояния в другое):

Процедурная анимация

Первой идеей было сделать в лоб и повесить на каждую кость сферический коллайдер, соединить их hinge joint'ами и пускай себе болтаются в тоннеле. Но эту идею я очень быстро отмёл.

Во-первых у меня в игре очень мало "честной" физики. Контроллер персонажа полностью кинематик и использует физ-движок только для нахождения коллизий вокруг себя с помощью всякого рода RayCast'ов и CapsuleCast'ов. Да и вообще недетерминированная физика в Unity меня неочень радует.

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

По этому пришлось писать свою реализацию поведения хвоста змеи.

Прототип на плоскости

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

Выглядело это вот так:

В принципе всё работало так, как я и ожидал. У меня не было цели сделать "ultimate snake controller" и я решил остановиться на текущей реализации, не смотря на то, что у неё были свои ограничения и недостатки, но меня они устраивали.

Собственно, скрипт контроллера змеи тут, если кому интересно.

Испытания на реальной змее

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

А потом и с анимацией:

Адаптация под 8 направлений

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

Финальный вариант выглядит так:

Анимация персонажа

Для этой способности я сделал простую анимацию перехода персонажа в форму змеи и выхода из нее.

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

Анимация персонажа в слоумо без эффектов выглядит вот так:

Щупальце

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

​Концепт
​Концепт

Процедурная генерация всего

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

В момент активации способности игра начинает в реальном времени

  • генерировать модель щупальца с присосками
  • назначать UV-координаты точкам
  • накладвать материал с текстурой
  • рассчитывать нормали поверхности
  • изгибать, скручивать и смещать щупальце в соотвествии с заданными параметрами

Ручное прописывание массивов точек, UV-координат, треугольников и прочего, даже с использованием циклов, то ещё удовольствие. Но если вам всё таки интересно глубже изучить тему процедурной генерации моделей в Unity, я рекомендую почитать статью на Catlike Coding и посмотреть серию видео на канале Board To Bits

Что получилось?

Для начала, я сделал щупальце, которое принимает базовый список параметров и применяет их ко всей модели:

На gif-ке показано изменение в реальном времени толщины щупальца, скручивания и отступа от центральной оси. Помимо этого можно задавать размер присосок, их количество и расстояние между ними.

Даже так щупальце уже выглядит приемлимо, но я решил на этом не останавливаться.

Если присмотреться к концепту выше, то можно заметить, что щупальце вначале несколькими витками скользит и "наматывается" на руку главного героя, а затем уже более широкими витками извивается в воздухе. Такого эффект нельзя добиться при равномерном (линейном) распределении параметров, по этому я сделал так, что каждый параметр можно контролировать с помощью Animation Curve (в данном случае не имеет ничего общего с анимацией и используется для задания нелинейного распределения).

С помощью этого инструмента можно, например:

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

Вот так щупальце выглядит, относительно статичного персонажа:

Анимация персонажа

Для щупальца я сделал анимацию вытягивания руки в напралении броска щупальца.

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

Рука корректно "смотрит" в нужном направлении, но смотрится это плохо, потому что плечо и торс персонажа никак на это не реагируют. В реальности мы физически не можем поднять руку выше уровня плеча, не подвинув само плечо, строение нашего скелета нам это не позволяет. Также, при прицеливании вниз, рука проходит сквозь ногу персонажа.

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

Получилось вот так:

Направление взгляда

Также я сделал так, что персонаж в любом состоянии поворачивает голову в направлении инпута игрока. Особенно хорошо это ощущается при игре с геймпадом, когда персонаж плавно поворачивает голову в направлении стика.

Это мелкая деталь, но она хорошо усиливает ощущение того, что персонаж "слушается" игрока.

Что дальше?

В этом месяце проходит фестиваль Keep Calm Do Games и я участвую в нём со своим проектом. Для подачи заявки надо сделать играбельный прототип и записать трейлер. Так что следующим шагом будет создание презентабельной локации и сборка тестового билда.

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

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

221221
46 комментариев

Добавь, плыз:

1. Ветку прокачки как в Path of Exile.
2. Мир побольше с уникальными биомами, чтоб можно было рыбу ловить и охотиться и ремеслом заниматься.
3. Чтоб навыки качались от использования.
4. Чтоб скиллы можно было комбинировать (лёд+вода как в Magicka).
5. Различные квесты с сюжетными линиями и озвучкой. Всякие твисты и драматизм не помешали бы.
6. Графику покруче, хотя бы как в Зельде последней.
7. Физику и разрушаемое окружение. Чтобы можно было разбить стену, кинуть фаерболл в траву а она сгорела. Чтобы каждый объект был разрушаемым.

Ну и всё в принципе! Игра четенькая будет.

17
Ответить

Суть такова...

51
Ответить

Корованы еще. 

23
Ответить

Скрытый клинок добавить и можно будет назвать ассасином

3
Ответить

Только не прокачка навыков от использлвания, пажожда

Ответить

Давно слежу за прогрессом.
Это очень круто в принципе, а для одного человека - и подавно. По своему опыту знаю, что даже задачи, кажущиеся очевидными (внятное управление для платформинга, например), зачастую выливаются в долгий тюнинг параметров, физики и прочего. А тут куча техник, каждая из которых требует отдельного разбирательства, да еще и хватает времени и сил все это подробно и понятно расписывать. 
Удачи!

15
Ответить

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

6
Ответить