Data exposition and blueprint override issue

Hello,

I’m facing some issues with data serialization that I don’t really understand. I exposed in an actor component a USTRUCT (FPerceptionConfig) that contains properties like a FName and an array of UObject (the UCLASS of the UObject is defined with EditInlineNew and DefaultToInstanced as they are polymorphic UObject configs) as you can see in attached file <DataConfig.PNG>.

It works and behave perfectly at runtime but the issue comes when I make a child blueprint of the one holding this data.

The data in the child blueprint appears correctly but as soon as I update the parent blueprint, whatever the field, even the FName (DetectionBoneName), the UObject configs on the child blueprint are set to None as you can see in attached file <ChildBlueprintDataConfigAfterParentUpdate.PNG> after updating the bone name to head2.

To “fix it”, I have to click “Reset property to its default value” on those fields but I lost the interest of having a parent blueprint that can hold the base data and children that can have overrides on this data.

Here the code if it can help :

UCLASS(Abstract, EditInlineNew, DefaultToInstanced)
class UBaseVisualDetectionConfig : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, Category="")
    TArray<FVisualDetectionShapeConfig> VisualDetectionShapeConfigs;
};

USTRUCT(BlueprintType)
struct FPerceptionConfig
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    FName DetectionBoneName;

    UPROPERTY(EditAnywhere)
    TArray<TObjectPtr<UBaseVisualDetectionConfig>> VisualDetectionConfigs;
};

Thanks !

[Attachment Removed]

Steps to Reproduce

  • Use the following code snippet and expose a FPerceptionConfig on an UActorComponent
  • Make a child blueprint of the one holding this data
  • Update the data in the parent blueprint
  • Notice that the child blueprint VisualDetectionConfigs data is now set to None

[Attachment Removed]

Hello! Unfortunately, all containers within UE - TArray, TMap, TSet, have a lot of gotchas surrounding inheritance.

Background info

When it comes to arrays of structs and plain-old-data:

  • Whenever you change one element of an array in the derived blueprint, the whole array is considered not inheriting anymore.
  • However, when making changes in the editor, the array element will still propagate as if it is inheriting.
  • This is deceptive, because if you reload the asset without saving, the value doesn’t get inherited. The child’s array was considered changed, so the child just loads the modified copy of the whole array.

That’s the simplest case and it already has the above known problem.

Your use case

An array of instanced objects, and (your case) a struct of an array of instanced objects are both what we consider more complex hierarchies. I can confirm that with your repro steps, the child’s objects often get destroyed and end up as None. I’ll continue investigating if that’s a known bug or not.

However, I recommend either simplifying the hierarchy, or implementing custom inheritance behavior.

Simplifying approach

Without the wrapping struct, a direct TArray<TObjectPtr<UBaseVisualDetectionConfig>> on the native actor component doesn’t show the same problem of the objects being nulled. Can you experiment with that setup and see if it works out well for you? You’ll have a polymorphic array. It should be trivial to put other struct members besides there.

Custom inheritance logic

In the Gameplay Ability System plugin, UGameplayEffect blueprints have custom inheritance logic for the polymorphic of UGameplayEffectComponents. It does some custom logic in UGameplayEffect::PostCDOCompiled(), especially in UGameplayEffect::PostCDOCompiledFixupSubobjects(), to compensate for some oddness around arrays of instanced objects inheritance. Note that this is also an array of instanced objects, without wrapping struct.

IMO, you’re best off mimicking the UGameplayEffect approach. While it’s complex, it works and is also a setup that we would prioritize maintaining if bugs come up there. I’ll look into your bug of what goes wrong when there is a wrapping struct, but it will be lower priority! I’ll file it though and share a public issue tracker link with you later.

[Attachment Removed]

Hello !

First of all, thank you very much for your answer, your explanations make things clearer.

If I understood your recommandation correctly, I tried putting the TArray<TObjectPtr<UBaseVisualDetectionConfig>> directly in my component but it seems to cause the exact same issue. After modifying the parent data, the child blueprints end up with broken datas.

