Quick answer: NotifyEnd does not always fire when a montage blends out or is interrupted. Track active notifies in the owning AnimInstance, listen on OnMontageBlendingOut, and run cleanup explicitly when the montage ends abnormally.
Here is how to fix Unreal Anim Notify State setups where state set in NotifyBegin (collision boxes enabled, particle systems spawned) is never cleaned up because NotifyEnd does not run on interrupt. The character cancels into another montage, leaving an attack hitbox active forever.
The Symptom
An AnimNotifyState enables a hitbox in NotifyBegin and disables in NotifyEnd. The character starts attacking, gets hit and cancels into a hit-react montage. The hitbox stays on, applying damage forever.
What Causes This
NotifyEnd skipped on interrupt. When montage blends out before reaching the notify’s end frame, NotifyEnd is not called.
Montage cancellation. Stopping a montage mid-play does not run pending notify ends.
Anim Blueprint update lag. Notifies fire from the animation update; if the AnimBP fails to update one frame for any reason, end frames can pass without firing.
The Fix
Step 1: Track active notifies in the AnimInstance.
// AnimInstance header
UPROPERTY()
TArray<UMyHitboxNotifyState*> ActiveNotifies;
virtual void NativeUninitializeAnimation() override;
virtual void OnMontageBlendingOut_Implementation(UAnimMontage* Montage, bool bInterrupted);
Step 2: Run cleanup on blend out.
void UMyAnimInstance::OnMontageBlendingOut_Implementation(UAnimMontage* M, bool bInterrupted)
{
if (bInterrupted)
{
for (auto* notify : ActiveNotifies)
{
if (notify) notify->ForceEndCleanup(this);
}
ActiveNotifies.Empty();
}
}
Step 3: Register/unregister notifies.
void UMyHitboxNotifyState::NotifyBegin(USkeletalMeshComponent* mesh, UAnimSequenceBase* anim, float dur, const FAnimNotifyEventReference& ref)
{
UMyAnimInstance* ai = Cast<UMyAnimInstance>(mesh->GetAnimInstance());
if (ai) ai->ActiveNotifies.Add(this);
EnableHitbox(mesh);
}
void UMyHitboxNotifyState::NotifyEnd(USkeletalMeshComponent* mesh, UAnimSequenceBase* anim, const FAnimNotifyEventReference& ref)
{
UMyAnimInstance* ai = Cast<UMyAnimInstance>(mesh->GetAnimInstance());
if (ai) ai->ActiveNotifies.Remove(this);
DisableHitbox(mesh);
}
void UMyHitboxNotifyState::ForceEndCleanup(UMyAnimInstance* ai)
{
// Run the same cleanup as NotifyEnd
DisableHitbox(ai->GetOwningComponent());
}
Step 4: Cover edge cases on uninitialize.
void UMyAnimInstance::NativeUninitializeAnimation()
{
for (auto* notify : ActiveNotifies)
if (notify) notify->ForceEndCleanup(this);
ActiveNotifies.Empty();
Super::NativeUninitializeAnimation();
}
Handles destruction without leaving notify state alive.
Step 5: For Blueprint, mirror the pattern in BP. Use Begin/End events on the notify state plus On Montage Blending Out on the AnimBP. Same cleanup logic.
“NotifyEnd is best-effort. Track active notifies, hook montage blend-out, force cleanup on interrupt.”
Related Issues
For Anim Blueprint state issues, see Anim Blueprint State Machine. For physics constraint after load, see Physics Constraint After Load.
Track notifies. Force cleanup on blend-out. Hitboxes do not stay on forever.