Extension de la Timeline : Un guide pratique

Unity a lancé Timeline en même temps que Unity 2017.1 et depuis, nous avons reçu de nombreux retours à son sujet. Après avoir discuté avec de nombreux développeurs et répondu aux utilisateurs sur les forums, nous nous sommes rendu compte que beaucoup d'entre vous souhaitaient utiliser Timeline autrement que comme un simple outil de séquençage. J'ai déjà donné quelques conférences à ce sujet (par exemple, à Unite Austin 2017) sur la façon d'utiliser Timeline pour des usages non conventionnels.
Timeline a été conçue dès le départ dans un souci d'extensibilité. L'équipe qui a conçu cette fonctionnalité a toujours eu à l'esprit que les utilisateurs souhaiteraient créer leurs propres clips et pistes en plus des clips et pistes intégrés. C'est pourquoi de nombreuses questions se posent sur la création de scripts avec Timeline. Le système sur lequel repose Timeline est puissant, mais il peut être difficile à utiliser pour les non-initiés.
Mais d'abord, qu'est-ce que la Timeline ? Il s'agit d'un outil d'édition linéaire permettant de séquencer différents éléments : clips d'animation, musique, effets sonores, plans de caméra, effets de particules et même d'autres Timelines. Par essence, il est très similaire à des outils tels que Premiere®, After Effects® ou Final Cut®, à la différence près qu'il est conçu pour une lecture en temps réel.
Pour un examen plus approfondi des bases de la Timeline, je vous conseille de visiter la section de documentation sur la Timeline du manuel Unity, car je ferai un usage intensif de ces concepts.
Timeline est mis en œuvre au-dessus de l'API Playables.
Il s'agit d'un ensemble d'API puissantes qui vous permettent de lire et de mélanger plusieurs sources de données (animation, audio et autres) et de les lire via une sortie. Ce système offre un contrôle programmatique précis, il a une faible surcharge et il est conçu pour être performant. Il s'agit d'ailleurs du même cadre que celui qui sous-tend la machine à états qui pilote le composant Animator, et si vous avez programmé pour l'Animator, vous verrez probablement des concepts familiers.
En principe, lorsqu'une Timeline commence à jouer, un graphe composé de nœuds appelés "Playables" est construit. Ils sont organisés dans une structure arborescente appelée PlayableGraph.
Note: Si vous souhaitez visualiser l'arbre de n'importe quel PlayableGraph dans la scène (Animateurs, Timelines, etc.), vous pouvez télécharger un outil appelé PlayableGraph Visualizer. Ce billet l'utilise pour visualiser les graphiques des différents clips personnalisés.
Je vais maintenant vous présenter trois exemples simples qui vous montreront comment étendre la Timeline. Afin de poser les bases, je commencerai par la manière la plus simple d'ajouter un script dans Timeline. Ensuite, d'autres concepts seront ajoutés progressivement afin d'utiliser la plupart des fonctionnalités.
J'ai préparé un petit projet de démonstration avec tous les exemples utilisés dans cet article. N'hésitez pas à le télécharger pour le suivre. Sinon, vous pouvez lire le billet seul.
Note: Pour les actifs, j'ai utilisé des préfixes pour différencier les classes dans chaque exemple ("Simple_", "Track_", "Mixer_", etc.). Dans le code ci-dessous, ces préfixes sont omis pour des raisons de lisibilité.
Ce premier exemple est très simple : l'objectif est de modifier la couleur et l'intensité d'un composant Light à l'aide d'un clip personnalisé. Pour créer un clip personnalisé, vous avez besoin de deux scripts :
- Un pour les données : hériter de PlayableAsset
- Un pour la logique : hériter de PlayableBehaviour
L'un des principes fondamentaux de l'API jouable est la séparation de la logique et des données. C'est pourquoi vous devrez d'abord créer un PlayableBehaviour, dans lequel vous écrirez ce que vous voulez faire, comme suit :
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;
}
}
}Que se passe-t-il ici ? Tout d'abord, vous trouverez des informations sur les propriétés de la lumière que vous souhaitez modifier. De plus, PlayableBehaviour possède une méthode appelée ProcessFrame que vous pouvez remplacer.
ProcessFrame est appelé à chaque mise à jour. Dans cette méthode, vous pouvez définir les propriétés de la lumière. Voici la liste des méthodes que vous pouvez remplacer dans PlayableBehaviour. Ensuite, vous créez un PlayableAsset pour le clip personnalisé :
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;
}
}Un actif jouable a deux fonctions. Tout d'abord, elle contient des données de clip, car elles sont sérialisées dans la ressource Timeline elle-même. Deuxièmement, il construit le PlayableBehaviour qui se retrouvera dans le graphe Playable.
Regardez la première ligne :
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);Cela permet de créer un nouveau jouable et d'y attacher un LightControlBehaviour, notre comportement personnalisé. Vous pouvez ensuite définir les propriétés de la lumière sur le PlayableBehaviour.
Qu'en est-il de la référence exposée? Étant donné qu'un PlayableAsset est un bien, il n'est pas possible de se référer directement à un objet dans une scène. Une ExposedReference agit alors comme une promesse que, lorsque CreatePlayable est appelé, un objet sera résolu.
Vous pouvez maintenant ajouter une piste lisible dans la Timeline, et ajouter le clip personnalisé en cliquant avec le bouton droit de la souris sur cette nouvelle piste. Attribuez un composant Light au clip pour voir le résultat.
Dans ce scénario, la piste jouable intégrée est une piste générique qui peut accepter des clips jouables simples comme celui que vous venez de créer. Pour les situations plus complexes, vous devrez héberger les clips sur une piste dédiée.
L'inconvénient du premier exemple est que chaque fois que vous ajoutez un clip personnalisé, vous devez attribuer un composant Light à chacun de vos clips, ce qui peut s'avérer fastidieux si vous en avez beaucoup. Vous pouvez résoudre ce problème en utilisant l'objet lié d'une piste.

