Extensão da Timeline: Um guia prático

A Unity lançou o Timeline junto com o Unity 2017.1 e, desde então, recebemos muitos comentários sobre ele. Depois de conversar com muitos desenvolvedores e responder aos usuários nos fóruns, percebemos que muitos dos senhores querem usar o Timeline para mais do que uma simples ferramenta de sequenciamento. Já dei algumas palestras sobre isso (por exemplo, na Unite Austin 2017) sobre como usar o Timeline para usos não convencionais.
O Timeline foi projetado tendo a extensibilidade como objetivo principal desde o início; a equipe que projetou o recurso sempre teve em mente que os usuários gostariam de criar seus próprios clipes e faixas, além dos incorporados. Por isso, há muitas perguntas sobre a criação de scripts com o Timeline. O sistema no qual a Timeline se baseia é poderoso, mas pode ser difícil de trabalhar para os não iniciados.
Mas, primeiro, o que é Timeline? É uma ferramenta de edição linear para sequenciar diferentes elementos: clipes de animação, música, efeitos sonoros, tomadas de câmera, efeitos de partículas e até mesmo outras Timelines. Em essência, ele é muito semelhante a ferramentas como Premiere®, After Effects® ou Final Cut®, com a diferença de que foi projetado para reprodução em tempo real.
Para uma análise mais aprofundada dos conceitos básicos da Timeline, aconselho os senhores a visitarem a seção de documentação da Timeline no Manual da Unity, pois usarei bastante esses conceitos.
O Timeline é implementado com base na API Playables.
É um conjunto de APIs poderosas que permite ao senhor ler e misturar várias fontes de dados (animação, áudio e outras) e reproduzi-las por meio de uma saída. Esse sistema oferece controle programático preciso, tem baixo overhead e é ajustado para desempenho. Aliás, essa é a mesma estrutura por trás da máquina de estado que aciona o componente Animator e, se o senhor já programou para o Animator, provavelmente verá alguns conceitos familiares.
Basicamente, quando uma Timeline começa a ser reproduzida, é criado um gráfico composto de nós chamados Playables. Eles são organizados em uma estrutura semelhante a uma árvore chamada PlayableGraph.
Observação: Se quiser visualizar a árvore de qualquer PlayableGraph na cena (Animators, Timelines etc.), o senhor pode fazer download de uma ferramenta chamada PlayableGraph Visualizer. Este post o utiliza para visualizar os gráficos dos diferentes clipes personalizados.
Agora, vou analisar três exemplos simples que mostrarão ao senhor como estender o Timeline. Para estabelecer as bases, começarei com a maneira mais fácil de adicionar um script no Timeline. Depois, mais conceitos serão adicionados gradualmente para que o senhor possa usar a maioria das funcionalidades.
Empacotei um pequeno projeto de demonstração com todos os exemplos usados nesta postagem. Fique à vontade para fazer o download e acompanhá-lo. Caso contrário, o senhor pode aproveitar a postagem por si só.
Observação: Para os ativos, usei prefixos para diferenciar as classes em cada exemplo ("Simple_", "Track_", "Mixer_" etc.). No código abaixo, esses prefixos são omitidos para facilitar a leitura.
Este primeiro exemplo é muito simples: o objetivo é alterar a cor e a intensidade de um componente Light com um clipe personalizado. Para criar um clipe personalizado, o senhor precisa de dois scripts:
- Um para os dados: herdando de PlayableAsset
- Um para a lógica: herdar de PlayableBehaviour
Um princípio fundamental da API Playable é a separação da lógica e dos dados. É por isso que o senhor precisará primeiro criar um PlayableBehaviour, no qual escreverá o que deseja fazer, assim:
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;
}
}
}O que está acontecendo aqui? Primeiro, há informações sobre quais propriedades da luz o senhor deseja alterar. Além disso, o PlayableBehaviour tem um método chamado ProcessFrame que o senhor pode substituir.
O ProcessFrame é chamado em cada atualização. Nesse método, o senhor pode definir as propriedades da luz. Aqui está a lista de métodos que o senhor pode substituir em PlayableBehaviour. Em seguida, o senhor cria um PlayableAsset para o clipe personalizado:
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;
}
}Um PlayableAsset tem duas finalidades. Primeiro, ele contém dados de clipe, pois são serializados dentro do próprio ativo Timeline. Em segundo lugar, ele constrói o PlayableBehaviour que será colocado no gráfico Playable.
Veja a primeira linha:
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);Isso cria um novo Playable e anexa um LightControlBehaviour, nosso comportamento personalizado, a ele. Em seguida, o senhor pode definir as propriedades de luz no PlayableBehaviour.
E quanto à ExposedReference? Como um PlayableAsset é um ativo, não é possível fazer referência direta a um objeto em uma cena. Uma ExposedReference atua como uma promessa de que, quando CreatePlayable for chamado, um objeto será resolvido.
Agora o senhor pode adicionar uma trilha reproduzível na Timeline e adicionar o clipe personalizado clicando com o botão direito do mouse nessa nova trilha. Atribua um componente Light ao clipe para ver o resultado.
Nesse cenário, a trilha Playable integrada é uma trilha genérica que pode aceitar esses clipes Playable simples, como o que o senhor acabou de criar. Para situações mais complexas, o senhor precisará hospedar os clipes em uma faixa dedicada.
Uma ressalva do primeiro exemplo é que cada vez que o usuário adiciona um clipe personalizado, precisa atribuir um componente Light a cada um dos clipes, o que pode ser entediante se houver muitos deles. O senhor pode resolver isso usando o objeto vinculado de uma trilha.

