Renaming a component in a base C++ classes causes inherited component in Blueprint to have empty details and is unusable

Hey there, in our project, we are using a character architecture that looks something like this, pretty basic:

[Image Removed]

A split between C++ base classes that define a basic component structure and Blueprint classes that specify actor-specific settings and behaviours.

As seen in the diagram, our base character defines a variable and a getter for a general MoverComponent, while the derived classes can implement their own specialisation of this component. This works somewhat ok, the only problem being the inability to specify a default value at the MyBaseCharacter level (that’s why it’s null and the derived class HAS to specify the implementation).

---

The issue I’m reporting is a “corruption” of the class tree structure when the base class renames its component. I have made a mistake of renaming “AnimalMovementComponent” -> “AnimalMoverComponent” in the class `AMyAnimalCharacter`, causing the component to get lost and not function properly. The side effect of this issue is an empty detail panel when selecting such a component in the derived Blueprint class (BP_GroundAnimal or lower).

[Image Removed]

I don’t think it’s the issue of the Mover component itself, but it’s the one that’s crashing in my case - the SharedSettings don’t get created, and the Walking mode calls ensure. Interestingly enough, the Walking mode still gets created even though the component itself is in a corrupted state, but the SharedSettings don’t. Maybe it has something to do with SharedSettings-refreshing being tied to PostLoad/PropertyChange. But that’s a different issue, I think.

I have noticed that this bug is pretty old and already being tracked https://issues.unrealengine.com/issue/UE\-115124 with forum posts spanning 2015.

Since the bug is already being tracked - I don’t want to hurry the fix (or yes, but you know), but rather how to fix a single occurrence of such a “corrupted” component? The only fix I managed to find is reparenting the BP to an Actor and back to our C++ class, but that obviously clears all the changes to the BP itself. I wouldn’t mind resetting changes to the MovementComponent (the corrupted one, it’s ok to throw it away), but resetting all the other components can get tedious with already created content in the game.

I was thinking about a simple script/editor utility, but I’m not sure where to locate this corrupted component.

Thanks a lot for your help!

[Attachment Removed]

Hey Patrik, changing a default subobject’s name parameter passed in the CreateDefaultSubobject call is and always will be problematic indeed!

