Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

Всем привет. На связи Fair Pixel. В этот раз мы расскажем про анимации в проекте Erra: Exordium. О том как они повлияли на геймплей и на каком велосипеде мы едем.

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

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

С чего мы начали...
...и к чему пришли.

Вслед за героем претерпели видимых изменений противники, как в плане движений, так и в плане видимой атаки. Но о противниках мы расскажем в другом выпуске нашего дневника.

Это лишний раз показывает важность предпродакшна и четкого видения геймплея.

А теперь про велосипед “Аниматор”

Без купюр и возможно с позором. В главных ролях: Unity Animator, программисты Fair Pixel.

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

Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

Первый Pipeline был следующим: художник давал кадры программисту или сам собирал их в анимацию в Unity (просто набор последовательных кадров, иногда с разным межкадровым интервалом), а программист добавлял анимацию в аниматор и настраивал связи.

Количество анимаций росло. Усложнялись состояния. Увеличивалось количество переходов. Это привело нас к тому, что кадры анимаций стали анимациями в аниматоре. И тут ты наверное крутишь палец у виска?!

Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

То ли из-за отсутствия опыта, то ли из-за страха перед увиденным, мы решили, что для нас стандартный Unity Animator слишком шикарен.

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

Во-вторых, мы решили отделить руки с огнестрельным оружием от тела, чтобы не рисовать всего персонажа целиком с оружием на каждый вид состояния. Представь себе, что персонаж с пистолетом может целится стоя, сидя, в движении. И оружие при этом не крутится как объект. Отдельное положение оружие - это конкретный кадр. Типа трупиксель! А потом ещё доставать оружие, прятать, перезаряжать и опять стоя, сидя, в движении… Прогрессия!

Поэтому было решено написать свой велосипед. Который был бы проще в настройках и решал бы вот всё выше написанное. В итоге информация про анимацию хранится в контейнере (благодаря ScriptableObject) и описана тремя полями: идентификатор, межкадровый интервал и набор спрайтов.

Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

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

Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

С появлением новых требований, менялась архитектура. На данный момент часть архитектуры выглядит так:

Дневник разработки Erra: Exordium #2: Покадровая анимация, код и велосипед

AnimationManager находится в каждом персонаже. Он хранит и управляет слоями анимации. LayerController содержит в себе перечень всех анимаций слоя. Анимацией управляет AnimationController. Таким образом, данные анимаций из ScriptableObject превращаются в AnimationController, в котором присутствует набор команд и событий.

Приведем пример на псевдокоде.

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

public class CharacterExample : IActor { private AnimationManager _animations; private SpriteRenderer _sr; private StateManager _states; public AnimationManager Animations { get { return _animations; } } public StateManager State { get { return _states; } } public CharacterExample(Animation[] animations) { _animations = new AnimationManager(_sr, animations); _states = new StateManager(); _states.Add(States.IDLE, new IdleState(this)); _states.Add(States.JUMP, new JumpState(this)); } public void Update() { _animations.Update(); _states.Update(); } }

Класс IdleState демонстрирует простой вызов смены анимации при старте состояния Idle.

public class IdleState : State { private CharacterExample _character; public IdleState(CharacterExample character) : base(character) { _type = States.IDLE; } public void Start() { _character.Animations.ChangeAnimation(AnimationsLayers.BASE, Animations.IDLE); } public void Update() { if (Input.Jump.Down) { _character.State.Set(States.JUMP); } else if (Input.Horizontal != 0f) { _character.State.Set(States.WALK); } } }

Класс JumpState демонстрирует варианты вызовов и события анимаций. При запуске состояния, аниматор переключится на анимацию JUMP. Затем произойдет переключение на анимацию JUMP_MOTION, когда сработает условие в событии FrameHandler. По завершению той или иной анимации, сработает переход в состояние IDLE. Ещё раз повторим, что это псеквдокод… Простая демонстрация некоторых возможностей.

public class JumpState : State { private CharacterExample _character; private AnimationController _animationJump; private AnimationController _animationJumpMotion; public JumpState(CharacterExample character) : base(character) { _type = States.JUMP; _animationJump = _character.Animations.Get(AnimationsLayers.BASE, Animations.JUMP); _animationJump.FrameHandler += AnimationJump_FrameHandler; _animationJump.EndHandler += AnimationJump_EndHandler; _animationJumpMotion = _character.Animations.Get(AnimationsLayers.BASE, Animations.JUMP_MOTION); _animationJumpMotion.EndHandler += AnimationJump_EndHandler; } public void Start() { _character.Animations.ChangeAnimation(Characters.AnimationsLayers.BASE, Characters.Animations.JUMP); } private void AnimationJump_FrameHandler(AnimationController controller) { if (controller.CurrentFrame > 2) { _character.Animations.ChangeAnimation(AnimationsLayers.BASE, Animations.JUMP_MOTION); } } private void AnimationJump_EndHandler(AnimationController controller) { _character.State.Set(States.IDLE); } }

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

И вот мы "победили", казалось бы, такую небольшую, но такую важную вещь. Зато теперь живем в мире и согласии с кодом и анимациями.
Нам будет очень интересно выслушать ваши примеры решения собственных велосипедов. А может у вас есть советы как можно было решить нашу проблему.
Будем рады услышать каждого!

69
8 комментариев

Это да, аниматор юнити не самый приятный товарищ. 

3
Ответить

не только нам так показалось?

1
Ответить

анимации прикольные)  желаю удачи) 

2
Ответить

Спасибо)

1
Ответить