Timelines are an amazing tool, with lots of nuances to using them. I am trying to write a custom BasicPlayableBehaviour to animate scrolling numbers in a text field, and while for the most part it’s working, I am struggling with some nuances. Would appreciate your help.
Here is the code:
public class ScrollNumbersPlayable : BasicPlayableBehaviour {
public ExposedReference<UnityEngine.UI.Text> text;
public int startingNumber;
public int endingNumber;
private UnityEngine.UI.Text _text;
public override void OnGraphStart(Playable playable) {
_text = text.Resolve(playable.GetGraph().GetResolver());
}
public override void ProcessFrame(Playable playable, FrameData info, object playerData) {
if (playable.GetTime() < 0) {
return;
}
double time = (playable.GetTime() / playable.GetDuration());
double value = startingNumber * (1 - time) + endingNumber * time;
_text.text = Math.Round(value).ToString();
}
}
The problems:
The value of the text field on the last frame of the clip is wrong, because playable.GetTime() is always less than playable.GetDuration() even on the last frame. Somehow I need to know that the last frame is reached. How?
In the editor, whenever the playhead is positioned before the clip starts, I want the text field value to be set to startingNumber. And whenever the playhead is positioned after the clip ends, I want it to be set to endingNumber. How do I do this?
For your first problem, there are two solutions, the first being an easy one and the other needing a little more setup.
a) Use OnBehaviourPause
In your custom PlayableBehaviour, override OnBehaviourPause and then check evaluationType from the FrameData argument. If the clip is in Playback, then you hit the end.
public override void OnBehaviourPause(Playable playable, FrameData info)
{
if (info.evaluationType == FrameData.EvaluationType.Playback)
{
Debug.Log("End of clip");
}
}
Be aware that this will only work in play mode.
b) Use a Mixer
A more advanced solution involves a Track Mixer. A mixer can get access to all the playable behaviours on a given track. It also has access to the global time, which can be useful to do processing that clips cannot do, since they have only a local time. See this Subtitle example from seant_unity, which will help you setup the mixer.
The mixer can also be used to solve your second problem.
When I pause the timeline halfway the playable clip it is also in playback mode.
From what I see now there is no clear way to see if the clip has actually ended.
Why is there no onBehaviourStart amd onBehaviourEnd functions?
This is pretty annoying actually, either IsDone on the Playable should tell if the Playable is done playing or give us some sort of normalized time to tell if a Playable is finished playing. Throwing a mixer into things doesn’t simplify the use of playables as it was originally meant to be.
I spent more than enough time today figuring out that double time = (playable.GetTime() / playable.GetDuration()); never gets to 1 or GetTime() never matches the GetDuration(). The simplest way would just to have a bool that tells us the Playable is done playing if it is not looping.
What is happening is the scheduler (timeline) is recognizing the clip/playable time will exceed it’s duration and deactivating it. This is preventing ‘Done’ from being set, and the time from advancing to/past the duration. A possible workaround is use OnBehaviourPause but check that playable.GetTime() + info.deltaTime >= playable.GetDuration() to distinguish between a clip completing and the timeline pausing.
We have been discussing scheduling with the playable team lately, so we will definitely bring this up. It’s something that definitely could be made easier!
I am not even using a PlayableBehavior just a plain Playable, it should be a part of it really. If I can play an AnimationClip through a Playable and am able to set a time on a Playable I should be able to get some direct value from it to see where exactly it is playing from easily. Either stick with the convention of a normalizedTime like we have with the animation state, or GetTime() should be in compared with GetDuration() and should match when it is done playing.
This should all be as easy as it was originally touted a couple of years ago:
PlayableGraph playableGraph;
myAnimationClipPlayable = Playables.AnimationPlayableUtilities.PlayClip(myAnimator, clip, out playableGraph);
myAnimationClipPlayable.SetSpeed(2f);
myAnimationClipPlayable.SetTime(0.5f);
var time = myAnimationClipPlayable.GetTime();
var normalizedTime = time / myAnimationClipPlayable.GetDuration();
if (normalizedTime >= 1f || time >= myAnimationClipPlayable.GetDuration() || myAnimationClipPlayable.IsDone()) {
Debug.Log("Playable is finished playing.");
}
Also the Unity docs are really lagging behind, I am getting a lot of 404’s as I go through to try to see what the API is for Playables and their tie ins with the Animator.
Hmm does GetDuration() calculate it differently? I will check that out and get back to you right away. I tried adding a deltaTime value to it, but then the animations after get messed up with part positioning.
Looks like it is better but my animations seem to not sync properly and some of the positions of are off and some game objects in the heirarchy are disabled when they should be enabled in the animations, is this because Playables do not benefit from the write default values of the State Machine?
Should probably update the Unity docs for the GetDuration() function and point it to the clip.length instead to get a proper value when playing from an animation clip. Why is it different from the clip anyway if I am only playing one at a time?
Playable.GetDuration() represents the duration of the playable, which is something you can set. Having PlayClip set it to the clip length probably makes sense.
As an example, Timeline sets the duration of the playable to match the length of the clip that generated it, so a clip that loops once will have duration = 2 * animationClip.length;
As for the positions being off, or the game objects being off or on, I believe your assessment is correct. The state machine Write Default Value is not applied to Playables.
More than three years have passed, are there any results?
I don’t understand why Unity often spends several years unable to implement a simple small function?
To add a requirement on this thread (just noticed it). I would like to know its the last frame in processFrame() so I can render a progress bar at 100%. Not sure a separate “onBehaviorEnd()” would solve that or not. (I am going to cheat and subtract a small value from the duration as a workaround.). I freezeframe the last frame, so it’s really obvious when the last frame still shows the progress bar at 99.8% but it’s actually finished.