Since you mention it’s okay to reset the changes on the corrupted component, I recommend these temporary steps:

  • Changing the variable name of the UPROPERTY, so MoverComponent -> MoverComponentTEMP
  • Also change the subobject name, so CreateDefaultSubobject<UMoverComponent>(TEXT(“AnimalMoverComponentTEMP”);
  • Compile and launch the editor, open the corrupted blueprints.
    • They will have a clean, and working MoverComponent. This isn’t really the point.
    • The point is: ‘MoverComponent’ as property doesn’t exist anymore. And the “old” MoverComponent named “AnimalMovementComponent” isn’t referenced, so will get trashed / cleaned up.
  • Save the asset.
    • The asset is now saved without any old subobjects.
  • Close the editor.
  • You can now undo all the code changes, both the variable name and subobject name.
  • Launch the editor again.
  • Open and resave the blueprints.
  • They again have reset components, that will continue to function. AnimalMoverComponentTEMP will be trashed.

It sounds like a lot of steps, but the important bit is: once something is loaded and unreferenced, it doesn’t get saved again. So that’s the workaround to resetting both “corrupted” pointers (UPROPERTY MovementComponent) and unwanted objects with the old name.

Bonus topic

Looking at your UML diagram, I see that you have an actor hierarchy where everyone will have a MoverComponent, but you want the class to be different. This can be achieved in a simpler way:

  • Let AMyBaseCharacter just create the component with a generic class CreateDefaultSubobject<UMoverComponent>(TEXT(“SomeName”)).
  • Use the actor constructor override that takes a FObjectInitializer, i.e. like: APawn::APawn(const FObjectInitializer& ObjectInitializer)
  • In the subclasses, you can change the component class with a derived class. See ADetourCrowdAIController for an example that changes its AAIController class’s path following component class
AAIController::AAIController(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	PathFollowingComponent = CreateOptionalDefaultSubobject<UPathFollowingComponent>(TEXT("PathFollowingComponent"));
}
ADetourCrowdAIController::ADetourCrowdAIController(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UCrowdFollowingComponent>(TEXT("PathFollowingComponent")))
{
 
}

You must provide the same name in the TEXT() macro. In blueprint, component classes can be overridden too by selecting the component and changing the class in the details panel.

Would this approach solve your needs better? This way, the base class can still have default values on the component that the derived classes inherit.

[Attachment Removed]

Thanks a lot for a detailed answer, I will definitely give it a try.

ad. bonus topic: that’s exactly what I was trying to do, but didn’t know about CreateOptionalDefaultSubobject! Thanks for the information

[Attachment Removed]

Hello! Apologies for the delay in response. I was not in office last week.

I believe what’s happening is this:

  • Map placed characters have been saved with those MoverComponents with the old name “HumanoidMoverComponent” that was previously created in AMyHumanoidCharacter.
  • Those components had MovementMode subobjects, which are the configurable entries in that mover component’s MovementModes TMap.
  • Those subobjects are saved in the asset. Upon loading, they still exist. Upon cooking, the assets try to be cooked.

The goal would be to resave those assets without those dangling subobjects. I would try two things:

  • Maybe resaving those actors in the level would already resolve the problem, so the error doesn’t occur anymore on cook. Have you tried that? We have a ResavePackages commandlet that may help to resave all actors on a map.
  • Otherwise, try some approach like this that finds and cleans up those subobjects. I’ll share a snippet below.

If resaving the actors wasn’t enough, you can try to find the unwanted old objects using ForEachObjectWithOuter to trash specific ones.That’s a trick to find something’s owned UObjects without having a current reference to them. You can use to do some logic after loading like this:

void AMyCharacterClass::PostLoad()
{
	Super::PostLoad();
 
	// Only process instances, not default objects
	if (!HasAnyFlags(EObjectFlags::RF_ClassDefaultObject))
	{
		// Try find the orphaned old movement component, which exists if it has been saved with it before.
		UObject* OldMovementComp = FindObjectWithOuter(this, UHumanoidMovementComponent::StaticClass() /*Previous class*/, FName("HumanoidMoverComponent") /*Previous name in CreateDefaultSubobject*/);
 
		// Callback to mark dangling objects as garbage
		bool bSomeObjectsMarkedAsGarbage = false;
		TFunction<void(UObject*)> Callback = [&bSomeObjectsMarkedAsGarbage](UObject* Inner)
			{
				UE_LOG(LogTemp, Warning, TEXT("Object found: %s"), *Inner->GetPathName());
 
				// Mark as garbage, so it will be destroyed on next GC pass. More importantly, it won't be saved anymore.
				Inner->MarkAsGarbage();
				bSomeObjectsMarkedAsGarbage = true;
			};
 
		// Now perform the callback on anything that is Outered to our old movement component
		ForEachObjectWithOuter(OldMovementComp, Callback, /*bIncludeNested=*/true);
 
		if (bSomeObjectsMarkedAsGarbage)
		{
			UE_LOG(LogTemp, Warning, TEXT("Resave this actor: %s"), *GetPathName());
		}
	}
}

Instead of doing things in an actor instance’s PostLoad(), you can also consider writing an editor utility widget that goes over all actors in the currently loaded level. MarkPackageDirty() to prompt resave is also helpful if you manually want to resave only affected actors. MarkPackageDirty() is disabled in PostLoad(), but can work if you go over the actors in a function called from a utility widget.

You can also consider writing a commandlet that goes over all actors in the level, similar to ResavePackages. We regularly use ResavePackages in Fortnite, plus custom commandlets, to update map actors after making changes to their class.

[Attachment Removed]

It definitely helped with blueprints finally compiling and being able to see details! But it turned out it created another issue :sweat_smile: The instances of characters that were already placed in the level suddenly log warnings during cook:

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘CommonLegacyMovementSettings_0’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultFallingMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultFlyingMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultNullMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘StanceSettings_0’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultWalkingMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘CommonLegacyMovementSettings_0’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultFallingMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultFlyingMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultNullMode’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘StanceSettings_0’: HumanoidMovementComponent …

LoadErrors: Warning: CreateExport: Failed to load Outer for resource ‘DefaultWalkingMode’: HumanoidMovementComponent …

Which sounds like instances of Blueprints having cooked child objects in the Level instance that don’t match the new object structure and are left without an Outer parent?

On a small sized level, I’ve tried just deleting and placing the actor back again, which helped. But it’s not possible to do on a level with 200 NPCs. Do you know if there’s a way to fix it globally?

[Attachment Removed]

Thanks for the help. I wasn’t able to fix it with your code (probably because the class doesn’t exist anymore?), but hooking into PreSave and searching directly by Outer’s name helped. It appears that the orphaned subobjects never had GetOuter as null at the time of loading/saving, but some old object called “TRASH_HumanoidMoverComponent”, which was probably already marked by GC and ended up nullptr after the fact.

This seemed to work when saving the level-instanced actor with incorrectly serialised subobjects:

void AWhCharacter::PreSave(FObjectPreSaveContext SaveContext)
{
	Super::PreSave(SaveContext);
 
#if WITH_EDITOR
	if (HasAnyFlags(RF_ClassDefaultObject))
        {
		return;
        }
 
	TArray<UObject*> SubObjects;
	GetObjectsWithOuter(this, SubObjects, /*bIncludeNestedObjects=*/true);
 
	for (UObject* SubObj : SubObjects)
	{
		if (!SubObj)
		{
			continue;
		}
 
		if (SubObj->GetOuter() == nullptr
			|| (SubObj->GetOuter()
				&& (SubObj->GetOuter()->GetName().StartsWith(TEXT("TRASH_HumanoidMoverComponent"))
					|| SubObj->GetOuter()->GetName().StartsWith(TEXT("TRASH_MoverComponent")))))
		{
			UE_LOG(LogGameCharacterModule, Verbose, TEXT("Marking orphaned object: %s (%s)"), *SubObj->GetPathName(), *SubObj->GetClass()->GetName());
			SubObj->MarkAsGarbage();
		}
	}
#endif
}

I still have to manually edit and resave all the actors in the level, but at least it reliably removed those subobjects.

[Attachment Removed]

Hello, I’m glad you found a way to perform the cleanup on PreSave. That looks good to me!

[Attachment Removed]