Une piste peut être liée à un objet ou à un composant, ce qui signifie que chaque clip de la piste peut agir directement sur l'objet lié. Il s'agit d'un comportement très courant et c'est en fait ainsi que fonctionnent les pistes d'animation, d'activation et de Cinemachine.
Si vous souhaitez modifier les propriétés d'une lumière avec plusieurs clips, vous pouvez créer une piste personnalisée qui demande un composant de lumière comme objet lié. Pour créer une piste personnalisée, vous avez besoin d'un autre script qui étend TrackAsset :
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset {}Il y a ici deux attributs :
- TrackClipType spécifie le type de PlayableAsset accepté par la piste . Dans ce cas, vous spécifierez le LightControlAsset personnalisé.
- TrackBindingType spécifie le type de liaison que la piste demandera (il peut s'agir d'un GameObjects, d'un Component ou d'un Asset). Dans ce cas, vous souhaitez un composant "Lumière".
Vous devez également modifier légèrement le PlayableAsset et le PlayableBehaviour afin de les faire fonctionner avec une piste. Pour référence, j'ai commenté les lignes dont vous n'avez plus besoin.
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}Le PlayableBehaviour n'a plus besoin d'une variable Light. Dans ce cas, la méthode ProcessFrame fournit directement l'objet lié à la piste. Il vous suffit d'attribuer à l'objet le type approprié. C'est génial !
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;
}
}Le PlayableAsset n'a plus besoin de contenir une ExposedReference pour un composant Light. La référence sera gérée par la piste et donnée directement au PlayableBehaviour.
Dans notre Timeline, nous pouvons ajouter une piste LightControl et y associer une lumière. Désormais, chaque clip que nous ajouterons à cette piste agira sur le composant Light lié à la piste.
Si vous utilisez le Visualiseur de graphiques pour afficher ce graphique, il se présente comme suit :

Comme prévu, vous voyez les clips sur le côté droit sous la forme de 5 blocs qui s'emboîtent les uns dans les autres. Vous pouvez considérer cette boîte comme la piste. Ensuite, tout se passe dans la Timeline : la boîte violette.
Remarque : La boîte rose appelée "Playable" est en fait un mélangeur de courtoisie Playable qu'Unity crée pour vous. C'est pourquoi il est de la même couleur que les clips. Qu'est-ce qu'un mixeur ? Je parlerai des mélangeurs dans l'exemple suivant.
La Timeline prend en charge le chevauchement des clips pour créer un mélange, ou fondu enchaîné, entre eux. Les clips personnalisés prennent également en charge le mélange. Pour l'activer, vous devez créer un mélangeur qui accède aux données de tous les clips et les mélange.
Un mélangeur dérive de PlayableBehaviour, tout comme le LightControlBehaviour que vous avez utilisé précédemment. En fait, vous utilisez toujours la fonction ProcessFrame. La principale différence réside dans le fait que ce Playable est explicitement déclaré comme un mixeur par le script de la piste, en remplaçant la fonction CreateTrackMixer. Le script LightControlTrack ressemble maintenant à ceci :
[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);
}
}
Lorsque le graphique lisible de cette piste est créé, il crée également un nouveau comportement (le mélangeur) et le connecte à tous les clips de la piste.
Vous souhaitez également déplacer la logique du PlayableBehaviour vers le mixeur. Ainsi, le PlayableBehaviour aura l'air bien vide :
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
Il contient uniquement les données qui proviendront de l'objet jouable au moment de l'exécution. Le mélangeur, quant à lui, aura toute la logique dans sa fonction 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;
}
}Les mixeurs ont accès à tous les clips présents sur une piste. Dans ce cas, vous devez lire les valeurs d'intensité et de couleur de tous les clips participant actuellement au mélange, et vous devez donc les parcourir à l'aide d'une boucle for. À chaque cycle, vous accédez aux entrées(GetInput(i)) et construisez les valeurs finales en utilisant le poids de chaque clip(GetInputWeight(i)) pour obtenir la contribution de ce clip au mélange.
Imaginez donc que deux clips se mélangent : l'un contribue au rouge et l'autre au blanc. Lorsque le mélange est au quart de sa durée, la couleur est 0,25 * Color.red + 0,75 * Color.white, ce qui donne un rouge légèrement délavé.
Une fois la boucle terminée, vous appliquez les totaux au composant lumineux lié. Cela vous permet de créer quelque chose comme ceci :

