Short Description
So, I’ve been investigating this bug for quite some time. I have also found several posts on UE forums on this issue. It affects several engine versions – I have personally reproduced it on 4.25.4 and 4.26.1 (using both Launcher version and recompiled from source).
The issue happens only with blueprints. There are 2 symptoms I know of:
-
UPROPERTY()
component pointer is somehow set toNULL
inBeginPlay()
though it has been set to a non-NULL
object in C++ constructor usingCreateDefaultSubobject
. - When opening component details tab, no component properties are visible.
Generally, the advice on the forums is to either reparent a Blueprint to some another parent and the reparent back to the original parent, or to just re-create Blueprint from scratch. In both cases you lose component settings stored in Blueprint, which leads to certain amount of work needed to be redone.
How to reproduce
-
Create a new UE4 project using Blank C++ template without starter content.
-
Create a new actor C++ class
ATestActor
. -
Add component property to this class, e.g.:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Test") class UCapsuleComponent* CapsuleComponent;
-
Create default subobject for a component in constructor, e.g.:
ATestActor::ATestActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("OldComponentName")); CapsuleComponent->SetupAttachment(RootComponent); }
-
Add NULL-check to BeginPlay:
void ATestActor::BeginPlay() { Super::BeginPlay(); checkf(IsValid(CapsuleComponent), TEXT("Capsule component is NULL")); }
-
Re-compile project and launch the engine.
-
Create Blueprint parented to
ATestActor
(e.g.,BP_TestActor
) and open it in Full Blueprint Editor.
Click onCapsuleComponent
in Components tab and verify that component properties are visible in Details tab.
Save and close the engine. -
Change the name of the component default subobject created in C++ constructor, e.g.:
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("NewComponentName"));
-
Launch engine and open BP_TestActor in Full Blueprint Editor.
Click onCapsuleComponent
in Components tab and verify that component NO properties are visible in Details tab (Details tab is empty). -
Drag and drop BP_TestActor onto a default map and click Play. Editor crashes on our check in
ATestActor::BeginPlay()
becauseCapsuleComponent == nullptr
.
Demo
I have created a simple demo project using the above scenario and published it on github: GitHub - vogoltsov/UE4_ActorComponent_PendingKill_Demo
Bug details
Notes:
- During loading, if project files remain unchanged, UE4 allocates the same object identifiers each time the project is loaded. So, I am using object identifiers from my demo project onwards.
- I am using Unreal Engine 4.26.1 recompiled from source. As I have added several pragmas to disable optimization on certain functions, so actual line numbers in code may differ a little.
I’ve traced the modifications of ATestActor
's member property CapsuleComponent
:
-
Default_TestActor[InternalIndex=19506]
(CDO forATestActor
) is created andCapsuleComponent
is set to a non-NULL
objectUCapsuleComponent[InternalIndex=19507]
. -
BP_TestActor.uasset
starts loading. -
Default_BP_TestActor[InternalIndex=23596]
(CDO forBP_TestActor
) is created andCapsuleComponent
is set to a non-NULL objectUCapsuleComponent[InternalIndex=23595]
. -
Default_BP_TestActor.CapsuleComponent
is set to Default_TestActor.CapsuleComponent[InternalIndex=19507]. -
Default_BP_TestActor.CapsuleComponent
is reverted toUCapsuleComponent[InternalIndex=23595]
(value set in 3). -
Default_BP_TestActor.CapsuleComponent
is set to a newly createdUCapsuleComponent[InternalIndex=23594]
.- If we inspect
GUObjectArray.ObjObjects.Objects[0][23594].Flags
, we can see that this object is already marked asRF_PendingKill
.
- If we inspect
-
During garbage collection,
Default_BP_TestActor.CapsuleComponent
is set toNULL
because the component instance is marked asRF_PendingKill
.
After that, I started investigating how UCapsuleComponent[InternalIndex=23594]
is created and why it is marked as RF_PendingKill
. The problem lies in how the object property is being de-serialized from the archive. This process can be summarized to:
- Save current object property value to a local variable (this is essentially a default subobject created in C++ constructor). Link to source.
- Read new property value from archive slot. Link to source.
- Obtain template to be used as new property value archetype. Link to source.
- Archetype object is searched in outer class CDO (which is
Default_TestActor
). Link to source. - To locate an archetype, perform thread-safe lookup in object outer hash map using
StaticFindObjectFastInternalThreadSafe()
. Link to source. - Lookup is performed by name supplied in archive (
Export.ObjectName
variable) which appears to be“OldComponentName”
. But the object created in constructor has been registered in outer map using name supplied inATestActor
constructor, which is“NewComponentName”
. Thus, outer hash map lookup fails, andNULL
is returned. - Because the object could not be found in outer hash map, class default object is returned as the archetype.
Link to source. - Construct new object using template.
Link to source. - Now everything goes well, until execution hits
AActorComponent::PostInitProperties()
. In normal circumstances, newly created component should be added as owned component to outer object (which isDefault_BP_TestActor
).
But becauseMyArchetype != GetClass()->ClassDefaultObject
check fails,MarkPendingKill()
is called.
Link to source. - Replace property value with new pointer. Link to source.
Proposed solution
I understand that changing component names is generally to be avoided, but it can sometimes be done “unintentionally” – e.g., to fix spelling errors during refactoring. The problem is not that component renaming may not be supported, but that Unreal Engine just silently sets component property to NULL
without raising an error or at least issuing a warning.
Personally, I would prefer a fail-fast solution when performing an invalid operation (i.e., renaming a component) leads to an error, so this can be immediately addressed. We would also need to add a new core redirect type to allow renaming components without breaking things. (This whole situation is somewhat similar to renaming C++ class which have existing child Blueprints.)
One way to achieve this would be to modify UActorComponent::PostInitProperties()
and move MyArchetype != GetClass()->ClassDefaultObject
inside ensureAlwaysMsgs()
or checkf()
, e.g.:
ensureAlwaysMsgf(MyArchetype != GetClass()->ClassDefaultObject, TEXT("Failed to get archetype for natively created component. Owner class: %s. Owner name: %s. Component class: %s. Component name: %s"), *OwnerPrivate->GetClass()->GetPathName(), *OwnerPrivate->GetFName().ToString(), *GetClass()->GetPathName(), *GetFName().ToString());
This would result at least in the following error being written to Output Log:
LogOutputDevice: Error: Ensure condition failed: MyArchetype != GetClass()->ClassDefaultObject [File:C:\Development\UnrealEngine\Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp] [Line: 293]
Failed to get archetype for natively created component. Owner class: /Game/BP_TestActor.BP_TestActor_C. Owner name: Default__BP_TestActor_C. Component class: /Script/Engine.CapsuleComponent. Component name: OldComponentName
I have attached a sample patch. I have tested on several of my pet projects as well as ActionRPG sample project, and this seems to be working fine. But more tests are needed for sure.
diff --git a/Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp b/Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp
index 87eee3ccab3..342449f3b07 100644
--- a/Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp
+++ b/Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp
@@ -289,7 +289,8 @@ void UActorComponent::PostInitProperties()
if (!FPlatformProperties::RequiresCookedData() && CreationMethod == EComponentCreationMethod::Native && HasAllFlags(RF_NeedLoad|RF_DefaultSubObject))
{
UObject* MyArchetype = GetArchetype();
- if (!MyArchetype->IsPendingKill() && MyArchetype != GetClass()->ClassDefaultObject)
+ ensureAlwaysMsgf(MyArchetype != GetClass()->ClassDefaultObject, TEXT("Failed to get archetype for natively created component. Owner class: %s. Owner name: %s. Component class: %s. Component name: %s"), *OwnerPrivate->GetClass()->GetPathName(), *OwnerPrivate->GetFName().ToString(), *GetClass()->GetPathName(), *GetFName().ToString());
+ if (!MyArchetype->IsPendingKill())
{
OwnerPrivate->AddOwnedComponent(this);
}