Search Unity

Playable and Timeline: does anybody use it? I need some tutorial on scripting.

Discussion in 'Scripting' started by cdytoby, Feb 7, 2018.

  1. cdytoby

    cdytoby

    Joined:
    Nov 19, 2014
    Posts:
    181
    PlayableAsset, PlayableBehaviour, PlayableBinding, PlayableGraph, PlayableHandle, PlayableOutput, IPlayable, .....

    Does anyone has some declare of how to use them in script and the relationship between them? Because I'm lost.

    What I'm trying to achieve can also not be found in anywhere in Internet:

    Load an image file of a spritesheet from local or internet url ( end with png), split them into multiple sprite or a sprite array or create a multi mode sprite(which can't be found anywhere how to create it), and play it on a sprite renderer with my own fps setting. All needs to be done only in runtime.

    I can just use my own way to switch the sprite of spriterenderer, but I'm looking for a better way for controlling it, and I tried to learn playable, and their definitions are chaotic.

    Can anybody help me?
     
    Last edited: Feb 7, 2018
  2. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Hey, yeah it seems the documentation on Playables is still growing (or hit a standstill as of my last edit in late 2022), and there's still a ton that needs to be explained about it. Also note that there's a Timeline section of the forums here where there's a lot of questions being asking about Timeline and the Playables API (since Unity's Timeline uses the system too!) So anyway here's my understanding of the Playables API:

    First, I 100% recommend getting Unity's PlayableGraph Visualizer (from Window > Package Manager in the menu bar at the top of Unity) -- it shows a visual representation of any graph you make as long as you make a call to the code they wrote.

    Also, I avoid the "var" keyword in C# altogether because I find it very important to know exactly the data types I'm working with. When I describe this stuff, I hope it doesn't make it too messy but I like to stick with the names and write them in PascalCaseLikeThis so you know too exactly what I'm referring to (for example, PlayableGraph, AnimatorControllerPlayable, ScriptPlayableOutput, etc.)

    ---

    The Playables API is all revolved around these things called PlayableGraphs. A PlayableGraph is something that contains nodes (each node is called a "Playable"), and the whole graph can be evaluated to do stuff (One evaluation is called a frame). They say the graph is a "data evaluation tree" -- it goes through all the nodes in the tree and sees what they have to say. They might tell objects in your scene to do certain things, or update your own scripts, for example. Each time the graph goes through a frame, all of its playables get their PrepareFrame(...) and ProcessFrame(...) methods called to actually do the cool stuff. Often times, graphs are played a lot in a row, -- the default setting is that a PlayableGraph will update (evaluate the whole thing) once every ordinary game frame, as if it was in the typical MonoBehaviour Update() method. Playables in the graph can play audio, animate objects, keep track of your own data structures, maintain game state, pretty much anything you want! It's often used for animation -- the Animator and AnimatorController use their own playable graph behind the scenes to control scene objects. In fact, you can even see the graph if you have an animator by calling animator.playableGraph.

    With Unity's graph visualizer tool, it shows the flow from right to left. Now specifically, every single node you see in the graph is a C# struct. There are two kinds: Playables, and PlayableOutputs. The outputs (PlayableOutputs) are all shown the left-most column. All the other nodes that aren't in the left-most column are just Playables. In order for any playables do productively do anything, you must connect them to an output. Playables can also be connected other playables, but they can still "be productive" if there's eventually one of them that connects the whole chain of them to an output.

    Because C# structs don't behave like normal classes (specifically they don't allow inheritance), interfaces were useful to use partially in the place of that. So because no other struct could inherit from Playable, they instead made it so all those structs implement the interface IPlayable. Similarly with PlayableOutputs (which are also structs), all of them must implement IPlayableOutput.

    ---

    I shouldn't go much longer without giving actual examples -- too much theory without practicality is no good either! To create graphs and playables, there's usually a static Create(...) method in each of the classes/structs. So to create a graph, you use PlayableGraph.Create(...). To create an AnimationClipPlayable, you use AnimationClipPlayable.Create(...), etc.

    Code (CSharp):
    1. PlayableGraph graph = PlayableGraph.Create("Not-So-Interesting Graph");
    The string passed into the method is just the name you want to give the graph. Technically it doesn't really matter, and it's usually only used in the editor. Anyway, the graph alone doesn't do anything (also we didn't tell it to start playing either!) Once we get there, we can play the graph with
    Code (CSharp):
    1. graph.Play();
    Once the graph is playing, any playables we put in it will by default start playing already, so we don't need to call the Play() method on every playable we put in.

    ---

    Now say I wanted to start off simple and use a graph to animate a GameObject in the scene. I have an AnimationClip, I just want to play it back. In terms of the Playables API, I'd need to put something in the graph. So first off, the perfect thing to use is the AnimationClipPlayable, which literally just wraps around the clip and lets it the clip be evaluated when the graph is evaluated. Second, The object in the scene that gets "taken control of" by that animation depends on the PlayableOutput that you'll make. Specifically because this is animation, that will be an AnimationPlayableOutput (which will also be a node in the graph, shown on the left in Unity's graph visualizer).

    So we need to make those 2 things. It doesn't really matter the order as long as you connect them well. So first let's make the clip's playable. This is with the graph from above.

    Code (CSharp):
    1. //Say that clip is an AnimationClip variable set in the inspector
    2. AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(graph, clip);
    And again, that does nothing on its own, because it's not connected to an output (and it's also not connected to any other playables cause we just inserted it into the graph. It's floating in space for all we care!)

    ---

    This example though, we're going to connect it directly to an animation output, so let's create the output:
    Code (CSharp):
    1. AnimationPlayableOutput animOutput = AnimationPlayableOutput.Create(graph, "Anim Output", objectToAnimate.GetComponent<Animator>());
    Okay so that Create(...) method took 3 arguments. The first (like many of the Playable/PlayableOutput Create(...) methods) is just the graph to put it in. We're using the graph we created above. The second argument is the name you want to give the output -- Also totally up to you. The third argument is important - it demonstrates the importance of the PlayableOutput too. It's the output's job to know what object in the scene will be animated/controlled. It's specifically looking for an Animator component -- the Playables API needs Animators to animate stuff. The object you want to animate is up to you. You can also make this a variable (public, [SerializeField] private, or etc.) in your inspector if this is a MonoBehaviour script so you can test it out.

    ---

    That's great, we made both of the nodes we needed in the graph! Except it doesn't do much because they're not connected. This is specific to PlayableOutputs; you need to set the output's "source playable" with this, using the above variables we created:

    Code (CSharp):
    1. animOutput.SetSourcePlayable(clipPlayable);
    This will connect the clip's playable to the output. Now we're all set! The clip has the AnimationClip's data, and sends it to the connected AnimationPlayableOutput, where the output holds a reference to the animator that will be "taken control of" when the graph evaluates (goes through a frame)!


    ---


    So of course there's a lot more that I could explain, but definitely don't underestimate the documentation that is currently available. They have some good examples. Notice things like the differences between connecting a Playable to a PlayableOutput vs. a Playable to another Playable. These things can easily be mixed up.


    The system was confusing at first for me because of one of the goals of the system: To avoid allocating memory for garbage collection. The thing about classes, when you create a class to define your own data type and instantiate it, you use that variable and stuff and that's all great. But once you're finished using that variable, the memory it took up in your computer needs to be released so that that space can be used up for something else later on. However, structs don't allocate memory for garbage collection! I don't fully understand how they could get away with that, but usually structs are used for small types that don't take a lot of space. So this was great for the PlayableGraphs because the idea is the graphs are lightweight and can be destroyed/taken down, and reconstructed "easily" without generating a whole lot of garbage for each of the playables.

    This was a key thing for me to understand because when you later get to see PlayableBehaviours, that's a class! I was like wait.. if PlayableBehaviour is a class that I inherit, then it couldn't possible go directly into the graphs, where every node should be a lightweight struct, right? And there you have it, they don't go in the graphs directly! There's a struct called ScriptPlayable<T>, where T is a PlayableBehaviour. That's where you can make your own customized playables and do custom things when your own playable is evaluated in your graphs.

    ---

    Anyway tons of words, but I hope this helps at least a little bit! Let me know if some of what I said was confusing or something.
     
    Last edited: Nov 1, 2022
    giraffe1, iphelf, CodeRonnie and 30 others like this.
  3. LoftySky

    LoftySky

    Joined:
    Mar 30, 2015
    Posts:
    13
    Big thanks. This is a well presented description.
     
    shotoutgames, ModLunar and Thorlar like this.
  4. danUnity

    danUnity

    Joined:
    Apr 28, 2015
    Posts:
    229
    Thanks a lot @ModLunar !! Very useful explanation! Exactly what I needed
     
    ModLunar likes this.
  5. cdytoby

    cdytoby

    Joined:
    Nov 19, 2014
    Posts:
    181
    I forget to say thank you. @ModLunar.

    I started this thread and actually in the end I didn't use the built-in Timeline and Playable in the end because I believed that it was quicker if I write it on my own to fulfill the requirements. But still thanks for your explaination. I'll mark this page and look into the Timeline when I have some time to spare, after all these years.

    I'm glad these posts can help other visitors.
     
    ModLunar likes this.
  6. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Absolutely, you're very welcome! These community discussions are great when the docs are lacking sometimes.

    I'm itching to do something cool with Playables and/or the C# Jobs system, but still have much to learn to make something with them still haha. Goodluck with your work/research @cdytoby !
     
    kaitsuks, danUnity and cdytoby like this.