Description

Context

In blueprint, one can bind to a referenced actor’s events by clicking an actor reference variable, and creating an event node through the variable’s detail panel. Under the hood, this adds an entry to the BlueprintGeneratedClass’s DynamicBindingObjects, which sets up the binding during AActor::ExecuteConstruction by modifying the referenced actor’s FMulticastInlineDelegateProperty (Event Dispatcher) value.

[Image Removed]

Problem

The referenced actor can end up in an invalid state, when the referencing and the referenced actors aren’t saved in tandem on a map with OFPA enabled. For example, clearing the reference but not resaving the referenced actor results in the cleared binding being reloaded. To make matters worse, the referenced actor is not marked dirty despite its Event Dispatcher state being changed.

If BP_ActorB binds to BP_ActorA’s event dispatcher, that binding is stored in BP_ActorA’s state. If BP_ActorB clears the reference to BP_ActorA, that binding is cleaned up and BP_ActorB is dirtied. However, BP_ActorA is not marked as dirty despite BP_ActorB having made a modification to A’s state. When reloading the map, BP_ActorA will still have BP_ActorB in its callback list.

Alternatively, if BP_ActorB’s event node is deleted, BP_ActorA still keeps an entry in its event dispatcher’s invocation list, despite that the function no longer exists. This will never get cleaned up, except if BP_ActorB is deleted from the map.

The following snippet can be used during debugging to print out the number of entries in BP_ActorA’s event dispatcher’s invocation list (assuming BP_ActorA’s event dispatcher is called ‘NewEventDispatcher’):

void AMyActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);
	PrintOutNumDelegateBindings("OnConstruction");
}

void AMyActor::BeginPlay()
{
	Super::BeginPlay();
	PrintOutNumDelegateBindings("BeginPlay");
}

void AMyActor::PrintOutNumDelegateBindings(const FString& Prefix)
{
	if (FMulticastInlineDelegateProperty* Prop = (FMulticastInlineDelegateProperty*)GetClass()->FindPropertyByName(FName("NewEventDispatcher")))
	{
		FMulticastScriptDelegate* ScriptDelegate = nullptr;
		ScriptDelegate = Prop->GetPropertyValuePtr_InContainer(this);
		const int32 NumObjects = ScriptDelegate->GetAllObjects().Num();
		UE_LOG(LogTemp, Warning, TEXT("%s | Delegates num: %d"), *Prefix, NumObjects);
	}
}

Suggested Fix

At the very least, BP_ActorA should be marked dirty when BP_ActorB sets or clears a reference to BP_ActorA and it has an event node that auto-binds to its event dispatcher. Ideally, BP_ActorA’s event dispatcher entries are checked on load and unintentional callbacks are cleaned up.

Steps to Reproduce

There are multiple ways to reproduce dangling bindings.

Shared Setup

  • Create an actor blueprint BP_ActorA
    • Give it a Event Dispatcher ‘MyEvent’
    • Call ‘MyEvent’ on BeginPlay
  • Create an actor blueprint BP_ActorB
    • Give it an editable variable: a BP_ActorA reference
    • Click the variable, in the details panel under ‘Events’ press the + next to MyEvent
    • This creates an event node that executes when a referenced BP_ActorA broadcasts MyEvent.
    • Print “Hello” when the event executes
  • Create an open world map (it’s important that the map uses One-File-Per-Actor
  • Place BP_ActorA and BP_ActorB into it.
  • Assign BP_ActorB the reference to BP_ActorA.
  • Start PIE and observe that “Hello” prints.
  • Move both BP_ActorA and BP_ActorB slightly (ensuring they are dirtied) and save the map.

Repro 1:

  • Both BP_ActorA and BP_ActorB must be saved after making BP_ActorB reference BP_ActorA, so the binding is valid.
  • Now, clear BP_ActorB’s reference to BP_ActorA. Save the map.
  • Reload the map. Start PIE.
  • Observe: “Hello” still prints. BP_ActorA fires BP_ActorB’s event.
  • Expected: “Hello” doesn’t print, because BP_ActorB doesn’t reference BP_ActorA anymore. Thus the event node in BP_ActorB shouldn’t fire.

Repro 2:

  • Delete BP_ActorB’s event node that binds to ActorA' Event Dispatcher. Recompile BP_ActorB.
  • Use the snippet from the description to print out how many callbacks are stored in ActorA’s Event Dispatcher.
  • Start PIE.
  • Observe: There is still 1 callback in the invocation list. This is a weak reference to BP_ActorB, and a function name that no longer exists.
  • Expected: There should be 0 callbacks in the invocation list.
Callstack

Non-fatal callstack. Just attaching this as a reference: this is an example where recompiling one actor class modifies the referenced actor’s Event Dispatcher (FMulticastDelegateProperty).

>	UnrealEditor-Engine.dll!UComponentDelegateBinding::BindDynamicDelegates(UObject * InInstance) Line 32	C++
 	UnrealEditor-Engine.dll!UBlueprintGeneratedClass::BindDynamicDelegates(const UClass * ThisClass, UObject * InInstance) Line 1621	C++
 	UnrealEditor-Engine.dll!AActor::ExecuteConstruction(const UE::Math::TTransform<double> & Transform, const FRotationConversionCache * TransformRotationCache, const FComponentInstanceDataCache * InstanceDataCache, bool bIsDefaultTransform, ESpawnActorScaleMethod TransformScaleMethod) Line 958	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 1563	C++
 	UnrealEditor-UnrealEd.dll!FBlueprintCompileReinstancer::ReplaceInstancesOfClass_Inner(const TMap<UClass *,UClass *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass *,UClass *,0>> & InOldToNewClassMap, const FReplaceInstancesOfClassParameters & Params) Line 2962	C++
 	UnrealEditor-UnrealEd.dll!FBlueprintCompileReinstancer::BatchReplaceInstancesOfClass(const TMap<UClass *,UClass *,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<UClass *,UClass *,0>> & InOldToNewClassMap, const FReplaceInstancesOfClassParameters & Params) Line 1942	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 2067	C++
 	UnrealEditor-Kismet.dll!FBlueprintCompilationManagerImpl::CompileSynchronouslyImpl(const FBPCompileRequest & Request) Line 380	C++
 	UnrealEditor-UnrealEd.dll!FKismetEditorUtilities::CompileBlueprint(UBlueprint * BlueprintObj, EBlueprintCompileOptions CompileFlags, FCompilerResultsLog * pResults) Line 801	C++
 	UnrealEditor-Kismet.dll!FBlueprintEditor::Compile() Line 4235	C++

Have Comments or More Details?

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

0
Login to Vote

Unresolved
CreatedFeb 19, 2026
UpdatedFeb 19, 2026
View Jira Issue