Moreover, we actually have a Component/SubComponents structure based on our previous projects (not in UE) rather than UE’s multiple components approach, so we can’t really put the data directly at the root of the actor component. I’m not sure if it’s the right approach, but we actually have one main actor component (plus others UE ones of course), and this actor component contains logics (called “SubComponents” in our case) created directly by the component in code, depending on the component data’s setup. These SubComponents are not UObjects but simple classes. We’ve kept this approach to maintain full control over how the SubComponents are ticked, which one are instantiated and in what order.

Finally, based on your explanations, I noticed that our UBaseVisualDetectionConfig class also contains another TArray of UObject, so maybe that’s why putting the data at the root of the actor component doesn’t work either.

On the other hand, I took a quick look on the UGameplayEffect class to see how the issue is handled there, but I don’t think we can replicate this approach. We’ll likely run into this kind of issue in various situations, and it would quickly become too difficult to maintain. Anyway, I think we’ll keep it simple by using UDataAsset and avoiding inheritance since we shouldn’t have too many perception variations to maintain.

Anyway, once again, thanks for your response, I’d be curious to hear your point of view on our Component/SubComponents code structure, we’re still early in the project so we can adjust if needed and also any insight you might have on this data issue.

[Attachment Removed]

Hey again!

“If I understood your recommandation correctly, I tried putting the TArray<TObjectPtr<UBaseVisualDetectionConfig>> directly in my component but it seems to cause the exact same issue. After modifying the parent data, the child blueprints end up with broken datas.”

You understood my recommendation correctly. I didn’t observe this problem when I was experimenting with it myself, but my setup might differ in important ways. Can you provide some more info about how you set things up? Specifically:

  • The component class, I assume it’s a C++ class since you mention TArray. Is that right?
  • Is the component class added to a C++ actor class (DSO), or is it added via blueprint (SCS)?
    • DSO = Default subobject, i.e. created in an actor’s native constructor via CreateDefaultSubobject<UMyActorComponent>()
    • SCS = Simple construction script, the system that manages components added in blueprint.
  • UBaseVisualDetectionConfig is abstract. Are the subclasses that you’re putting into the component’s array native classes or blueprints?

Those factors are important to reproduce, because components added via blueprint are recreated often in editor (like during actor BP recompile) and objects defined by blueprint classes are recreated all the time (like during BP_MyVisualConfig recompile). I was testing with a native component class, natively added to an actor class AMyActor (so a DSO of the actor), and native UBaseVisualDetectionConfig subclass. The only way blueprint was involved was the data configuration on BP_MyActor and BP_MyActor_Child.

“I’m not sure if it’s the right approach, but we actually have one main actor component (plus others UE ones of course), and this actor component contains logics (called “SubComponents” in our case) created directly by the component in code, depending on the component data’s setup. These SubComponents are not UObjects but simple classes.”

Since your SubComponents are simple classes, created at runtime from the component’s data setup, I’m not worried about that part of the architecture. The main challenge indeed is to find a data setup that gives the flexibility and robust inheritance that your team needs.

“We’ve kept this approach to maintain full control over how the SubComponents are ticked, which one are instantiated and in what order.”

