AnimNotifies not reliable in certain circumstances

Problems with the reliability of AnimNotifyState::EndNotify.

(Expected behavior, but important to note: If you cancel/restart a montage during tick, a previous same frame notify will be delivered incorrectly or late, after the Montage End. Stting the notify to Branchpoint helps.)

Now the real bugs:

  1. When branching back to the same animation, all NotifyState guarantees are off
    1. Re-starting the same animation before another is finished, will screw up your notifies and their callbacks
    2. EVEN with Branchpoint=true, this is still true
  2. When canceling an ability montage with a regular montage, the ability montage entity may receive delegate callbacks that came from a different montage. This is because non-ability montages don’t increment the LocalAnimMontageInfo.PlayInstanceId.

For these reasons, I made these changes:

(Solution attempt 1)

-at first, created ReliableNotifyState with isNativeBranchingPoint=true -- THIS DID NOT FIX IT.

(Solution 2)

-created ReliableNotifyState, which is a global list of all NotifyStates that have been started. When a Montage Ended event is received, I ForceNotifyEnd on all notifies that match the Montage/Animatable key.

Solution 2 partially worked - unless I branch to the SAME montage. Example: the player taps melee, then waits for the branch window and cancels the tail of the melee by restarting it (branch back into restarting itself.)

So the problem here is that you just can’t tell the difference between notifies that were triggered from the 1st or 2nd melee. The notifyEnds don’t get flushed before the new montage is started (not even when using branchpoint) so it appears you never received the EndNotify from the previous montage.

(Solution 3) I exposed MontageEndedEvent.MontageInstanceID like so:

void UAnimInstance::TriggerMontageEndedEvent(const FQueuedMontageEndedEvent& MontageEndedEvent)