Uma trilha pode ter um objeto ou um componente vinculado a ela, o que significa que cada clipe na trilha pode operar diretamente no objeto vinculado. Esse é um comportamento muito comum e, na verdade, é assim que as trilhas Animation, Activation e Cinemachine funcionam.
Se quiser modificar as propriedades de uma luz com vários clipes, o senhor pode criar uma trilha personalizada que solicite um componente de luz como um objeto vinculado. Para criar uma trilha personalizada, o senhor precisa de outro script que estenda o TrackAsset:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset {}Há dois atributos aqui:
- TrackClipType especifica o tipo de PlayableAsset que a trilha aceitará. Nesse caso, o senhor especificará o LightControlAsset personalizado.
- TrackBindingType especifica o tipo de vinculação que a trilha solicitará (pode ser um GameObjects, um Component ou um Asset). Nesse caso, o senhor deseja um componente Light.
O senhor também precisa modificar ligeiramente o PlayableAsset e o PlayableBehaviour para que eles funcionem com uma faixa. Para referência, comentei as linhas que o senhor não precisa mais.
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}O PlayableBehaviour não precisa de uma variável Light agora. Nesse caso, o método ProcessFrame fornece diretamente o objeto vinculado da trilha. Tudo o que o senhor precisa é converter o objeto para o tipo apropriado. Que legal!
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;
}
}O PlayableAsset não precisa mais manter uma ExposedReference para um componente Light. A referência será gerenciada pela trilha e fornecida diretamente ao PlayableBehaviour.
Em nossa Timeline, podemos adicionar uma faixa LightControl e vincular uma luz a ela. Agora, cada clipe que adicionarmos a essa trilha funcionará no componente Light que está vinculado à trilha.
Se o senhor usar o Graph Visualizer para exibir esse gráfico, ele terá a seguinte aparência:

Como esperado, o senhor vê os clipes no lado direito como 5 blocos que se alimentam de um. O senhor pode pensar em uma caixa como a pista. Depois, tudo vai para a Timeline: a caixa roxa.
Observação: A caixa rosa chamada "Playable" é, na verdade, um misturador de cortesia Playable que a Unity cria para o senhor. É por isso que ele é da mesma cor que os clipes. O que é um misturador? Falarei sobre mixers no próximo exemplo.
A Timeline suporta a sobreposição de clipes para criar mescla ou crossfading entre eles. Os clipes personalizados também suportam a combinação. No entanto, para ativá-lo, o senhor precisa criar um mixer que acesse os dados de todos os clipes e os combine.
Um mixer deriva do PlayableBehaviour, assim como o LightControlBehaviour que o senhor usou anteriormente. Na verdade, o senhor ainda usa a função ProcessFrame. A principal diferença é que esse Playable é explicitamente declarado como um mixer pelo script da trilha, substituindo a função CreateTrackMixer:
[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);
}
}
Quando o Playable Graph dessa faixa for criado, ele também criará um novo comportamento (o mixer) e o conectará a todos os clipes da faixa.
O senhor também deseja mover a lógica do PlayableBehaviour para o mixer. Dessa forma, o PlayableBehaviour agora parecerá bastante vazio:
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
Basicamente, ele contém apenas os dados que virão do PlayableAsset em tempo de execução. O mixer, por outro lado, terá toda a lógica em sua função 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;
}
}Os mixers têm acesso a todos os clipes presentes em uma faixa. Nesse caso, o senhor precisa ler os valores de intensidade e cor de todos os clipes que participam atualmente da mescla, portanto, precisa iterar por eles com um loop for. Em cada ciclo, o senhor acessa as entradas(GetInput(i)) e constrói os valores finais usando o peso de cada clipe(GetInputWeight(i)) para obter o quanto esse clipe está contribuindo para a combinação.
Então, imagine que o senhor tenha dois clipes se misturando: um está contribuindo com vermelho e o outro com branco. Quando a mistura está a um quarto do caminho, a cor é 0,25 * Color.red + 0,75 * Color.white, o que resulta em um vermelho ligeiramente desbotado.
Após o término do loop, o senhor aplica os totais ao componente Light vinculado. Isso permite que o senhor crie algo como isto:

