Создание игры на ваших глазах — часть 7: 2D-анимации в Unity («как во флэше»)

  • Tutorial
В этой статье поговорим о 2D анимациях в Unity. Я расскажу о своем опыте работы с родными анимациями в юнити, о том, насколько тайм-лайны похожи на флэшевские, об управлении анимациями, event'ах, вложенности, и о том, как художник справляется с анимированием.

Для начала, немного теории.

В Unity есть две сущности:

1. Анимация (то, что отображается в окно «Animation»)
2. Mechanim дерево анимаций (то, что отображается в окне «Animator»).



Ниже я немного расскажу, что это такое и как нам может приходиться (или не пригодиться).

Animation


Итак, анимация. По сути — это таймлайн с ключевыми кадрами. Здесь вы можете двигать, поворачивать, масштабировать ваши объекты. Естественно, можно рисовать кривые и пользоваться разными изингами. И даже управлять любыми (в т.ч. самописными) их свойствами. То есть вполне можно написать компонент с float паблик-значением «яркость» и эту самую «яркость» анимировать наравне с x, y, z штатными средствами. Спрайты поддерживают покадровую анимацию.



Кстати, несмотря на то, что у каждой анимации есть FPS (поле «sample»), сами анимации к FPS не привязаны. Они привязаны ко времени. Т.е. если вы делаете анимацию с 5 FPS, где у вас объект двигается из точки А в точку Б с помощью задания двух ключевых кадров в начале и в конце, то в игре этот объект не будет двигаться ступеньками с 5 FPS. Анимация рассчитывается каждый кадр игры, а FPS внутри анимации сделан лишь для вашего удобства, чтобы вам не частить кадры.

Animator


Это — большая и сложная система, которая непосредственно управляет анимациями. То есть анимация — это просто файл (ресурс) с настройками ключевых кадров и сама по себе ничего не умеет. Вот именно компонент «Animator» — это то, что умеет играть эти анимации.

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

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



то есть содержать одну-единственную анимацию и никаких связей/переходов.

Начинаем шаманить.


Пока все понятно. Но давайте подумаем, как сделать что-то чуть более сложное?

Мой конкретный случай — у нас есть сугроб снега, в котором сидит заяц. Сугроб сам по себе шевелится:



Далее, мы хотим сделать такую анимацию:



1. сугроб, шевелясь, двигается влево
2. из сугроба выглядывает заяц (анимация пульсации останавливается):

3. сугроб двигается вправо

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

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

Хотелось бы большей гибкости.

Есть другой вариант. Мы анимируем выглядывание зайца в отдельном объекте (так же, как мы сделали это с шевелением сугроба), а в основном тайм-лайне просто включаем этот объект (active) в нужный момент и анимация начинается.

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

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

То есть сделать так:



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

Что нам для этого понадобится? Unity позволяет добавлять на анимацию вызов кастомных юзер event'ов. Это именно то, что нам нужно! Осталось только правильно все написать.

Первое, что нам понадобится, это написать простой компонент (в нашем случае он называется GJAnim) и повесить его на тот же объект, на котором висит наш аниматор. Именно методы этого компонента мы сможем вызывать событиями с таймлайна.

Напишем метод для постановки на паузу. Кстати, в юнити нет такой прямой возможности. Для того, чтобы поставить на паузу анимацию, обычно применяется немного грязный хак с выставлением ее скорости в 0. Он в целом работает, правда, странности есть (об этом — в последней части статьи).

public void Pause()
{
	_animator.speed = 0;
}

protected void Resume()
{
	_animator.speed = 1;
}

Где _animator — это переменная, в которой мы закешировали компонент "Animator":

_animator = GetComponent<Animator>();


Если вы обратили внимание на скрин выше, над ключевым кадром, который я пометил цифрой «2» стоит небольшая вертикальная черта. Именно за ней скрывается вызов события (метода) «Pause»:



Стоит отметить, что в такие события можно даже передавать параметр. Поддерживаются string, float и объект из библиотеки (не со сцены).

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

	public void ResumeParent()
	{
		Transform pr = transform;

		while (true)
		{
			pr = pr.parent;
			if (pr == null)
			{
				Debug.LogWarning("No GJAnim found in parents!");
				return;
			}
			GJAnim a = pr.gameObject.GetComponent<GJAnim>();
			if (a != null)
			{
				a.Resume();
				return;
			}
		}
	}


Этот метод ищет среди родителей компонент "GJAnim" и снимает его с паузы. Соответственно, ставим это событие на окончание анимации нашего кролика:



Profit!


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

Кроме того, все делается в пределах юнитевского UI и достаточно прозрачно для любого аниматора. Наш художник через час попадания ему в руки этого инструмента, уже во всю анимировал.

О багах Unity и сумасшествии.


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

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

Так вот, мы заметили, что кадры иногда проскакивают.

Долго-долго дебажили, много писали в лог… и вот что выяснили:

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

Это влечет за собой чуть ли не полностью неисправимый фейл. Мы ловили ситуацию, когда аниматор выполнял все действия с кадром и вставал на паузу (это ок), а потом в этот же кадр выполнял еще и следующий кадр. То есть за один кадр рассчитывал сразу два кадра анимации. И то, что в 1-м кадре было событие, ставящее скорость анимации в 0, не мешало ему рассчитать еще и следующий кадр, который, по видимому, уже лежал в стеке.

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

