Когда мы начинали проектировать архитектуру своих анимационных графов, мы, конечно же, смотрели на другие аналоги, в частности на Animator от Unity. Однако мы хотели сделать более универсальное решение. В отличие от того же Unity у нас есть кастомизация анимационных состояний через интерфейс контроллеров. Но для начала стоит разобраться, что такое анимационный граф состояний. Если вы уже с этим сталкивались, имеет смысл пропустить вводную часть и перейти к особенностям реализации.
Итак, что же это такое — анимационный граф состояний?
Анимационный граф состояний позволяет представить в графическом виде переходы между различными состояниями анимации.
Возьмем, например, анимацию персонажа:
У нас есть трехмерная модель человечка и есть несколько его анимаций:
- idle — стоит на месте;
- walk — идет вперед;
- sitting — сидит;
- hello — машет рукой.
Классический подход управления анимациями таков: если нужно, чтобы объект стоял — включаем idle, ходил — walk, сидел — sitting. Но с этим есть определенные сложности.
Во-первых, нужно вручную управлять длительностью и последовательностью анимаций. Например, чтобы человек сел, сначала нужно проиграть анимацию, как он садится, а потом начать играть зацикленную анимацию, где человек уже сидит. Подгонять в коде стыки этих анимаций сложно и неудобно.
Во-вторых, стыки между анимациями становятся заметны, если концы анимации не совпадают, или нам нужно включить другую анимацию посреди текущей. В этом случае просто невозможно сопоставить анимации идеально. Вспомните старые игры, где анимации персонажей переключались мгновенно.
Анимационный граф предназначен для решения этих вопросов. С ним вам не нужно оперировать анимациями вручную, теперь вы оперируете состояниями. Как объект будет анимироваться для достижения этого состояния — это работа аниматоров и дизайнеров. Теперь программист не задумывается о таймингах и последовательности анимации, он просто указывает, в какое состояние должен перейти объект.
Также с анимационным графом отпадает проблема стыковки анимаций. При переходе между состояниями мы можем сделать плавный переход одной анимации к другой. Это делается с помощью весов. Вес — это коэффициент смешивания от 0 до 1, где 0 означает, что анимация никак не влияет на объект, а 1 — полностью влияет.
Например, переход между ходьбой (walk) и стоянием (idle) очень требователен к настройке процесса. В любой момент анимации ходьбы персонаж может остановиться. Поэтому переход осуществляется не мгновенно, а за какой-то небольшой промежуток времени. В это время вес ходьбы убывает от 1 до 0, а вес стояния увеличивается от 0 до 1. Важно, чтобы сумма весов была равна единице, иначе могут появиться артефакты.
Как это все работает?
Граф состоит из состояний и переходов. Состояние — это набор анимационных контроллеров, каждый из которых может проигрывать какую-то анимацию на объекте или выполнять какую-то логику. Контроллер имеет точки входа и выхода — это те моменты, когда граф включает состояние с этим контроллером и выключает соответственно. Также контроллер имеет функцию обновления, где, помимо промежутка времени с прошлого кадра, приходит вес перехода. Для смешивания анимаций его нужно обязательно учитывать.
Контроллеры имеют единый интерфейс. Дополнительно разработчики могут добавлять свои контроллеры. Например, можно сделать контроллер, который выполняет какую-то логику или устанавливает текст на попапе и т.д. Эта простая кастомизация позволяет использовать анимационный граф очень гибко.
Также у нас есть переменные. Эти переменные можно выставлять извне, в том числе из кода, а затем читать их в контроллерах. Так, например, можно переключать какую-то анимацию у персонажа на одном и том же состоянии. В целом, можно даже повторить парадигму перехода между состояниями через переменные и условия, наподобие Unity. В связке с кастомизируемыми контроллерами получается довольно удобно.
Переходов может быть сколько угодно. Множество переходов может приходить в состояние и точно так же неограниченно выходить. Переходы определяют возможность достижения состояний. Например, если между состояниями A и F нет перехода напрямую, но есть цепочка A>B>C>D>E>F, то при запросе перехода из А в F граф сам поймет, что ему нужно пройти промежуточные состояния B, C, D, и E.
Переходы имеют настройки интервала начала и длительность. С длительностью все просто — это время, за которое будет осуществлен переход. А вот интервал уже посложнее: он определяет допустимый промежуток времени анимации, когда переход может быть начат.
Например, для того чтобы персонаж сел, сначала нужно проиграть анимацию, как он садится, а затем запустить анимацию сидения. В таком случае интервал перехода из «садится» в «сидит» должен быть в конце анимации «садится», чтобы мы увидели, как он садится, а затем в конце быстро, но плавно перейти в анимацию сидения.
Другой пример: персонаж идет и ему нужно остановиться. В таком случае интервал начала перехода должен быть на всю длину анимации, ведь персонаж может остановиться в любой момент.
Анимационный граф делает всю сопутствующую работу:
- планирует путь до необходимого состояния;
- обновляет работающие в данный момент состояния;
- осуществляет плавный переход между состояниями;
- регулирует веса в них.
Интересные возможности
В движке Playrix есть много разных типов анимаций: 3Dмодели, Spine, Flash, эффекты частиц, скелетная анимация. На каждый тип существует определенный контроллер.
Кроме простых анимационных контроллеров у нас есть несколько вспомогательных. Например, рандомизированный контроллер. Он может включать в себя список других контроллеров и вероятности их выбора. Каждый раз, когда объект переходит в состояние с таким рандомизированным контроллером, происходит случайный выбор с учетом вероятностей, и начинает функционировать выбранный контроллер. Остальные спят и бездействуют, дожидаясь своего момента.
Но иногда на одном состоянии нам нужно переключать анимации. Например, если у нескольких персонажей один и тот же граф, и у всех есть какая-то анимация действия. Один персонаж должен достать метлу и начать мести дорогу, другой достать фотоаппарат и начать фотографировать, третий ест мороженое. Для таких ситуаций есть специальный контроллер, который тоже содержит в себе список контроллеров, но, в отличие от рандомизированного, здесь он выбирает контроллер в зависимости от переменной.
Переменные задаются в графе и их можно менять извне, например из кода. В данном примере используется строковый тип, и каждому типу действия соответствует некое значение переменной. Когда в игре создается персонаж, то ему устанавливается эта переменная в зависимости от желаемого поведения.
Еще у нас есть контроллер, который может смешивать несколько анимаций. Например, можно смешивать анимацию ходьбы влево, вправо и вперед. Тем самым при поворотах можно регулировать веса между ними так, чтобы ноги персонажа не проскальзывали и ходьба выглядела естественно.
We need to go deeper
В том, что мы делаем свой Unity, есть масса плюсов. Один из них — мы можем сделать, как хотим и что захотим. А захотели мы неограниченную возможность расширения анимационного графа.
У нас есть интерфейс контроллера, есть несколько контроллеров «из коробки» и есть возможность имплементировать интерфейс и сделать в нем все, что угодно (и необязательно это будет анимация):
- изменить текст на кнопке;
- взаимодействовать с другими объектами в иерархии;
- и даже поуправлять другим графом анимаций.
Такой подход у нас использован в посетителях зоопарка в игре Wildscapes. Каждый посетитель имеет два графа: один для анимации модели, другой для анимации поведения.
Первый граф довольно простой, он управляет ходьбой, умеет проигрывать какие-то отдельные анимации персонажа.
Второй граф гораздо сложнее и имеет некие сценарии поведения. Например, сначала персонаж идет, затем сидит на скамейке, здоровается с кем-то, фотографирует и идет дальше. Это отдельная ветка с состояниями.
Данную логику можно было бы поместить и на первом графе, но тогда бы анимации дублировались множество раз. Но с двумя графами все гораздо проще. Управляющий граф содержит в себе цепочки состояний, включающие состояния из первого графа, который работает параллельно.
Что дальше?
Наш граф уже умеет многое, но есть еще большой простор для развития. В планах сделать группировку нескольких состояний, со вложенностью. Это позволит значительно упрощать графы с квестами. Также в планах работа по улучшению отображения графов и связей. Сейчас связи на больших графах напоминают спагетти (даже цвет похож), и порой в них легко запутаться.
Комментарии (12)
IvanKamynin
18.10.2019 22:14Вот, что мне нравится у вас, ребята — то, что к разработке вы подходите качественно: красивая графика, современные технологии, грамотный подход к продвижению кампании(многие ли игровые кампанми имеют блог на хабре, уж не говоря о слове — Интересный?) и многое другое. По статье увы, ничего написать не могу — все в статье и написано. Просто r3sp3c7 за качество, труд.
Breadpack
20.10.2019 11:11Грамотный подход продвижения игр != продвижение игры через Хабр. Тут игроков не так уж и много.
19as
18.10.2019 22:44Заинтересовал редактор графа. Не могли прокомментировать, используя какие технологии он сделан? Например, могу предположить реализацию с помощью фреймворка Qt или полностью собственная реализация на OpenGL.
anz Автор
20.10.2019 11:48У нас для UI редакторов используется ImGUI, простая и быстрая библиотека
19as
21.10.2019 19:14Спасибо. У него немного другие назначения, нежели чем у Qt.
Считаю, правильный выбор, если у вас не очень сложная логика между иерархией диалогов или виджетов и все нормально по скорости укладывается ( имею ввиду рендер), т.к. ImGUI постоянно все заново создаёт в процессе рендеринга кадра, даже если объекты уже были созданы и видны в текущем кадре. Как я понимаю, надо самому дополнительно строить некую систему кеширования объектов сцены.
oleginsite
19.10.2019 18:17-2Насколько я понял, вы велосипед изобрели. Потому что в Юнити все это реализовано. И плавная смена анимации и прочее.
anz Автор
20.10.2019 11:49По сути да, ничего нового здесь нет… Но зато мы сделали решение для себя, и гораздо более гибкое, чем в unity
undersky
19.10.2019 18:43На скрине выглядит как паутина. Можно ли ей будет пользоваться при росте сложности?
Пользуясь преимуществом некомпетентности, задам тупой вопрос — может вам сделать "декомпозицию"? То есть на верхнем уровне у вас какие-то обобщенные состояния, но можно декомпозировать на более мелкие детали (и так рекурсивно, при необходимости)? Это позволит каждый граф (модель) сохранить читаемым.anz Автор
20.10.2019 11:59Да, со сложными графами проблема. Как раз собираемся делать такую группировку
neurocore
Жёсткая сетка. Я вот думал о возможной реализации игровых событий и так и не смог прийти к единой структуре. По идее, событие должно быть как-то инициировано, значит нужны начальные условия (временная отметка или привязка с другим событиям). Событие должно быть завершено, значит есть условия завершения и статус завершения. А что ещё?
Epsiloncool
Я тоже в своё время задался этим вопросом и в итоге пришёл к реализации на модифицированных маркированных сетях Петри. Точки (маркеры) указывают на текущее состояние объектов, в то время как переходы могут быть условными и включать в себя несколько условий — таймер, например, или наличие определённо состояния в другом объекте. Штука получилась удобная, но оформить это в виде UI пока не могу — не хватает времени. Для себя использую на бумаге.
Уверен, что у ребят из Playrix тоже есть отличное бомбическое решение для этой задачи и они когда-нибудь им поделятся :)