Расширение Timeline: Практическое руководство

Unity запустила Timeline вместе с Unity 2017.1, и с тех пор мы получили множество отзывов о ней. Пообщавшись со многими разработчиками и ответив на вопросы пользователей на форумах, мы поняли, что многие из Вас хотят использовать Timeline не только как простой инструмент для создания последовательностей. Я уже выступал с несколькими докладами на эту тему (например, на Unite Austin 2017) о том, как использовать Timeline для нетрадиционных целей.
Timeline с самого начала разрабатывалась с учетом расширяемости; команда, создававшая эту функцию, всегда имела в виду, что пользователи захотят создавать собственные клипы и треки в дополнение к встроенным. Поэтому возникает множество вопросов о создании сценариев с Timeline. Система, на которой построен Timeline, очень мощная, но с ней может быть сложно работать непосвященным.
Но сначала, что такое Timeline? Это инструмент линейного редактирования для создания последовательности различных элементов: анимационных клипов, музыки, звуковых эффектов, снимков с камеры, эффектов частиц и даже других Timeline. По сути, он очень похож на такие инструменты, как Premiere®, After Effects® или Final Cut®, с той лишь разницей, что он предназначен для воспроизведения в реальном времени.
Для более подробного ознакомления с основами Timeline я советую Вам посетить раздел документации по Timeline в руководстве Unity, поскольку я буду широко использовать эти концепции.
Timeline реализована поверх API Playables.
Это набор мощных API, позволяющих Вам считывать и смешивать несколько источников данных (анимацию, аудио и многое другое) и воспроизводить их через выход. Эта система предлагает точный программный контроль, имеет низкие накладные расходы и настроена на производительность. Кстати, это та же самая структура, которая стоит за машиной состояний, управляющей компонентом Аниматор, и если Вы программировали для Аниматора, Вы, вероятно, увидите некоторые знакомые концепции.
В основном, когда Timeline начинает проигрываться, строится граф, состоящий из узлов, называемых Playables. Они организованы в древовидную структуру, называемую PlayableGraph.
Примечание: Если Вы хотите визуализировать дерево любого PlayableGraph в сцене (Аниматоры, Timeline и т.д.), Вы можете скачать инструмент под названием PlayableGraph Visualizer. В этом посте он используется для визуализации графиков для различных пользовательских клипов.
Сейчас я рассмотрю три простых примера, которые покажут Вам, как расширить Timeline. Чтобы заложить основу, я начну с самого простого способа добавления скрипта в Timeline. Затем постепенно будут добавляться новые концепции, чтобы использовать большинство функциональных возможностей.
Я собрал небольшой демонстрационный проект со всеми примерами, используемыми в этой заметке. Не стесняйтесь скачать его, чтобы следовать за ним. В противном случае Вы можете наслаждаться этим постом самостоятельно.
Примечание: Для активов я использовал префиксы, чтобы различать классы в каждом примере ("Simple_", "Track_", "Mixer_" и т.д.). В приведенном ниже коде эти префиксы опущены для удобства чтения.
Этот первый пример очень прост: цель состоит в том, чтобы изменить цвет и интенсивность компонента Light с помощью пользовательского клипа. Для создания пользовательского клипа Вам понадобятся два сценария:
- Один для данных: наследование от PlayableAsset
- Один для логики: наследование от PlayableBehaviour
Основным принципом Playable API является разделение логики и данных. Поэтому сначала Вам нужно создать PlayableBehaviour, в котором Вы напишите, что Вы хотите сделать, например, так:
public class LightControlBehaviour : PlayableBehaviour
{
public Light light = null;
public Color color = Color.white;
public float intensity = 1f;
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
if (light != null)
{
light.color = color;
light.intensity = intensity;
}
}
}Что здесь происходит? Во-первых, здесь есть информация о том, какие свойства света Вы хотите изменить. Кроме того, у PlayableBehaviour есть метод ProcessFrame, который Вы можете переопределить.
ProcessFrame вызывается при каждом обновлении. В этом методе Вы можете установить свойства Light. Вот список методов, которые Вы можете переопределить в PlayableBehaviour. Затем Вы создаете PlayableAsset для пользовательского клипа:
public class LightControlAsset : PlayableAsset
{
public ExposedReference<Light> light;
public Color color = Color.white;
public float intensity = 1.0f;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);
var lightControlBehaviour = playable.GetBehaviour();
lightControlBehaviour.light = light.Resolve(graph.GetResolver());
lightControlBehaviour.color = color;
lightControlBehaviour.intensity = intensity;
return playable;
}
}PlayableAsset имеет два назначения. Во-первых, он содержит данные клипа, поскольку они сериализованы в самом активе Timeline. Во-вторых, он создает PlayableBehaviour, который будет находиться в графе Playable.
Посмотрите на первую строку:
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);Это создаст новый Playable и прикрепит к нему LightControlBehaviour, наше пользовательское поведение. Затем Вы можете установить свойства света на PlayableBehaviour.
А как насчет ExposedReference? Поскольку PlayableAsset - это актив, невозможно напрямую ссылаться на объект в сцене. ExposedReference в этом случае выступает в качестве обещания, что при вызове CreatePlayable будет создан объект.
Теперь Вы можете добавить воспроизводимую дорожку на Timeline и добавить пользовательский клип, щелкнув правой кнопкой мыши на этой новой дорожке. Присвойте клипу компонент Light, чтобы увидеть результат.
В этом сценарии встроенная дорожка Playable Track - это общая дорожка, которая может принимать такие простые клипы Playable, как тот, который Вы только что создали. Для более сложных ситуаций Вам потребуется разместить клипы на специальной дорожке.
Одна из оговорок первого примера заключается в том, что каждый раз, когда Вы добавляете свой собственный клип, Вам нужно назначать компонент Light для каждого из Ваших клипов, что может быть утомительно, если у Вас их много. Вы можете решить эту проблему, используя связанный объект дорожки.