Still, if you would migrate the data + logic of your custom SubComponent classes to separate ActorComponents, that would the workflow that UE’s actor-components is designed and most tested for:

  • Any derived BP can add additional components, but not remove them
  • Any derived BP can change an inherited component’s class to a specialized class (select component in the BP editor -> ComponentClass).
  • Any derived BP can change an inherited component’s property, without it affecting how other properties and other components inherit.
    • This avoids the weirdness around array inheritance where in the editor sessions values propagate (per element inheritance), but if children are reloaded without saving, the value may be loaded differently (whole array loading in its old state)
  • If components need to set things up in a certain order, then you may get away with using the component’s register/initialize/activate/beginplay functions in specific ways. But you can also modify engine code to change the order that components are iterated for initialization. See [this other [Content removed] case where a studio needed that. I gave some tips there, though they also found their own solution.
  • ActorComponents do come with overhead, which is where your approach of lightweight non-UObject classes would save a lot of memory usage, and avoid more garbage collection workload.

In terms of data setup, I would still like to know more about what parts of your setup are C++ vs BP, but I also wanted to make another suggestion: try experimenting with TMaps<FName, TObjectPtr<UBaseVisualDetectionConfig>. Arrays and inheritance get messy once you delete or insert elements, but TMaps are more stable since inheritance happens per key (FName in this case). In other words: it’s clear that whatever object is associated with key “Foo” in the derived BP, inherits from the object with key “Foo” in the parent BP, even when you add/remove entries from the parent. Still, children should not remove entries.

[Attachment Removed]

Hello,

Sure, I’ll try to provide as much info as I can.

I tried to put the data in my UAIComponent, that inherits from UCharacterComponent, itself inheriting from UBaseComponent, inheriting from another UBaseProjectComponent finally inheriting from UE’s UActorComponent class.

UCLASS(Category = "AI", ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UAIComponent : public UCharacterComponent
{
    GENERATED_BODY()
 
public:
    UAIComponent() {}
 
public:
    UPROPERTY(EditAnywhere)
    TArray<TObjectPtr<UBaseVisualDetectionConfig>> VisualDetectionConfigs;
 
    UPROPERTY(EditAnywhere, Category="AI")
    FAIConfig AIConfig;
};

This UAIComponent is added to a C++ actor class (DSO) this way :

AAIPawn::AAIPawn()
{
    AIComponent = CreateDefaultSubobject<UAIComponent>(TEXT("AIComponent"));
}

AAIPawn inherits from ACharacterPawn, then a ABaseProjectPawn and finally from UE’s APawn class.

UBaseVisualDetectionConfig is, as you said, abstract and all subclasses are only C++ classes like this :

UCLASS()
class UPlayerVisualDetectionConfig : public UBaseVisualDetectionConfig
{
	GENERATED_BODY()
};

So, with all that being said, it looks like that I have the same setup as yours (no BP setup except for the Actor setup). I created an Actor derived from my AAIPawn (called BP_BaseAIPawn), then another BP inheriting from BP_BaseAIPawn. So maybe I’m doing something wrong elsewhere.

About the TMap, it seems that I have the exact same result. I tried to add default data in my BP_BaseAIPawn, but it appears directly as None in the child blueprints as you can see :

  • Test is the array, Test2 is the TMap

[Image Removed]

  • Result in child blueprints :

[Image Removed]

  • If I reset the properties they appear correctly, then if I add another element in the BP_BaseAIPawn’s TMap, it broke again all the data on children BP :

[Image Removed]

Regarding the Subcomponents topic, thank you again for having taking the time to share your perspective and experience. It’s very valuable for us and we’ll take it into account to make the better choice for our needs.

[Attachment Removed]

Hey again! My apologies for taking a while to get back to you.

I’ve reproduced the problem in your screenshots. Can you try adding the Instanced UPROPERTY specificier to VisualDetectionConfigs?

UCLASS()
class THIRDPERSON573_API UMyAIComponent : public UActorComponent
{
	GENERATED_BODY()
 
public:
	UMyAIComponent();
 
	UPROPERTY(EditAnywhere, Instanced)
	TArray<TObjectPtr<UBaseVisualDetectionConfig>> VisualDetectionConfigs;
};

Both UCLASS(DefaultToInstanced) on the class and UPROPERTY(Instanced) on the object ptr property are needed. One affects how the object instances are treated, the other is how the reference is treated.

[Attachment Removed]

Glad to be of help!

[Attachment Removed]

No problem, I also switched on another topic so I took time to actually test your solution.

But yes, thank you very much, it’s now working as expected !

I was well missing the Instanced keyword on my UPROPERTY, thanks for the explanation !

[Attachment Removed]