{

OnMontageEnded.Broadcast(MontageEndedEvent.Montage, MontageEndedEvent.bInterrupted, MontageEndedEvent.MontageInstanceID);

Now I can tell whether an EndNotify came from a previous montage play.

------------------

Question 1: The current documentation really glosses over this and makes it seem like this should all “just work”. Is there an alternate solution? Is this expected when branching back into the same montage and triggering montage stops from outside GAS? Maybe the documentation could include a little mention of these details.

Steps to Reproduce
Create a montage with Notifies and NotifyStates. Trigger it from an GAS Ability. Make the GAS ability re-triggerable InstancePerActor. Have other systems also able to trigger montages from blueprint which can cancel the ability, then have the user re-trigger the ability after the cancel.

  1. If you cancel a montage due to notify handling from inside the montage, a previous same frame notify can fail to be sent
    1. setting the notify to Branchpoint seems to fix this
  2. When branching back to the same animation, all NotifyState guarantees are off
    1. Re-starting the same animation before another is finished, will screw up your notifies
    2. EVEN with Branchpoint=true, this is still true
  3. When canceling an ability montage with a regular montage, the ability montage entity may receive notifies that came from a different montage. This is because non-ability montages don’t increment the LocalAnimMontageInfo.PlayInstanceId.

Hi, it sounds like there’s a variety of issues here, some that are related to purely Montage behaviour and others that are related to GAS’s use of montages. I’ll go through the points you mentioned in your repro steps as a starting point:

> If you cancel a montage due to notify handling from inside the montage, a previous same frame notify can fail to be sent

I don’t think we’ve seen this one previously. I’ve been trying to get a repro but I haven’t had any luck so far. Can you show me how you’re cancelling the montage - is this just by playing another montage, or are you cancelling the GAS ability?

>When branching back to the same animation, all NotifyState guarantees are off

Re-starting the same animation before another is finished, will screw up your notifies

This issue is related to the way that we sample notify states. The current functionality is overly simplistic in the way that it detects when a state notify beings/ends. It’s just based on whether the state notify was active on the last frame or not. This causes various bugs with state notifies - for instance, a state notify that lasts for the entire duration of the animation is never seen to have finished/started when the animation loops. And re-entering animations/montages fails to fire the begin/end events as you’ve seen.

I’m going to add a JIRA to track this since it has come up a few times recently. We are also planning on revisiting notify functionality for integration with UAF so I expect this behaviour will be fixed at that point at the latest.

> When canceling an ability montage with a regular montage, the ability montage entity may receive notifies that came from a different montage. This is because non-ability montages don’t increment the LocalAnimMontageInfo.PlayInstanceId.

Similar to the first issue, I couldn’t get a repro on this, so it would be useful to get more information on your setup. Are you using the PlayMontageAndWait ability function, and if so, is it the OnCompleted delegate that’s firing incorrectly?

Looking at the code for that ability, I see that the delegate should be bound to the specific montage instance via UAnimInstance::Montage_SetEndDelegate. And that should be fired when the montage instance is blended out. So I expect that I’m missing something with your setup.

I did run into a different issue with the OnCompleted delegate from PlayMontageAndWait which is that the ability can be destroyed before the montage has blended out, in which case the delegate is never fired. I’m going to add a JIRA for that since it also seems like a bug.

> (Solution 3) I exposed MontageEndedEvent.MontageInstanceID like so: …

This change seems reasonable. We could make this change to add that functionality. I take it that you’re tracking the montage instance IDs somehow when you play each montage in order for this to be useful?

Hello,

we are currently also encountering issues when re-starting the same montage again.

So we are testing the following code before starting a new montage:

`if (FAnimMontageInstance* Instance = AnimInstance->GetInstanceForMontage(MontageToStart))
{
FMontageBlendSettings BlendOutSettings;

BlendOutSettings.Blend = MontageToStart->BlendOut;
BlendOutSettings.Blend.BlendTime = 0;
BlendOutSettings.BlendMode = MontageToStart->BlendModeOut;
BlendOutSettings.BlendProfile = MontageToStart->BlendProfileOut;

Instance->Stop(BlendOutSettings, true);

Instance->Terminate();
}`and at the moment it looks like nothing breaks.

Kind regards

Moritz Grunwald

Hi [mention removed]​, I had a bit more time to look into this again over the last few days. I’ve also discussed it with the dev team. In Fortnite, we only really trigger montages via GAS and not via the regular montage playback functionality which is likely why we haven’t run into this kind of problem previously.

Having said that, I’m still unsure about exactly what the problem is that you’re running into. You mentioned that you fixed the problem by changing the OnMontageEnded delegate signature to take the montage instance ID. But the GAS code doesn’t use the UAnimInstance::OnMontageEnded delegate. Did you bind some custom code to this? Or did you mean FAnimMontageInstance::OnMontageEnded delegate rather than the anim instance equivalent? That one is bound in the GAS code. It would be useful if I can get a repro before committing any changes.

Hello, I’d like to ask if this issue has been fixed yet.
I tested it and found that the AnimNotifyState works correctly when re-starting the same animation before it is finished, but NotifyBegin of the next animation still triggers before NotifyEnd of the cancelled animation when the AnimNotifyState starts near the first frame.

I don’t get it, and I don’t get why someone at Epic wouldn’t know that Notify was never ment to be used to “guarantee” anything.

The docs on them are/were pretty darn clear.

The only notifies ever ment to be a “guarantee” are not really notifies but essentially event calls masked as notifies that can be placed at state machine transitions.

Everything else is “don’t use a notify if you need it to fire reliably" and has always been that way…

Expecting a notify in a montage to trigger anything with a 100% guarantee is not only wrong, but bad practice too…

There are alternatives, most in the form of 3rd party plugins. If you need timing to be right, then you need time as the underlying function.

A simplest use case is “this montage is playing, we know the montage, we set a manual delay of x seconds before continuing on with whatver code needs to run”. Obviously you need to clean that up and allow cancelations.

I didn’t see any mention of timing guarantees in the documentation — perhaps I missed it. In my opinion, Notify and NotifyState seem like the perfect tools for animation frame-based logic, which is probably why many new users like me regard them as such.

By the way, this is the document I reffered to:
Animation Notifies in Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

I must admit, it’s really surprising to learn that timing guarantees were never their intended purpose. It never occurred to me that they were designed this way until you pointed it out. If they were truly reliable, they’d be much more powerful.

Is there any official word on whether this design will remain the same going forward?