Анимационный граф состояний

Привет! Мы тут в Playrix решили сделать свой Unity3D. А там есть Animator. В этой статье я расскажу, как мы сделали его у себя и как он работает.

67ba579dec3dab6038fcdfc5cc9aad25.png


Когда мы начинали проектировать архитектуру своих анимационных графов, мы, конечно же, смотрели на другие аналоги, в частности на Animator от Unity. Однако мы хотели сделать более универсальное решение. В отличие от того же Unity у нас есть кастомизация анимационных состояний через интерфейс контроллеров. Но для начала стоит разобраться, что такое анимационный граф состояний. Если вы уже с этим сталкивались, имеет смысл пропустить вводную часть и перейти к особенностям реализации.

Итак, что же это такое — анимационный граф состояний?


b20d16b789def0dd88b8a65ee6deb99d.png


Анимационный граф состояний позволяет представить в графическом виде переходы между различными состояниями анимации.

Возьмем, например, анимацию персонажа:

535bf0e940d917fc81734e0747a71de1.png


У нас есть трехмерная модель человечка и есть несколько его анимаций:  

  • idle — стоит на месте;
  • walk — идет вперед;
  • sitting — сидит;
  • hello — машет рукой.


Классический подход управления анимациями таков: если нужно, чтобы объект стоял — включаем idle, ходил — walk, сидел — sitting. Но с этим есть определенные сложности.

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

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

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

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

Например, переход между ходьбой (walk) и стоянием (idle) очень требователен к настройке процесса. В любой момент анимации ходьбы персонаж может остановиться. Поэтому переход осуществляется не мгновенно, а за какой-то небольшой промежуток времени. В это время вес ходьбы убывает от 1 до 0, а вес стояния увеличивается от 0 до 1. Важно, чтобы сумма весов была равна единице, иначе могут появиться артефакты.

Как это все работает?


94dfc8f3609b5f7353813ee40fa071b6.gif


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

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

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

Переходов может быть сколько угодно. Множество переходов может приходить в состояние и точно так же неограниченно выходить. Переходы определяют возможность достижения состояний. Например, если между состояниями A и F нет перехода напрямую, но есть цепочка A→B→C→D→E→F, то при запросе перехода из А в F граф сам поймет, что ему нужно пройти промежуточные состояния B, C, D, и E.

ca5a755065638088486c8aaae5e30eb3.png


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

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

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

Анимационный граф делает всю сопутствующую работу:

  • планирует путь до необходимого состояния;
  • обновляет работающие в данный момент состояния;
  • осуществляет плавный переход между состояниями;
  • регулирует веса в них.


Интересные возможности


В движке Playrix есть много разных типов анимаций: 3Dмодели, Spine, Flash, эффекты частиц, скелетная анимация. На каждый тип существует определенный контроллер.

Кроме простых анимационных контроллеров у нас есть несколько вспомогательных. Например, рандомизированный контроллер. Он может включать в себя список других контроллеров и вероятности их выбора. Каждый раз, когда объект переходит в состояние с таким рандомизированным контроллером, происходит случайный выбор с учетом вероятностей, и начинает функционировать выбранный контроллер. Остальные спят и бездействуют, дожидаясь своего момента.

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

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

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

We need to go deeper


c17ad819664185e383a0a5942fcbf7ef.png


В том, что мы делаем свой Unity, есть масса плюсов. Один из них — мы можем сделать, как хотим и что захотим. А захотели мы неограниченную возможность расширения анимационного графа.

У нас есть интерфейс контроллера, есть несколько контроллеров «из коробки» и есть возможность имплементировать интерфейс и сделать в нем все, что угодно (и необязательно это будет анимация):

  • изменить текст на кнопке;
  • взаимодействовать с другими объектами в иерархии;
  • и даже поуправлять другим графом анимаций.


Такой подход у нас использован в посетителях зоопарка в игре Wildscapes. Каждый посетитель имеет два графа: один для анимации модели, другой для анимации поведения. 

Первый граф довольно простой, он управляет ходьбой, умеет проигрывать какие-то отдельные анимации персонажа.

Второй граф гораздо сложнее и имеет некие сценарии поведения. Например, сначала персонаж идет, затем сидит на скамейке, здоровается с кем-то, фотографирует и идет дальше. Это отдельная ветка с состояниями. 

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

e1e4c7eb5f1ca273a001c98e5c61c6e6.png


Что дальше?


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

© Habrahabr.ru