Timeline verlängern: Ein praktischer Leitfaden

Unity hat Timeline zusammen mit Unity 2017.1 auf den Markt gebracht und seitdem haben wir eine Menge Feedback dazu erhalten. Nach Gesprächen mit vielen Entwicklern und Antworten auf Benutzer in den Foren haben wir festgestellt, dass viele von Ihnen Timeline für mehr als nur ein einfaches Sequenzierungswerkzeug verwenden möchten. Ich habe bereits einige Vorträge darüber gehalten (zum Beispiel auf der Unite Austin 2017), wie man Timeline für unkonventionelle Zwecke nutzen kann.
Bei der Entwicklung von Timeline stand von Anfang an die Erweiterbarkeit im Vordergrund. Das Team, das die Funktion entworfen hat, hatte immer im Hinterkopf, dass die Benutzer zusätzlich zu den eingebauten Clips und Tracks auch eigene Clips und Tracks erstellen möchten. Daher gibt es viele Fragen zur Skripterstellung mit Timeline. Das System, auf dem Timeline aufbaut, ist leistungsstark, aber für Nichteingeweihte kann es schwierig sein, damit zu arbeiten.
Aber zuerst: Was ist Timeline? Es handelt sich um ein lineares Bearbeitungswerkzeug, mit dem Sie verschiedene Elemente aneinanderreihen können: Animationsclips, Musik, Soundeffekte, Kameraaufnahmen, Partikeleffekte und sogar andere Timelines. Im Wesentlichen ist es Tools wie Premiere®, After Effects® oder Final Cut® sehr ähnlich, mit dem Unterschied, dass es für die Wiedergabe in Echtzeit entwickelt wurde.
Wenn Sie sich eingehender mit den Grundlagen der Timeline befassen möchten, empfehle ich Ihnen, den Abschnitt Timeline-Dokumentation im Unity-Handbuch zu besuchen, da ich dort ausgiebig von diesen Konzepten Gebrauch machen werde.
Timeline wird auf der Playables API implementiert.
Es handelt sich um eine Reihe leistungsstarker APIs, mit denen Sie mehrere Datenquellen (Animationen, Audio und mehr) lesen und mischen und über eine Ausgabe wiedergeben können. Dieses System bietet eine präzise programmatische Steuerung, es hat einen geringen Overhead und ist auf Leistung getrimmt. Es ist übrigens dasselbe Framework, das hinter dem Zustandsautomaten steht, der die Animator-Komponente antreibt. Wenn Sie für den Animator programmiert haben, werden Sie wahrscheinlich einige vertraute Konzepte erkennen.
Wenn die Wiedergabe einer Timeline beginnt, wird ein Graph erstellt, der aus Knoten besteht, die Playables genannt werden. Sie sind in einer baumartigen Struktur organisiert, die PlayableGraph genannt wird.
Anmerkung: Wenn Sie den Baum eines beliebigen abspielbaren Graphen in der Szene (Animatoren, Timelines usw.) visualisieren möchten, können Sie ein Tool namens PlayableGraph Visualizer herunterladen. In diesem Beitrag werden die Diagramme für die verschiedenen benutzerdefinierten Clips visualisiert.
Ich werde Ihnen nun anhand von drei einfachen Beispielen zeigen, wie Sie Timeline erweitern können. Um die Grundlagen zu schaffen, beginne ich mit dem einfachsten Weg, ein Skript in Timeline hinzuzufügen. Dann werden nach und nach weitere Konzepte hinzugefügt, um die meisten der Funktionen zu nutzen.
Ich habe ein kleines Demoprojekt mit allen in diesem Beitrag verwendeten Beispielen gepackt. Laden Sie sie herunter und folgen Sie ihr. Ansonsten können Sie den Beitrag auch alleine genießen.
Anmerkung: Für die Assets habe ich Präfixe verwendet, um die Klassen in jedem Beispiel zu unterscheiden ("Simple_", "Track_", "Mixer_", usw.). Im folgenden Code werden diese Präfixe aus Gründen der Lesbarkeit weggelassen.
Dieses erste Beispiel ist sehr einfach: Das Ziel ist es, die Farbe und Intensität einer Lichtkomponente mit einem benutzerdefinierten Clip zu ändern. Um einen benutzerdefinierten Clip zu erstellen, benötigen Sie zwei Skripte:
- Eine für die Daten: Erben von PlayableAsset
- Einer für die Logik: Erben von PlayableBehaviour
Ein zentraler Grundsatz der Playable API ist die Trennung von Logik und Daten. Deshalb müssen Sie zunächst ein PlayableBehaviour erstellen, in das Sie schreiben, was Sie tun möchten, etwa so:
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;
}
}
}
Was ist hier los? Zunächst erhalten Sie Informationen darüber, welche Eigenschaften des Lichts Sie ändern möchten. Außerdem verfügt PlayableBehaviour über eine Methode namens ProcessFrame, die Sie außer Kraft setzen können.
ProcessFrame wird bei jeder Aktualisierung aufgerufen. Mit dieser Methode können Sie die Eigenschaften des Lichts einstellen. Hier ist die Liste der Methoden, die Sie in PlayableBehaviour außer Kraft setzen können. Dann erstellen Sie ein PlayableAsset für den benutzerdefinierten Clip:
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;
}
}
Ein PlayableAsset hat zwei Aufgaben. Erstens enthält sie Clipdaten, da sie im Timeline-Asset selbst serialisiert sind. Zweitens wird das PlayableBehaviour erstellt, das im Playable-Diagramm landen wird.
Sehen Sie sich die erste Zeile an:
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);
Dadurch wird ein neues Playable erstellt und ein LightControlBehaviour, unser benutzerdefiniertes Verhalten, daran angehängt. Sie können dann die Lichteigenschaften für das PlayableBehaviour festlegen.
Was ist mit der ExposedReference? Da ein PlayableAsset ein Asset ist, ist es nicht möglich, direkt auf ein Objekt in einer Szene zu verweisen. Eine ExposedReference fungiert dann als Versprechen, dass beim Aufruf von CreatePlayable ein Objekt aufgelöst wird.
Jetzt können Sie eine abspielbare Spur in der Timeline hinzufügen und den benutzerdefinierten Clip hinzufügen, indem Sie mit der rechten Maustaste auf diese neue Spur klicken. Weisen Sie dem Clip eine Lichtkomponente zu, um das Ergebnis zu sehen.
In diesem Szenario ist die integrierte abspielbare Spur eine generische Spur, die diese einfachen abspielbaren Clips wie den, den Sie gerade erstellt haben, aufnehmen kann. Für komplexere Situationen müssen Sie die Clips auf einer eigenen Spur hosten.
Ein Nachteil des ersten Beispiels ist, dass Sie jedes Mal, wenn Sie Ihren benutzerdefinierten Clip hinzufügen, jedem Ihrer Clips eine Lichtkomponente zuweisen müssen, was mühsam sein kann, wenn Sie sehr viele davon haben. Sie können dieses Problem lösen, indem Sie das gebundene Objekt einer Spur verwenden.

