A more general solution is to set up a notification at some arbitrary time in the animation cycle, where the end of the cycle is a special case. I’ve created a class that holds an animation clip, and fires an event at a specified time in the animation cycle. I will present two equivalent implementations below, one that uses class AnimationEvent (which is sort of the native Unity way), and one that uses a Coroutine to measure the animation time against the notification time.
Personally, I prefer using the second technique. Why? Unity uses its messaging system (e.g. SendMessage()) to fire the AnimationEvent. The message is bound only by the name of the method, and the message is sent to all script components on the GameObject. If some other script on your GameObject has a method with the same name, it too will receive the message, which is almost certainly a bug.
You can mitigate the issue by creating a mangled name for your method, but that gets really ugly–and it doesn’t even completely solve the problem! Consider if you have two instances of the script component on a single GameObject. (You might do this if you have two different clips that you want to monitor.) Now both components receive the message, regardless of the method name. I’ve addressed this by adding the script’s object ID as a parameter on the AnimationEvent, and then checking the value in the message handler. It works, but it does add complexity.
As for sending the notification to the outside world, you could use SendMessage(), but I prefer using a .NET event. It has several advantages: you get better performance; you avoid the binding issue described above; it is architecturally more elegant, as it insulates the class from being concerned with who’s listening for the notification.
So, the prose are nearly complete.
To use this, add it as a component to the GameObject that gets animated. You can set the clip at design time or at run-time. I’ve expressed the time to fire the notification as animation frames, but you could just as well use seconds; if the frame is set to zero, I take that to mean at the end of the clip. You attach a handler to the NotifyFrameReached event, and voila! You have notification that you’ve reached the specified frame!
CAVEATS:
- In the Message-based implementation, I don’t provide means to remove the AnimationEvent; if you were to set a second clip at run-time, the event on the old clip would continue to fire each time it runs.
- When you add a handler to a .NET event, you should make sure to remove it once you no longer need to handle the event; else you will leak memory.
And without further ado, my preferred Coroutine-based implementation:
using UnityEngine;
using System;
using System.Collections;
public class NotifyingAnimControl_CoroutineBased : MonoBehaviour
{
//--------------------------------------------------------------------------
public float notificationFrame;
//--------------------------------------------------------------------------
[SerializeField]
AnimationClip animClip;
//--------------------------------------------------------------------------
public event EventHandler NotifyFrameReached;
//--------------------------------------------------------------------------
protected void RaiseNotifyFrameReached()
{
var handler = NotifyFrameReached;
if( null == handler ) return;
handler( this, EventArgs.Empty );
}
//--------------------------------------------------------------------------
public void SetClip( AnimationClip clip )
{
animClip = clip;
// Note--you can have different wrap modes if you want...
animClip.wrapMode = WrapMode.Once;
ScheduleNotification();
}
//--------------------------------------------------------------------------
public void PlayAnimation()
{
animation.CrossFade( animClip.name );
// NOTE: It is critical to start this *after* starting the clip, else
// the CheckEventTime fails on it's first iteration!
StartCoroutine( CheckEventTime() );
}
//--------------------------------------------------------------------------
void Start()
{
if( null != animClip )
{
SetClip( animClip );
}
}
//--------------------------------------------------------------------------
float EventTime { get; set; }
//--------------------------------------------------------------------------
AnimationState AnimState { get { return animation[ animClip.name ]; } }
//--------------------------------------------------------------------------
protected void ScheduleNotification()
{
EventTime = (0 == notificationFrame)
? animClip.length
: notificationFrame / animClip.frameRate;
}
//--------------------------------------------------------------------------
// The virtue of using this technique (rather than an AnimationEvent) is
// that AnimationEvent fires its signal using SendMessage(), which winds
// up going to all components on the GameObject. If the GameObject has
// more than one of these components on it, they *all* receive the message.
// You then need a filter to figure out if the component is the intended
// receiver, and in the mean time, you are getting superfluous events.
IEnumerator CheckEventTime()
{
while( AnimState.time < EventTime &&
animation.IsPlaying( animClip.name ) )
{
yield return new WaitForEndOfFrame();
}
RaiseNotifyFrameReached();
}
}
And here’s the Messaged-based implementation:
using UnityEngine;
using System;
public class NotifiyingAnimControl_MessageBased : MonoBehaviour
{
//--------------------------------------------------------------------------
public float notificationFrame;
//--------------------------------------------------------------------------
[SerializeField]
AnimationClip animClip;
//--------------------------------------------------------------------------
public event EventHandler NotifyFrameReached;
//--------------------------------------------------------------------------
protected void RaiseNotifyFrameReached()
{
var eventDelegate = NotifyFrameReached;
if( null == eventDelegate ) return;
eventDelegate( this, EventArgs.Empty );
}
//--------------------------------------------------------------------------
public void SetClip( AnimationClip clip )
{
animClip = clip;
// Note--you can have different wrap modes if you want...
animClip.wrapMode = WrapMode.Once;
ScheduleNotification();
}
//--------------------------------------------------------------------------
void Start()
{
if( null != animClip )
{
SetClip( animClip );
}
}
//--------------------------------------------------------------------------
protected void ScheduleNotification()
{
var animEvent = new AnimationEvent();
animEvent.time = (0 == notificationFrame)
? animClip.length
: notificationFrame / animClip.frameRate;
animEvent.functionName = "DoGenericOneShotNotification";
animEvent.intParameter = GetInstanceID();
animClip.AddEvent( animEvent );
}
//--------------------------------------------------------------------------
public void PlayAnimation()
{
animation.CrossFade( animClip.name );
}
//--------------------------------------------------------------------------
void DoGenericOneShotNotification(AnimationEvent animEvent)
{
if( animEvent.intParameter != GetInstanceID() ) return;
RaiseNotifyFrameReached();
}
}