Vous pouvez voir maintenant que la boîte rouge est exactement le mélangeur Playable que vous avez programmé et sur lequel vous avez maintenant un contrôle total. Cela contraste avec l'exemple 2 ci-dessus, où le mélangeur était celui fourni par défaut par Unity.
Remarquez également que, comme le graphique se trouve au milieu d'un mélange, les boîtes vertes 2 et 3 ont toutes deux une ligne brillante qui les relie au mélangeur, ce qui indique que leur poids est d'environ 0,5 chacune.
Gardez à l'esprit que chaque fois que vous mettez en œuvre des mélanges dans un mélangeur, c'est à vous de décider de la logique à appliquer. Mélanger deux couleurs est facile, mais que se passe-t-il lorsque vous mélangez (exemple sauvage) deux clips qui représentent différents états de l'IA dans votre système d'IA ? Deux lignes de dialogue dans votre interface utilisateur ? Comment mélanger deux poses statiques dans une animation en stop-motion ? Peut-être que votre mélange n'est pas continu, mais qu'il est "échelonné" (de sorte que les poses se fondent les unes dans les autres, mais par incréments discrets) : 0, 0.25, 0.5, 0.75, 1).
Avec ce système puissant à votre disposition, les scénarios sont passionnants et infinis !
Pour terminer ce guide, revenons à l'exemple précédent et mettons en œuvre une autre façon de déplacer les données en utilisant ce que nous appelons des "modèles". L'un des grands avantages de ce modèle est qu'il vous permet d'encadrer les propriétés du modèle, ce qui rend possible la création d'animations pour des clips personnalisés directement sur la Timeline.
Dans l'exemple précédent, vous aviez une référence au composant Light, à la couleur et à l'intensité à la fois sur le PlayableAsset et le PlayableBehaviour. Les données ont été configurées dans l'inspecteur sur l'objet jouable, puis copiées dans le comportement jouable lors de la création du graphique.
Il s'agit d'une méthode valable, mais elle duplique les données qui doivent alors être synchronisées en permanence. Cela peut facilement conduire à des erreurs. Au lieu de cela, vous pouvez utiliser le concept de "modèle" de comportement jouable, en créant une référence à ce dernier dans le jeu 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;
}
}Le LightControlAsset ne contient plus qu'une référence au LightControlBehaviour et non plus les valeurs elles-mêmes. Il y a encore moins de code qu'avant !
Ne modifiez pas le comportement du LightControlBehaviour:
[System.Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}La référence au modèle produit désormais automatiquement cet inspecteur lorsque vous sélectionnez le clip dans la Timeline :

Une fois ce script mis en place, vous êtes prêt à animer. Notez que si vous créez un nouveau clip, vous verrez un bouton circulaire rouge sur l'en-tête de piste. Cela signifie que le clip peut maintenant être encadré par des clés sans qu'il soit nécessaire d'y ajouter un animateur. Il vous suffit de cliquer sur le bouton rouge, de sélectionner le clip, de positionner la tête de lecture à l'endroit où vous souhaitez créer une clé et de modifier la valeur de cette propriété.
Vous pouvez également développer la vue Courbes en cliquant sur le bouton de la boîte blanche, pour voir les courbes créées par les images clés :

Il y a un avantage supplémentaire : vous pouvez double-cliquer sur le clip de la Timeline, et Unity ouvrira le panneau Animation et le liera à la Timeline. Vous remarquerez qu'ils sont liés lorsque ce bouton apparaîtra :

Lorsque cela se produit, vous pouvez scruter à la fois la Timeline et la fenêtre d'animation et les têtes de lecture resteront synchronisées, de sorte que vous aurez un contrôle total sur vos images clés. Vous pouvez maintenant modifier votre animation dans la fenêtre Animation pour travailler sur les images-clés dans un environnement plus confortable :

Dans cette vue, vous pouvez utiliser toute la puissance des courbes d'animation et du dopesheet pour affiner les animations de vos clips personnalisés.
Remarque : Lorsque vous animez des choses de cette manière, vous créez des clips d'animation. Vous les trouverez sous l'atout Timeline :

J'espère que cet article vous a permis de découvrir les possibilités infinies qu'offre Timeline lorsque vous passez à l'étape suivante avec l'écriture de scripts.
N'hésitez pas à m'envoyer un message sur Twitter pour me faire part de vos questions, de vos commentaires et de vos créations en Timeline !