An eine Spur kann ein Objekt oder eine Komponente gebunden werden. Das bedeutet, dass jeder Clip auf der Spur dann direkt auf das gebundene Objekt wirken kann. Das ist ein ganz normales Verhalten und genau so funktionieren die Spuren Animation, Aktivierung und Cinemachine.
Wenn Sie die Eigenschaften eines Lichts mit mehreren Clips ändern möchten, können Sie eine benutzerdefinierte Spur erstellen, die nach einer Lichtkomponente als gebundenes Objekt fragt. Um einen benutzerdefinierten Track zu erstellen, benötigen Sie ein weiteres Skript, das TrackAsset erweitert:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset {}
Hier gibt es zwei Attribute:
- TrackClipType gibt an, welchen PlayableAsset-Typ der Track akzeptieren wird. In diesem Fall geben Sie das benutzerdefinierte LightControlAsset an.
- TrackBindingType gibt an, nach welcher Art von Bindung der Track fragen wird (es kann ein GameObject, eine Komponente oder ein Asset sein). In diesem Fall benötigen Sie eine Komponente Licht.
Sie müssen auch das PlayableAsset und das PlayableBehaviour leicht anpassen, damit sie mit einem Track funktionieren. Als Referenz habe ich die Zeilen, die Sie nicht mehr benötigen, auskommentiert.
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}
Das PlayableBehaviour braucht jetzt keine Light-Variable mehr. In diesem Fall liefert die Methode ProcessFrame direkt das gebundene Objekt der Spur. Alles, was Sie tun müssen, ist, das Objekt auf den entsprechenden Typ zu übertragen. Das ist klasse!
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;
}
}
Das PlayableAsset muss keine ExposedReference für eine Light-Komponente mehr enthalten. Die Referenz wird von der Spur verwaltet und direkt an das PlayableBehaviour weitergegeben.
In unserer Timeline können wir eine LightControl-Spur hinzufügen und ein Licht an sie binden. Jetzt wirkt jeder Clip, den wir zu dieser Spur hinzufügen, auf die Lichtkomponente, die an die Spur gebunden ist.
Wenn Sie den Graph Visualizer verwenden, um dieses Diagramm anzuzeigen, sieht es ungefähr so aus:

