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.