К дорожке может быть привязан объект или компонент, что означает, что каждый клип на дорожке может работать с привязанным объектом напрямую. Это очень распространенное поведение, и на самом деле именно так работают дорожки Animation, Activation и Cinemachine.
Если Вы хотите изменить свойства Light с помощью нескольких клипов, Вы можете создать пользовательскую дорожку, которая будет запрашивать компонент Light в качестве связанного объекта. Чтобы создать пользовательский трек, Вам нужен другой скрипт, расширяющий TrackAsset:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset {}Здесь есть два атрибута:
- TrackClipType указывает, какой тип PlayableAsset будет принимать дорожка. В этом случае Вы укажете пользовательский LightControlAsset.
- TrackBindingType указывает, какой тип привязки будет запрашиваться для трека (это может быть GameObject, Component или Asset). В этом случае Вам нужен компонент Light.
Вам также нужно немного изменить PlayableAsset и PlayableBehaviour, чтобы они работали с дорожкой. Для справки, я закомментировал строки, которые Вам больше не нужны.
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}Теперь PlayableBehaviour не нуждается в переменной Light. В этом случае метод ProcessFrame предоставляет связанный объект дорожки напрямую. Все, что Вам нужно, это привести объект к соответствующему типу. Это здорово!
public class LightControlAsset : PlayableAsset
{
//public ExposedReference<Light> light;
public Color color = Color.white;
public float intensity = 1f;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);
var lightControlBehaviour = playable.GetBehaviour();
//lightControlBehaviour.light = light.Resolve(graph.GetResolver());
lightControlBehaviour.color = color;
lightControlBehaviour.intensity = intensity;
return playable;
}
}PlayableAsset больше не нужно хранить ExposedReference для компонента Light. Ссылка будет управляться дорожкой и передаваться непосредственно в PlayableBehaviour.
На нашей временной шкале мы можем добавить дорожку LightControl и привязать к ней свет. Теперь каждый клип, который мы добавим на эту дорожку, будет работать с компонентом Light, который привязан к этой дорожке.
Если Вы используете Graph Visualizer для отображения этого графика, он будет выглядеть примерно так:

