Description

Context

Blueprint actors placed on the map can bind to another actor's event. If an ActorB has a reference to ActorA, it can bind to an event dispatcher in A by creating an event node in ActorB's event graph that's automatically bound to A at editor-time and runtime in AActor::ExecuteConstruction.

Problem

In UE 5.4.4 and prior this worked as intended. Starting from UE 5.5 recompiling ActorB's class results in duplicate bindings accumulating. In-game this means the event is executed more than once and this becomes more the more often the class is recompiled.

Likely cause{}

Since UE 5.5 weak object pointers are fixed up when objects are reinstanced. This is the cause of this bug: dynamic bindings are saved as weak object pointers. Since those automatically became stale prior to UE 5.5, dynamic bindings didn't need explicit cleanup. Now that weak object pointers are fixed up, the bindings from previous actor class iterations need to be explicitly cleaned up.

Suggested Workaround{}

UE 5.5 projects affected by this bug can disable the weak object pointer fixup behavior by running the following console command or setting this cvar:

UObject.UseSerializeToFindWeakReferencers 0

Recompiling the actor blueprint that binds to another actor will then cleanup all existing bindings. Loading a level with such actors will also cleanup existing duplicate bindings. Only the new, valid binding will remain.

Suggested Fix

Since multicast delegate bindings relied on weak object pointers becoming stale, and we intentionally don't let weak object pointers become stale anymore during object reinstancing, the fix should likely involve explicitly cleaning up multicast delegate bindings before readding them. Since duplicate bindings can have been serialized by resaving in UE 5.5, the fix should also clean up those duplicates.

Steps to Reproduce
  • Create an actor class 'BP_ActorA' with an blueprint Event Dispatcher 'MyEvent'.
    • In BP_ActorA's event graph, on BeginPlay: Call MyEvent.
  • Create an actor class 'BP_ActorB' that has a editable variable of type BP_ActorA 'ActorARef'
  • Select the variable 'ActorARef' in the blueprint editor. In the details panel press the + next to 'My Event'.
    • This creates an Event Node in BP_ActorB's event graph binding to BP_ActorA's event. Print a string "Event fired!" in that event.
  • Place both in a level and assign the placed BP_ActorA to BP_ActorB's ActorARef
  • Recompile BP_ActorB 10 times.
  • Start playing in editor. BP_ActorA will then call MyEvent.
  • Observe: BP_ActorB prints a string 10 times (or however many times you recompiled).
  • Expected: BP_ActorB prints a string just once. It should only have bound to BP_ActorA's event once.
Callstack

Callstack is non-fatal, serves as reference:

>    UnrealEditor-CoreUObject.dll!FMulticastInlineDelegateProperty::AddDelegate(TScriptDelegate<FNotThreadSafeDelegateMode> ScriptDelegate, UObject * Parent, void * PropertyValue) Line 492    C++
     UnrealEditor-Engine.dll!UComponentDelegateBinding::BindDynamicDelegates(UObject * InInstance) Line 34    C++
     UnrealEditor-Engine.dll!UBlueprintGeneratedClass::BindDynamicDelegates(const UClass * ThisClass, UObject * InInstance) Line 1605    C++
     UnrealEditor-Engine.dll!AActor::ExecuteConstruction(const UE::Math::TTransform<double> & Transform, const FRotationConversionCache * TransformRotationCache, const FComponentInstanceDataCache * InstanceDataCache, bool bIsDefaultTransform, ESpawnActorScaleMethod TransformScaleMethod) Line 942    C++
     UnrealEditor-UnrealEd.dll!FActorReplacementHelper::Finalize(const TMap<UObject *,UObject *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UObject *,UObject *,0>> & OldToNewInstanceMap, const TSet<UObject *,DefaultKeyFuncs<UObject *,0>,FDefaultSetAllocator> * ObjectsThatShouldUseOldStuff, const TArray<UObject *,TSizedDefaultAllocator<32>> & ObjectsToReplace, const TMap<FSoftObjectPath,UObject *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<FSoftObjectPath,UObject *,0>> & ReinstancedObjectsWeakReferenceMap) Line 1562    C++
     UnrealEditor-UnrealEd.dll!FBlueprintCompileReinstancer::ReplaceInstancesOfClass_Inner(const TMap<UClass *,UClass *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass *,UClass *,0>> & InOldToNewClassMap, const FReplaceInstancesOfClassParameters & Params) Line 2947    C++
     UnrealEditor-UnrealEd.dll!FBlueprintCompileReinstancer::BatchReplaceInstancesOfClass(const TMap<UClass *,UClass *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass *,UClass *,0>> & InOldToNewClassMap, const FReplaceInstancesOfClassParameters & Params) Line 1941    C++
     UnrealEditor-Kismet.dll!FBlueprintCompilationManagerImpl::FlushReinstancingQueueImpl(bool bFindAndReplaceCDOReferences, TMap<UClass *,TMap<UObject *,UObject *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UObject *,UObject *,0>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass *,TMap<UObject *,UObject *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UObject *,UObject *,0>>,0>> * OldToNewTemplates) Line 2035    C++
     UnrealEditor-Kismet.dll!FBlueprintCompilationManagerImpl::CompileSynchronouslyImpl(const FBPCompileRequest & Request) Line 377    C++
     UnrealEditor-UnrealEd.dll!FKismetEditorUtilities::CompileBlueprint(UBlueprint * BlueprintObj, EBlueprintCompileOptions CompileFlags, FCompilerResultsLog * pResults) Line 777    C++
     UnrealEditor-Kismet.dll!FBlueprintEditor::Compile() Line 4188    C++
 

Have Comments or More Details?

There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-273286 in the post.

0
Login to Vote

Unresolved
ComponentUE - Framework - Blueprint
Affects Versions5.5
Target Fix5.6
CreatedApr 17, 2025
UpdatedApr 17, 2025
View Jira Issue