На данный момент проблема выглядит неисправимой. Как мы справились? Поставили FPS таких анимаций в 20. Видимо, на таком FPS'е случая, когда юнити хочет просчитать два кадра за одну итерацию — не случается.

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

Что с этим делать — не ясно.

Все статьи серии:
  1. Идея, вижен, выбор сеттинга, платформы, модели распространения и т.п
  2. Шейдеры для стилизации картинки под ЭЛТ/LCD
  3. Прикручиваем скриптовый язык к Unity (UniLua)
  4. Шейдер для fade in по палитре (а-ля NES)
  5. Промежуточный итог (прототип)
  6. Поговорим о пиаре инди игр
  7. 2D-анимации в Unity («как во флэше»)
  8. Визуальное скриптование кат-сцен в Unity (uScript)
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    +1
    У меня вот такой вопрос по 2D анимации (возможно глупый, Unity для меня — просто увлечение). Допустим мы делаем стратегию, в ней есть юниты, например пехотинец красной команды и пехотинец синей команды, для них нарисованы несколько spritesheet (idle для красного и синего, бег и т.п.), очевидно, что для анимации используются спрайты. Логика контроллера и набор анимаций одинаковы, разница лиш в spritesheet (ведь у нас синие и красные). Вопрос: возможно ли, и если да, то как, использовать один и тот же Animation Controller вместе с набором AnimationClip (idle, бег..) для обоих пехотинцев? Или, если точнее, возможно ли подменять спрайты или целые spritesheet в AnimationClip? Или же надо создавать клоны Animation Controller/AnimationClip для каждого вида?
      0
      Присоединяюсь у вопросу. Вообще как-то можно делать анимацию с параметрами? Если есть несколько объектов со схожей анимацией — можно ли как-то унифицировать процесс?
      Пока дошел только до идеи реализовывать анимацию через скрипты и передвать туда все, что нужно, но это как-то топорно.
        +1
        Параметризацию можно сделать через BlendTree в Mecanim.
        Посмотрите видео от Unity Tech. в ютуб канале. Там среди них было выдиео, где докладчик реализовал движение 2д персонажа (пчёлка летала) в любые стороны на один раз подготовленных анимациях. То есть если у вас разные персонажи отличаются лишь спрайтами, но используют одинаковые анимации, то одна и та же стэйт-машина может ими управлять, а в скриптах мы только лишь задаём параметры для неё.
          0
          Дополнение: анимации тогда лучше делать через кривые, а не покадровые SpriteSheet'ы. В таком случае не будет Pixel Perfect, но жизнь станет значительно проще.
            0
            Спибо за информацию. Про BlendTree и Mecanim впервые услышал. Буду изучать.
              0
              Да, BlendTree очень мощная штука. Обязательно посмотрите в стороне нее.
        0
        Почему вы не сделали разные анимации для разных подобъектов и не настроили стэйты в Mecanim? В итоге остался бы всего один скрипт, управляющий стэйт-машиной.
          0
          Хотелось полностью визуального контроля над анимациями. Чтобы ты на таймлайне мог видеть, что после чего идет и сколько времени занимает.
            0
            Визуальный редактор Mecanim'а — недостаточный контроль? Там же всё буквально мышкою настраивается. И сэйты и переходы и условия переходов и блендинг и так далее.
              0
              Да, Механим хорош именно для смены стейтов одного спрайта по каким-то условиям/триггерам. Но для реализации на нем секвенции анимаций, привязанной по длительности к каждой из вложенных — неудобно. Гораздо удобнее видеть все на таймлайне.

              По крайней мере, говорю за себя.
                0
                Я понимаю, на вкус или цвет маркеры разные. Вам удобнее флеш-стайл, я же говорю, что и секвенции, особенно по длительности, делать в меканиме достаточно удобно и наглядно. Но это уже вопрос привычки.
          0
          Я так понял, что стандартных событий для анимаций не существует, что то типа начало/конец анимации. И их надо самому выставлять на таймлайне?
            0
            Да, так и есть. Особенно не хватает события окончания анимации, которое вызывалось бы не на последнем кадре анимации, а после него (в первом кадре следующей анимации, если предусмотрен переход). В своём проекте я реализовал такую возможность жутким костылём. Может кто-нибудь предложит решение?
            0
            Жду не дождусь юнити 5, где много чего вкусного.
              0
              Спасибо за статью. У меня вопрос по событиям анимации.
              Если повесить несколько на одном кадре, эти «якоря» в таймлайне встают друг за друга, а срабатывает только первое событие. Зачем вообще давать возможность кидать назначать несколько методов на один кадр, если срабатывает один? Или есть возможность заставить срабатывать сразу все?

              И еще очень и очень интересует вопрос, как получить длину анимации из аниматора? Я использую Animator с кучей клипов внутри и либо они переключаются по внутренним условиям, либо переключаю их с помощью _animator.Play("Mlee");. Как в данном случае получить из аниматора, сколько по времени займет анимация Mlee?

              Only users with full accounts can post comments. Log in, please.