Hi RainyRizzle, I need an OnAnimationFinished event. How can I achieve this? Is there a native solution?
Explanation (Why I need this?):
I am writing an AnyPortrait animation controller and this controller takes all animClipNames into an array.
Variables:
[SerializeField]
private apPortrait[] portraits; // Array of AnyPortrait objects
[SerializeField]
private List<PortraitAnimationData> portraitAnimationDataList = new List<PortraitAnimationData>(); // List of animation data for each portrait
private int[] currentAnimationIndices; // Array to track currently played animation indices for each portrait
public bool enableDebugLogs = true; // Enable or disable debug logs
A data class:
[System.Serializable]
public class PortraitAnimationData
{
public List<string> animationClipNames = new List<string>(); // List of animation clip names
public List<string> morphControllerParamNames = new List<string>(); // List of morph controller param names
}
Initialization:
private void Awake()
{
// Automatically assign portraits from the scene if not manually set
if (portraits == null || portraits.Length == 0)
{
portraits = FindObjectsOfType<apPortrait>();
}
// If no portraits found, log an error
if (portraits.Length == 0)
{
if (enableDebugLogs) Debug.LogError("No apPortrait objects found in the scene.");
return;
}
// Initialize the portrait animation data list and the current animation indices
InitializeData();
}
private void Start()
{
// Populate the portraitAnimationDataList and currentAnimationIndices at the start
FillAnimationData();
}
private void InitializeData()
{
// Initialize the animation indices
currentAnimationIndices = new int[portraits.Length]; // Make sure currentAnimationIndices is initialized
}
private void FillAnimationData()
{
// Initialize the list if it's empty
if (portraitAnimationDataList.Count == 0)
{
for (int i = 0; i < portraits.Length; i++)
{
portraitAnimationDataList.Add(new PortraitAnimationData());
}
}
// Now fetch animation data for each portrait
for (int i = 0; i < portraits.Length; i++)
{
FetchAnimationClipNames(i); // Fetch animation clip names
FetchMorphControllerParamNames(i); // Fetch morph controller param names
}
}
private void FetchAnimationClipNames(int portraitIndex)
{
if (portraitIndex < 0 || portraitIndex >= portraits.Length)
{
if (enableDebugLogs) Debug.LogWarning("Invalid portrait index.");
return;
}
var portrait = portraits[portraitIndex];
var animClips = portrait._animClips;
List<string> clipNames = new List<string>();
foreach (var clip in animClips)
{
if (clip != null && !string.IsNullOrEmpty(clip._name))
{
clipNames.Add(clip._name);
}
}
portraitAnimationDataList[portraitIndex].animationClipNames = clipNames;
}
private void FetchMorphControllerParamNames(int portraitIndex)
{
if (portraitIndex < 0 || portraitIndex >= portraits.Length)
{
if (enableDebugLogs) Debug.LogWarning("Invalid portrait index.");
return;
}
var portrait = portraits[portraitIndex];
var controlParams = portrait._controller._controlParams;
List<string> paramNames = new List<string>();
foreach (var param in controlParams)
{
if (param != null && !string.IsNullOrEmpty(param._keyName))
{
paramNames.Add(param._keyName);
}
}
portraitAnimationDataList[portraitIndex].morphControllerParamNames = paramNames;
}
Then I switch between animations with buttons using PlayNextAnim() and PlayPreviousAnim() functions.
PlayNextAnim()
public void PlayNextAnim(int portraitIndex)
{
if (portraitIndex < 0 || portraitIndex >= portraits.Length)
{
if (enableDebugLogs) Debug.LogWarning("Invalid portrait index.");
return;
}
var animationData = portraitAnimationDataList[portraitIndex];
if (animationData.animationClipNames == null || animationData.animationClipNames.Count == 0)
{
if (enableDebugLogs) Debug.LogWarning("No animation clips found for the selected portrait.");
return;
}
if (currentAnimationIndices[portraitIndex] < animationData.animationClipNames.Count - 1)
{
currentAnimationIndices[portraitIndex]++;
string nextAnim = animationData.animationClipNames[currentAnimationIndices[portraitIndex]];
PlayAnimation(portraitIndex, nextAnim);
if (!IsAnimLooped(portraitIndex, nextAnim))
{
// Hook up the completion check to automatically move to the next animation
// Coding is required here
}
}
else
{
if (enableDebugLogs)
Debug.LogWarning($"Already at the last animation for portrait {portraits[portraitIndex].name}.");
}
PlayPreviousAnim()
public void PlayPreviousAnim(int portraitIndex)
{
if (portraitIndex < 0 || portraitIndex >= portraits.Length)
{
if (enableDebugLogs) Debug.LogWarning("Invalid portrait index.");
return;
}
var animationData = portraitAnimationDataList[portraitIndex];
if (animationData.animationClipNames == null || animationData.animationClipNames.Count == 0)
{
if (enableDebugLogs) Debug.LogWarning("No animation clips found for the selected portrait.");
return;
}
// If there's a previous animation, decrement the index
if (currentAnimationIndices[portraitIndex] > 0)
{
currentAnimationIndices[portraitIndex]--;
PlayAnimation(portraitIndex, animationData.animationClipNames[currentAnimationIndices[portraitIndex]]);
}
else
{
if (enableDebugLogs)
Debug.LogWarning($"Already at the first animation for portrait {portraits[portraitIndex].name}.");
}
}
Think of many animation clips, some animations are looped, some are not. Here's my function to check if it's looped or not.
IsAnimLooped()
private bool IsAnimLooped(int portraitIndex, string animClipName)
{
var portrait = portraits[portraitIndex];
if (portrait == null)
return false;
var animClip = portrait._animClips.Find(clip => clip._name == animClipName);
if (animClip != null)
{
bool IsLooped = animClip.IsLoop;
if (IsLooped)
{
if (enableDebugLogs)
Debug.LogWarning($"Animation clip '{animClipName}' is looped");
}
else
{
if (enableDebugLogs)
Debug.LogWarning($"Animation clip '{animClipName}' is not looped.");
}
return IsLooped;
}
if (enableDebugLogs)
Debug.LogWarning($"Animation clip '{animClipName}' not found in portrait {portrait.name}.");
return false;
}
Most of them are idle animations, but there are also transition animations.
[What I want?]
After the transitions (non-looped animations) are finished, I want the next loop animation to play automatically.
Multiple scenarios:
Scenario 1 (one non-looped animation between two looped animations):
0) Looped Anim (player button click is required for next anim) -> 1) Non-looped Anim (auto skip to next anim when animation finished) -> 2) Looped Anim (player button click is required for next anim)
Scenario 2 (two non-looped animation between two looped animations):
0) Looped Anim (player button click is required for next anim) -> 1) Non-looped Anim (auto skip to next anim when animation finished) -> 2) Non-looped Anim (auto skip to next anim when animation finished) -> 3) Looped Anim (player button click is required for next anim)
I hope you understand what I'm trying to do. Thank you in advance!
Hi!
It seems like you want to know when the animation ends, so you can play the animation continuously.
So it would be more helpful to answer about playing using a queue rather than whether the animation ends.
There are two main ways to make animations play continuously.
1. Use functions with "Queued" attached, such as PlayQueued and CrossFadeQueued.
2. Add an "Animation Event" to the end of the animation, and play the next animation when the event is called.
In our opinion, it is convenient and good to use the PlayQueued function when requesting to play Non-Loop and Loop animations sequentially.
For example, you can write the playback code in one function like this:
void PlayAnimA() { portrait.Play("Transition to Anim A"); //(Non-Loop Animation) portrait.PlayQueued("Anim A"); }
However, in most cases, it is more useful to utilize "Animation Events" to create games.
https://rainyrizzle.github.io/en/AdvancedManual/AD_AnimationEvents.html
The above methods are generally recommended when the character's animations are played according to developer-defined rules.
However, your goal seems to be that the user wants to play the character's motions freely.
If complex playback requests occur simultaneously and unintentionally by the user, additional rules will be needed to handle them.
Depending on how you set up the rules, you may need to write additional code beyond the functions we provide.
So after reading your post, we prepared a simple example code.
See how the implementation changes depending on how you control the Queued method.
[ Method 1 ]
The first way is to use the "PlayQueued" function provided by AnyPortrait.
For this function, please refer to the following manual.
https://rainyrizzle.github.io/en/Script/SC_Animation.html
public class AnimScriptTest : MonoBehaviour { public apPortrait portrait; public string animName_1; public string animName_2; public string animName_3; void Update() { if (Input.GetKeyDown(KeyCode.Alpha1)) { PlayAnimAutoQueued(animName_1); } if (Input.GetKeyDown(KeyCode.Alpha2)) { PlayAnimAutoQueued(animName_2); } if (Input.GetKeyDown(KeyCode.Alpha3)) { PlayAnimAutoQueued(animName_3); } } private void PlayAnimAutoQueued(string animName) { // Method 1. portrait.PlayQueued(animName); } }
The above example contains content that plays 3 animations based on keyboard input.
It is convenient to write because the animations are played immediately or after a while by the "portrait.PlayQueued(...)" code.
However, PlayQueued has the characteristic of continuously overlapping animation playback requests.
Since the number of animations waiting continues to increase, many animations will continue to be waiting until the general playback function Play is called.
In other words, if the user presses the "Next Anim" button multiple times, many animations will be waiting to be played.
In addition, this method is likely to malfunction for playback requests that are entered indiscriminately.
So if you want to improve this and limit the number of waiting animations, try the second method.
[ Method 2 ]
The second method is to use Coroutines, which can be implemented by slightly modifying the existing script.
public class AnimScriptTest : MonoBehaviour { public apPortrait portrait; public string animName_1; public string animName_2; public string animName_3; //Currently playing animation data (Method 2, 3) private apAnimPlayData _curAnimPlayData = null; void Update() { if (Input.GetKeyDown(KeyCode.Alpha1)) { PlayAnimAutoQueued(animName_1); } if (Input.GetKeyDown(KeyCode.Alpha2)) { PlayAnimAutoQueued(animName_2); } if (Input.GetKeyDown(KeyCode.Alpha3)) { PlayAnimAutoQueued(animName_3); } } private void PlayAnimAutoQueued(string animName) { // Method 1. //portrait.PlayQueued(animName); // Method 2. StopAllCoroutines(); StartCoroutine(CheckQueueAndPlay(animName)); } // Play the animation immediately or after a while. private IEnumerator CheckQueueAndPlay(string animName) { apAnimPlayData nextAnimPlayData = portrait.GetAnimationPlayData(animName); // If the animation is invalid or currently playing, this request is ignored. if(nextAnimPlayData == null) { yield break; } if(nextAnimPlayData == _curAnimPlayData && nextAnimPlayData.PlaybackStatus == apAnimPlayData.AnimationPlaybackStatus.Playing) { yield break; } if(_curAnimPlayData == null) { //If there is no animation playing, it starts playing immediately. _curAnimPlayData = nextAnimPlayData; portrait.Play(_curAnimPlayData); yield break; } else if(_curAnimPlayData.IsLoop || _curAnimPlayData.PlaybackStatus != apAnimPlayData.AnimationPlaybackStatus.Playing) { //If the currently playing animation is a loop type or has finished playing, it will continue playing. _curAnimPlayData = nextAnimPlayData; portrait.CrossFade(_curAnimPlayData); yield break; } //Wait for the currently playing animation to finish. while(true) { //(Conditional statement for error handling) if(_curAnimPlayData == null) { break; } //If the playing animation has finished, the loop ends. if(_curAnimPlayData.IsLoop || _curAnimPlayData.PlaybackStatus != apAnimPlayData.AnimationPlaybackStatus.Playing) { break; } //Wait until the next frame. yield return new WaitForEndOfFrame(); } // Apply the pending animation request and continue playing the animation. _curAnimPlayData = nextAnimPlayData; portrait.CrossFade(_curAnimPlayData); } }
First, you can see that the "apAnimPlayData _curAnimPlayData" variable has been added.
This is declared to continuously monitor information about the currently playing animation.
StopAllCoroutines();
StartCoroutine(CheckQueueAndPlay(animName));
This is the code that starts the coroutine.
We call StopAllCoroutines() beforehand to invalidate any previous waiting requests and to ensure that only one animation is played or waiting.
private IEnumerator CheckQueueAndPlay(string animName) {...}
This is a coroutine function.
Depending on the validity of the data and the playback status, it waits or tries to play.
You can see a while loop that continues to wait until the condition is met.
Also check out using CrossFade instead of Play for a smooth transition.
This method is effective in preventing malfunctions because only one animation is queued at most, even if the user generates numerous playback requests.
However, this method does not implement one of your requirements, which is to insert two consecutive Transition animations.
In order to meet the requirement, you will eventually need to make the Queue where the animations wait larger than 1.
In that case, the third method, which creates the Queue directly, will work.
[ Method 3 ]
The third method goes back to the method 1 of acknowledging a large number of pending requests.
This method may seem like a lot of hard work for the same code, but it has the great advantage of allowing the user to manage the Queue directly, rather than relying on the internal system of AnyPortrait.
The code below is not complete, but provides a basis for implementing your own requirements.
You can modify the following code to limit the size of the Queue, or to limit the combination of certain animations, as you wish.
public class AnimScriptTest : MonoBehaviour { public apPortrait portrait; public string animName_1; public string animName_2; public string animName_3; //Currently playing animation data (Method 2, 3) private apAnimPlayData _curAnimPlayData = null; //A queue where animation play requests are stored. (Method 3) private List<apAnimPlayData> _queuedData = null; void Update() { if (Input.GetKeyDown(KeyCode.Alpha1)) { PlayAnimAutoQueued(animName_1); } if (Input.GetKeyDown(KeyCode.Alpha2)) { PlayAnimAutoQueued(animName_2); } if (Input.GetKeyDown(KeyCode.Alpha3)) { PlayAnimAutoQueued(animName_3); } //Automatically play queued animations by updating the animation queue every frame. UpdateQueue(); } private void PlayAnimAutoQueued(string animName) { // Method 1. //portrait.PlayQueued(animName); // Method 2. //StopAllCoroutines(); //StartCoroutine(CheckQueueAndPlay(animName)); // Method 3. apAnimPlayData nextAnimPlayData = portrait.GetAnimationPlayData(animName); if(nextAnimPlayData == null) { return; } if(_queuedData == null) { _queuedData = new List<apAnimPlayData>(); } _queuedData.Remove(nextAnimPlayData); _queuedData.Add(nextAnimPlayData); } // A function that attempts to play animations by pulling them out one by one from the queue where they are stored. private void UpdateQueue() { int nQueue = _queuedData != null ? _queuedData.Count : 0; if(nQueue == 0) { return; } //Take the first animation data from the Queue and check it. apAnimPlayData nextAnimPlayData = _queuedData[0]; //If the data is not valid, the data is discarded and checked in the next frame. if(nextAnimPlayData == null) { _queuedData.Remove(nextAnimPlayData); return; } //If it is the same as the currently playing data, //it is removed from the queue and checked again in the next frame. if(nextAnimPlayData == _curAnimPlayData && nextAnimPlayData.PlaybackStatus == apAnimPlayData.AnimationPlaybackStatus.Playing) { _queuedData.Remove(nextAnimPlayData); return; } if(_curAnimPlayData == null) { //If there is no data being played, play immediately. _curAnimPlayData = nextAnimPlayData; portrait.Play(_curAnimPlayData); //Remove the animation from the Queue. _queuedData.Remove(nextAnimPlayData); } else if(_curAnimPlayData.IsLoop || _curAnimPlayData.PlaybackStatus != apAnimPlayData.AnimationPlaybackStatus.Playing) { //If the current animation is a loop type or has ended, it continues playing. _curAnimPlayData = nextAnimPlayData; portrait.CrossFade(_curAnimPlayData); //Remove the animation from the Queue. _queuedData.Remove(nextAnimPlayData); } } }
List<apAnimPlayData> _queuedData = null;
Create an animation queue that you can control as a variable.
Control this variable and write your own constraints.
UpdateQueue();
The function that automatically plays the animation by updating the queue is called every frame in the Update function.
private void PlayAnimAutoQueued(string animName) { ... }
Modify the function that plays the animation.
The modified function does not play the animation directly, but only puts it in the Queue.
The animation will be played automatically in the UpdateQueue() function afterwards.
Also make sure to perform Remove at the same time to avoid adding duplicate animations to the Queue.
private void UpdateQueue() { ... }
This function is called every frame.
It compares the animation data that was first added to the queue with the current animation status to decide whether to play it.
If the condition is not met, this function does not perform any separate action.
On the other hand, if the condition is met, it takes the animation at the very front of the queue and plays it.
The above code is a rough idea we came up with to implement your goal.
We wrote it in a hurry, so there may be problems.
Rather than using this code as is, we recommend that you check what idea the code was written based on.
We hope our answer was helpful.
Thank you.
P.S. Our team's work for today is over.
If you need any additional assistance, we will be happy to help you the next day.