Wie erwartet, sehen Sie die Clips auf der rechten Seite als 5 Blöcke, die in einen einfließen. Sie können sich die eine Box als die Strecke vorstellen. Dann kommt alles in die Timeline: die lila Box.
Hinweis: Das rosafarbene Kästchen mit der Bezeichnung "Playable" ist eigentlich ein Höflichkeitsmixer Playable, den Unity für Sie erstellt. Deshalb hat sie die gleiche Farbe wie die Clips. Was ist ein Mixer? Ich werde im nächsten Beispiel über Mixer sprechen.
Die Timeline unterstützt überlappende Clips, um Überblendungen zwischen ihnen zu erzeugen. Benutzerdefinierte Clips unterstützen auch Überblendungen. Um dies zu ermöglichen, müssen Sie einen Mixer erstellen, der auf die Daten aller Clips zugreift und sie mischt.
Ein Mixer leitet sich von PlayableBehaviour ab, genau wie der LightControlBehaviour, den Sie zuvor verwendet haben. In der Tat verwenden Sie immer noch die Funktion ProcessFrame. Der Hauptunterschied besteht darin, dass dieses Playable vom Track-Skript explizit als Mixer deklariert wird, indem die Funktion CreateTrackMixer überschrieben wird. Das LightControlTrack-Skript sieht nun wie folgt aus:
[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);
}
}
Wenn die abspielbare Grafik für diese Spur erstellt wird, wird auch ein neues Verhalten (der Mixer) erstellt und mit allen Clips der Spur verbunden.
Außerdem möchten Sie die Logik aus dem PlayableBehaviour in den Mixer verschieben. Daher wird das PlayableBehaviour jetzt ziemlich leer aussehen:
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
Sie enthält im Grunde nur die Daten, die zur Laufzeit aus dem PlayableAsset kommen werden. Der Mixer hingegen hat die gesamte Logik in seiner ProcessFrame-Funktion:
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;
}
}
Mixer haben Zugriff auf alle Clips, die sich auf einer Spur befinden. In diesem Fall müssen Sie die Werte für Intensität und Farbe aller Clips, die derzeit an der Überblendung beteiligt sind, auslesen, also müssen Sie diese mit einer for-Schleife durchlaufen. Bei jedem Zyklus greifen Sie auf die Eingaben zu(GetInput(i)) und erstellen die endgültigen Werte unter Verwendung der Gewichtung jedes Clips(GetInputWeight(i)), um zu ermitteln, wie viel dieser Clip zur Mischung beiträgt.
Stellen Sie sich also vor, Sie haben zwei Clips, die ineinander übergehen: der eine steuert Rot und der andere Weiß bei. Wenn die Überblendung ein Viertel des Weges zurückgelegt hat, ist die Farbe 0,25 * Farbe.rot + 0,75 * Farbe.weiß, was zu einem leicht verblassten Rot führt.
Sobald die Schleife beendet ist, wenden Sie die Summen auf die gebundene Komponente Licht an. Damit können Sie etwas wie dieses erstellen:

