Extensão da Timeline: Um guia prático

CIRO CONTINISIO / UNITY TECHNOLOGIESContributor
Sep 5, 2018|13 Mínimo
Extensão da Timeline: Um guia prático
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

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.

A API jogável

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.

Ativos

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.

Exemplo 1 - Clipes personalizados

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.

Exemplo 2 - Trilhas personalizadas

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.

Imagem
Imagem

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:

Imagem
Imagem

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.

Exemplo 3 - Misturando clipes com um mixer

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:

Imagem

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!

Exemplo 4 - Animação de clipes personalizados

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:

Imagem

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:

Imagem
Imagem

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:

Imagem

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:

Imagem

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:

Imagem
Em conclusão

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!