{"id":2223,"title":"\u0417\u0430\u0433\u0430\u0434\u043a\u0438 \u0414\u0440\u0435\u0432\u043d\u0435\u0433\u043e \u0415\u0433\u0438\u043f\u0442\u0430: \u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u043a\u0432\u0435\u0441\u0442 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432","url":"\/redirect?component=advertising&id=2223&url=https:\/\/tjournal.ru\/special\/egypt&hash=e984e3ad6f09a93c4ac1d44a0c67c5ea94b453eb4900281befab21cab31bf040","isPaidAndBannersEnabled":false}
Инди
Fair Pixel

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{ "author_name": "Fair Pixel", "author_type": "self", "tags": ["\u043b\u0438\u0447\u043d\u044b\u0439\u043e\u043f\u044b\u0442","\u0434\u0435\u0432\u043b\u043e\u0433","\u0431\u0443\u0434\u0435\u0442\u043f\u043e\u043b\u0435\u0437\u043d\u043e"], "comments": 8, "likes": 66, "favorites": 59, "is_advertisement": false, "subsite_label": "indie", "id": 711314, "is_wide": false, "is_ugc": true, "date": "Thu, 22 Apr 2021 19:05:19 +0300", "is_special": false }
0
8 комментариев
Популярные
По порядку
Написать комментарий...
3

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

Ответить
1

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

Ответить
2

Переход в стейт анимации можно напрямую вызывать без связей в аниматоре с помощью Animator.Play. В некоторых случаях удобнее управлять переходом между стейтами аниматора из кода, как, полагаю, на втором скриншоте с Walk/Walk_Aim стейтами

https://docs.unity3d.com/ScriptReference/Animator.Play.html

Ответить
1

Уж простите за поздний ответ, но лучше, как говорится, позже, чем никогда...

Искали в архивах, то что было второй версией работы с аниматором. Была попытка дать ему ещё один шанс, без использования связей. Именно вызовами анимаций через код. Но это все равно не давало гибкости и контроля. Таким образом, Unity Animator становится лишь проигрывателем, что в принципе можно написать и самому в "33 строки". 

Мы ещё очень переживаем за оптимизацию. Об этом, тоже как-нибудь расскажем. Отсутствие компонентов Unity Animator на объектах сцены, тоже немного срезает нагрузку.

Ответить
1

Я бы не стал полностью отказываться от связей, а просто поделил бы нужные группы действий, между которыми нужно часто переключаться и назначил бы между ними связи, а вот переключение уже на что–то совсем другое, например, переход с idle анимации на определенную атаку оружием, делал бы через код, чтобы у Idle подгруппы не было десятков связей между разными мувсетами оружия.
Еще есть Blend Tree тип в аниматоре, блендящий разные анимации в зависимости от пользовательских параметров, который тоже очень сильно упрощает жизнь, и вот его уже сложно написать в 33 строки, как мне кажется.

Ответить
1

Возможности Unity Animator мы все рассмотрели. Поэтому для нашего случая было именно такое решение. Простая последовательность кадров. И всё это тесно связано с нашей архитектурой машины состояний (и да, вариант машины состояний и Unity Animator мы также рассматривали).
Что касается смешивания анимаций, то это не наш случай. У нас  в анимации четко прорисован весь кадр, а не состоящий из объектов, которыми управляет анимация (вращение, позиция...). 

Ответить
2

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

Ответить
1

Спасибо)

Ответить

Комментарии

null