Sie können nun sehen, dass der rote Kasten genau der Mixer Playable ist, den Sie programmiert haben und über den Sie nun die volle Kontrolle haben. Dies steht im Gegensatz zum obigen Beispiel 2, bei dem der Mixer der Standardmixer von Unity war.
Da sich das Diagramm in der Mitte einer Mischung befindet, haben die grünen Kästchen 2 und 3 beide eine helle Linie, die sie mit dem Mixer verbindet. Dies zeigt an, dass ihr Gewicht jeweils etwa 0,5 beträgt.
Denken Sie daran, dass Sie bei jeder Implementierung von Blends in einem Mixer selbst entscheiden, welche Logik dahinter steckt. Zwei Farben zu mischen ist einfach, aber was passiert, wenn Sie (z.B.) zwei Clips mischen, die unterschiedliche KI-Status in Ihrem KI-System darstellen? Zwei Dialogzeilen in Ihrer Benutzeroberfläche? Wie überblenden Sie zwei statische Posen in einer Stop-Motion-Animation? Vielleicht ist Ihre Überblendung nicht kontinuierlich, sondern "stufenweise" (d.h. die Posen gehen ineinander über, aber in diskreten Schritten): 0, 0.25, 0.5, 0.75, 1).
Mit diesem leistungsstarken System stehen Ihnen aufregende und endlose Szenarien zur Verfügung!
Als letzten Schritt in dieser Anleitung lassen Sie uns zum vorherigen Beispiel zurückkehren und eine andere Art der Datenverschiebung implementieren, die wir als "Vorlagen" bezeichnen. Ein großer Vorteil dieses Musters ist, dass Sie die Eigenschaften der Vorlage mit Keyframes versehen können, so dass Sie Animationen für benutzerdefinierte Clips direkt in der Timeline erstellen können.
Im vorherigen Beispiel hatten Sie einen Verweis auf die Lichtkomponente, die Farbe und die Intensität sowohl auf dem PlayableAsset als auch auf dem PlayableBehaviour. Die Daten wurden im Inspector auf dem PlayableAsset eingerichtet und dann zur Laufzeit beim Erstellen des Diagramms in das PlayableBehaviour kopiert.
Das ist eine gute Methode, aber sie dupliziert die Daten, die dann jederzeit synchronisiert werden müssen. Dies kann leicht zu Fehlern führen. Stattdessen können Sie das Konzept einer PlayableBehaviour- "Vorlage" verwenden, indem Sie einen Verweis darauf im PlayableAsset erstellen. Schreiben Sie also zunächst Ihr LightControlAsset wie folgt um:
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}
Das LightControlAsset hat jetzt nur noch einen Verweis auf das LightControlBehaviour und nicht mehr die Werte selbst. Es ist sogar noch weniger Code als vorher!
Lassen Sie das LightControlBehaviour unverändert:
[System.Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
Der Verweis auf die Vorlage erzeugt nun automatisch diesen Inspektor, wenn Sie den Clip in der Timeline auswählen:

Sobald Sie dieses Skript fertiggestellt haben, können Sie mit der Animation beginnen. Wenn Sie einen neuen Clip erstellen, sehen Sie eine kreisförmige rote Schaltfläche in der Kopfzeile der Spur. Das bedeutet, dass der Clip jetzt mit Keyframes versehen werden kann, ohne dass ein Animator hinzugefügt werden muss. Klicken Sie einfach auf die rote Schaltfläche, wählen Sie den Clip aus, positionieren Sie den Abspielkopf an der Stelle, an der Sie einen Key erstellen möchten, und ändern Sie den Wert dieser Eigenschaft.
Sie können die Ansicht Kurven auch erweitern, indem Sie auf das weiße Kästchen klicken, um die von den Keyframes erstellten Kurven zu sehen:

Es gibt noch einen zusätzlichen Vorteil: Wenn Sie auf den Timeline-Clip doppelklicken, öffnet Unity das Animationsfenster und verknüpft ihn mit der Timeline. Sie werden feststellen, dass sie verknüpft sind, wenn diese Schaltfläche angezeigt wird:

In diesem Fall können Sie sowohl auf der Timeline als auch im Animationsfenster scrubben, und die Abspielköpfe werden synchron gehalten, so dass Sie die volle Kontrolle über Ihre Keyframes haben. Sie können nun Ihre Animation im Animationsfenster ändern, um die Keyframes in einer komfortableren Umgebung zu bearbeiten:

In dieser Ansicht können Sie die volle Leistung der Animationskurven und des Dopesheets nutzen, um die Animationen Ihrer benutzerdefinierten Clips zu verfeinern.
Hinweis: Wenn Sie Dinge auf diese Weise animieren, erstellen Sie Animationsclips. Sie finden sie unter dem Asset Timeline:

Ich hoffe, dieser Beitrag war eine wertvolle Einführung in die unendlichen Möglichkeiten, die Timeline bietet, wenn Sie es mit Skripten auf die nächste Stufe heben.
Bitte schreiben Sie mir auf Twitter Ihre Fragen, Ihr Feedback und Ihre Timeline-Kreationen!