Agora é possível ver que a caixa vermelha é exatamente o mixer Playable que o senhor programou e sobre o qual agora tem controle total. Isso contrasta com o Exemplo 2 acima, em que o mixer era o padrão fornecido pela Unity.
Observe também que, como o gráfico está no meio de uma mistura, as caixas verdes 2 e 3 têm uma linha brilhante conectada ao misturador, indicando que o peso delas é algo como 0,5 cada.
Lembre-se de que sempre que implementar combinações em um mixer, cabe ao usuário decidir qual é a lógica. Misturar duas cores é fácil, mas o que acontece quando o senhor está misturando (exemplo selvagem) dois clipes que representam diferentes estados de IA em seu sistema de IA? Duas linhas de diálogo em sua IU? Como o senhor mescla duas poses estáticas em uma animação stop-motion? Talvez sua mistura não seja contínua, mas "escalonada" (de modo que as poses se transformam umas nas outras, mas em incrementos discretos): 0, 0.25, 0.5, 0.75, 1).
Com esse sistema poderoso à sua disposição, os cenários são empolgantes e intermináveis!
Como etapa final deste guia, vamos voltar ao exemplo anterior e implementar uma maneira diferente de mover os dados usando algo que chamamos de "modelos". Uma grande vantagem desse padrão é que ele permite que o senhor crie um quadro-chave para as propriedades do modelo, possibilitando a criação de animações para clipes personalizados diretamente na Timeline.
No exemplo anterior, o senhor tinha uma referência ao componente Light, a cor e a intensidade no PlayableAsset e no PlayableBehaviour. Os dados foram configurados no PlayableAsset no Inspector e, em seguida, no tempo de execução, foram copiados para o PlayableBehaviour ao criar o gráfico.
Essa é uma maneira válida de fazer as coisas, mas duplica os dados que precisam ser mantidos em sincronia o tempo todo. Isso pode facilmente levar a erros. Em vez disso, o senhor pode usar o conceito de um "modelo" PlayableBehaviour, criando uma referência a ele no PlayableAsset:
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}O LightControlAsset agora só tem uma referência ao LightControlBehaviour em vez dos próprios valores. O código é ainda menor do que antes!
Deixe o LightControlBehaviour inalterado:
[System.Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}A referência ao modelo agora produz automaticamente esse inspetor quando o senhor seleciona o clipe na Timeline:

Quando o script estiver pronto, o senhor estará pronto para animar. Observe que, se o senhor criar um novo clipe, verá um botão vermelho circular no cabeçalho da faixa. Isso significa que o clipe agora pode ter um quadro-chave sem a necessidade de adicionar um Animador a ele. Basta clicar no botão vermelho, selecionar o clipe, posicionar o indicador de reprodução onde deseja criar uma chave e alterar o valor dessa propriedade.
O senhor também pode expandir a visualização Curves clicando no botão da caixa branca para ver as curvas criadas pelos quadros-chave:

Há uma vantagem extra: o senhor pode clicar duas vezes no clipe da Timeline, e o Unity abrirá o painel Animation e o vinculará à Timeline. O senhor perceberá que eles estão vinculados quando esse botão for exibido:

Quando isso acontece, o senhor pode fazer scrub na Timeline e na janela Animation e os playheads serão mantidos em sincronia, para que tenha controle total sobre os quadros-chave. Agora, o senhor pode modificar a animação na janela Animation (Animação) para trabalhar nos quadros-chave em um ambiente mais confortável:

Nessa visualização, o senhor pode usar todo o poder das curvas de animação e do dopesheet para realmente refinar as animações de seus clipes personalizados.
Observação: Quando o senhor anima as coisas dessa forma, está criando Animation Clips. O senhor pode encontrá-los no ativo Timeline:

Espero que este post tenha sido uma introdução valiosa às infinitas possibilidades que o Timeline pode oferecer quando o senhor o leva para o próximo nível com scripts.
Entre em contato comigo pelo Twitter com suas perguntas, comentários e criações da Timeline!