Как и ожидалось, Вы видите зажимы с правой стороны в виде 5 блоков, которые складываются в один. Вы можете считать, что одна коробка - это дорожка. Затем все попадает на Timeline: фиолетовая коробка.
Примечание. Розовый квадратик под названием "Playable" на самом деле является любезным микшером Playable, который Unity создает для Вас. Вот почему он того же цвета, что и клипсы. Что такое миксер? Я расскажу о миксерах в следующем примере.
Timeline поддерживает наложение клипов друг на друга для создания смешивания, или кроссфейдинга, между ними. Пользовательские клипы также поддерживают смешивание. Однако, чтобы включить его, Вам нужно создать микшер, который получит доступ к данным из всех клипов и смешает их.
Микшер происходит от PlayableBehaviour, как и LightControlBehaviour, который Вы использовали ранее. На самом деле, Вы по-прежнему используете функцию ProcessFrame. Ключевое отличие заключается в том, что этот Playable явно объявлен как микшер в сценарии трека, путем переопределения функции CreateTrackMixer.Теперь сценарий LightControlTrack выглядит следующим образом:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount) {
return ScriptPlayable<LightControlMixerBehaviour>.Create(graph, inputCount);
}
}
Когда будет создан график воспроизведения для этой дорожки, он также создаст новое поведение (микшер) и подключит его ко всем клипам на дорожке.
Вы также хотите перенести логику из PlayableBehaviour в микшер. Таким образом, PlayableBehaviour теперь будет выглядеть совершенно пустым:
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
По сути, он содержит только те данные, которые будут поступать от PlayableAsset во время выполнения. С другой стороны, микшер будет содержать всю логику в своей функции ProcessFrame:
public class LightControlMixerBehaviour : PlayableBehaviour
{
// NOTE: This function is called at runtime and edit time. Keep that in mind when setting the values of properties.
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
Light trackBinding = playerData as Light;
float finalIntensity = 0f;
Color finalColor = Color.black;
if (!trackBinding)
return;
int inputCount = playable.GetInputCount (); //get the number of all clips on this track
for (int i = 0; i < inputCount; i++)
{
float inputWeight = playable.GetInputWeight(i);
ScriptPlayable<LightControlBehaviour> inputPlayable = (ScriptPlayable<LightControlBehaviour>)playable.GetInput(i);
LightControlBehaviour input = inputPlayable.GetBehaviour();
// Use the above variables to process each frame of this playable.
finalIntensity += input.intensity * inputWeight;
finalColor += input.color * inputWeight;
}
//assign the result to the bound object
trackBinding.intensity = finalIntensity;
trackBinding.color = finalColor;
}
}Микшеры имеют доступ ко всем клипам, присутствующим на дорожке. В этом случае Вам нужно прочитать значения интенсивности и цвета всех клипов, участвующих в данный момент в смешивании, поэтому Вам нужно пройтись по ним с помощью цикла for. На каждом цикле Вы получаете доступ к входным данным(GetInput(i)) и выстраиваете итоговые значения, используя вес каждого клипа(GetInputWeight(i)), чтобы узнать, какой вклад вносит этот клип в смесь.
Итак, представьте, что у Вас есть два клипа, которые смешиваются: один вносит красный цвет, а другой - белый. Когда смесь пройдена на четверть пути, цвет составляет 0,25 * Color.red + 0,75 * Color.white, в результате чего получается слегка блеклый красный.
По окончании цикла Вы применяете итоговые значения к связанному компоненту Light. Это позволит Вам создать нечто подобное:

Теперь Вы видите, что красная коробка - это именно тот микшер Playable, который Вы запрограммировали, и над которым у Вас теперь есть полный контроль. Это отличается от примера 2 выше, где миксер использовался по умолчанию, предоставленный Unity.
Также обратите внимание, что поскольку график находится в середине смеси, зеленые коробки 2 и 3 обе имеют яркую линию, соединяющую их с миксером, что указывает на то, что их вес примерно равен 0,5 для каждой.
Помните, что когда бы Вы ни реализовывали бленды в микшере, Вы сами решаете, какой будет логика. Смешать два цвета очень просто, но что произойдет, если Вы смешаете (дикий пример) два клипа, которые представляют различные состояния ИИ в Вашей системе ИИ? Две строки диалога в вашем пользовательском интерфейсе? Как соединить две статичные позы в анимации stop-motion? Возможно, Ваш бленд не непрерывный, а "ступенчатый" (так что позы переходят друг в друга, но с дискретными интервалами): 0, 0.25, 0.5, 0.75, 1).
С этой мощной системой в Вашем распоряжении сценарии будут захватывающими и бесконечными!
В качестве последнего шага в этом руководстве давайте вернемся к предыдущему примеру и реализуем другой способ перемещения данных с помощью того, что мы называем "шаблонами". Одно из главных преимуществ этого шаблона заключается в том, что он позволяет Вам делать ключевые кадры в свойствах шаблона, что дает возможность создавать анимацию для пользовательских клипов прямо на Timeline.
В предыдущем примере у Вас была ссылка на компонент Light, цвет и интенсивность как в PlayableAsset, так и в PlayableBehaviour. Данные были установлены на PlayableAsset в Инспекторе, затем во время выполнения они были скопированы в PlayableBehaviour при создании графика.
Это вполне приемлемый способ, но он дублирует данные, которые затем необходимо постоянно синхронизировать. Это может легко привести к ошибкам. Вместо этого Вы можете использовать концепцию "шаблона" PlayableBehaviour, создав ссылку на него в PlayableAsset.Итак, сначала перепишите свой LightControlAsset следующим образом:
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}Теперь LightControlAsset содержит только ссылку на LightControlBehaviour, а не сами значения. Это еще меньше кода, чем раньше!
Оставьте LightControlBehaviour без изменений:
[System.Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}Ссылка на шаблон теперь автоматически создает этот Инспектор, когда Вы выбираете клип на Timeline:

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

Есть еще одно преимущество: Вы можете дважды щелкнуть по клипу Timeline, и Unity откроет панель "Анимация" и свяжет его с Timeline. Вы заметите, что они связаны, когда появится эта кнопка:

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

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

