Hello and thanks to all in advance.
I am currently trying to have a 2D sprite of a spaceship play an animation when the player dies and then disable the object. I am trying to accomplish this within a coroutine. To do this, I need the length of the animation but the animation has the samples set to 15 frames a second in order to slow the playback. Therefore, I need to access the clip via the runtimeAnimatorController
instead of just calling GetCurrentAnimatorClipInfo(0)
. I accomplish this with the following code:
private IEnumerator ExplodeRoutine()
{
m_animator.Play("PlayerExplodingAnim");
AnimationClip clip = null;
AnimationClip[] clips = m_animator.runtimeAnimatorController.animationClips;
AnimatorClipInfo[] clipInfo = m_animator.GetCurrentAnimatorClipInfo(0);
for (int i = 0; i < clips.Length; i++)
{
if (clipInfo[0].clip.name == clips[i].name)
{
clip = clips[i];
break;
}
}
Debug.Assert(clip != null);
if (clip != null)
{
yield return new WaitForSeconds(clip.length);
gameObject.SetActive(false);
}
}
Here is the state machine for the animator:
What’s going wrong is the value of clip
is “PlaceholderIdleAnimation”. From my understanding, when Play
is called from an animator without the layer it will attempt to go to a state of the same name. It actually works this way in other places in my project.
When debugging, the animation “PlayerExplodingAnim” does indeed play and shows in the state machine as being played, but the state remains on the idle animation.
The most obvious thing is am I setting it to the idle animation outside the coroutine? No, unfortunately. I’m not able to figure out what’s going wrong here.
Hi,
It seems that the problem is in internal animator state not being updated after m_animator.Play("PlayerExplodingAnim");
that’s why value of clip
is “PlaceholderIdleAnimation” - clip being played at the momnet ExplodeRoutine
is called.
I would propose getting clip directly by name instead:
private IEnumerator ExplodeRoutine()
{
m_animator.Play("PlayerExplodingAnim");
// Assuming that clip name is the same as animator state name and layer index is zero
var clip = m_animator.GetAnimationClip(0, "PlayerExplodingAnim");
Debug.Assert(clip != null);
if (clip != null)
{
yield return new WaitForSeconds(clip.length);
gameObject.SetActive(false);
}
}
private static AnimationClip GetAnimationClip(this Animator animator, int layerIndex, string clipName)
{
var clipsInfo = animator.GetCurrentAnimatorClipInfo(layerIndex);
var index = Array.FindIndex(clipsInfo, x => x.clip.name == clipName);
if (index == -1)
{
Debug.LogWarningFormat("Clip with name {0} not found in layer with index {1}", clipName, layerIndex);
return null;
}
var clipInfo = clipsInfo[index];
return clipInfo.clip;
}
Hope this helps.
Another approach on the problem you are solving here with coroutine would be using animation event at the end of “PlayerExplodingAnim” clip, see https://docs.unity3d.com/Manual/animeditor-AnimationEvents.html for more info.
Actually that does help. Although your code is something I’ve tried already, what you said about it not yet being set before the rest of the coroutine is most likely exactly what’s happening. It put me on to the following solution:
private IEnumerator ExplodeRoutine()
{
m_animator.Play("PlayerExplodingAnim");
AnimationClip clip = GetControllerClipFromName(m_animator, "PlayerExplodingAnim");
WaitForSeconds defaultWait = new WaitForSeconds(clip.length);
while (true)
{
AnimatorStateInfo info = m_animator.GetCurrentAnimatorStateInfo(0);
if (info.IsName("PlayerExplodingAnim"))
{
if (info.length <= info.normalizedTime)
break;
else
yield return new WaitForSeconds(info.length - info.normalizedTime);
}
else
yield return defaultWait;
}
gameObject.SetActive(false);
}
public static AnimationClip GetControllerClipFromName(Animator anim, string name)
{
AnimationClip[] clips = anim.runtimeAnimatorController.animationClips;
for (int i = 0; i < clips.Length; i++)
{
if (clips[i].name.Equals(name))
return clips[i];
}
return null;
}