Announcement

Collapse
No announcement yet.

Blueprint recompile invalidates references in native component

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    Blueprint recompile invalidates references in native component

    Big wall of text incoming, thanks in advance to anyone bold enough to venture forth.

    I have a strange issue with UObject references being nulled out when the owning blueprint class is compiled. The setup is as follows:

    A blueprint class, MyCharacter, inherits ACharacter. It adds a number of (custom) native components, one of them being a bit more complex than the others (which are very straight-forward). The complex one has the following basic layout data-wise:

    Code:
    UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
    class UNCHAINED_API ULeetNpcProgramComponent : public UActorComponent
    {
      GENERATED_BODY()
    public:
    [...]
      UPROPERTY(Instanced, EditAnywhere, Category = "LeetNpc")
      TArray<UNpcProgramContainer*> Programs;
    };
    So, an actor component with an array of UObject-derived containers:

    Code:
    UCLASS()
    class UNCHAINED_API UNpcProgramContainer : public UObject
    {
      GENERATED_BODY()
    public:
      UNpcProgramContainer();
    
      [Other POD properties here]
    
      UPROPERTY(EditAnywhere, Category = "LeetNpc")
      UNpcProgram* Program;
    
      UPROPERTY(Instanced, EditAnywhere, Category = "LeetNpc")
      ULeetUtilityAction* UtilityAction;
    
      UPROPERTY(Instanced, EditAnywhere, Category = "LeetNpc")
      ULeetBlackboard* Blackboard;
    
      UPROPERTY(Instanced, EditAnywhere, Category = "LeetNpc")
      TArray<ULeetEventDefinition*> ExecutionEvents;
    };
    Code:
    UNpcProgramContainer::UNpcProgramContainer()
    {
      Program = CreateDefaultSubobject&amp;lt;UNpcProgram&amp;gt;(FName(TEXT("NpcProgram"))); 
      UtilityAction = CreateDefaultSubobject&amp;lt;ULeetUtilityAction&amp;gt;(FName(TEXT("UtilityAction")));
      Blackboard = CreateDefaultSubobject&amp;lt;ULeetBlackboard&amp;gt;(FName(TEXT("Blackboard")));
    }
    It's these references (Program, UtilityAction and Blackboard; ExecutionEvents is empty) that are nulled out when the blueprint is recompiled. The POD properties all remain intact. I've tried with and without Instanced - I don't really need that property except for ease of validating data through the editor detail view. (The normal use case is to view and manipulate all this data through a custom editor.) All objects are created with their logical parent as Outer, i.e. A creates and references B, B is created with A as its Outer. My thinking is that all these objects will be stored with their respective parents, ultimately leading to an in-world actor and thus persistent storage in the map. Saving and loading the entire object hierarchy to/from disk works fine.

    I've played around with using other objects as Outer, e.g. having all objects live with the component's Outer. This leads to hard crashes on blueprint recompile, typically when the object is reconstructed. It can also lead to hard crashes in serialization during a transactional snapshot (Modify() called on e.g. Container object).

    In general, I get the feeling that I'm doing something that causes UE4 to be unable to properly reflect the object hierarchy under certain conditions, such as blueprint compilation. I've also had instances where seemingly unrelated data can't be edited without a crash, such as a blueprint variable or a property on an unrelated component. I've only worked with UE4 properly a couple of months, so it may well be that I have something very basic wrong.

    Finally, it *seems* that these problems go away, or are at least harder to reproduce, if the blueprint class instead derives a custom native class that creates the offending component (ULeetNpcProgramComponent).

    For completeness sake, the referenced objects are laid out as follows: (It should be noted I've had these issues even without the references to UtilityAction and Blackboard)

    Program:
    Code:
    UCLASS()
    class UNCHAINED_API UNpcProgram : public UObject
    {
      GENERATED_BODY()
    public:
      UNpcProgram();
    
      UPROPERTY(EditAnywhere, Category = "LeetNpc")
      TArray<FNpcProgramEntry> Entries;
    };
    The Entries array is populated from a custom editor.

    UtilityAction:
    Code:
    UCLASS()
    class UNCHAINED_API ULeetUtilityAction : public UObject
    {
      GENERATED_BODY()
    public:
      UPROPERTY(EditAnywhere, Category = "LeetUtilityAi")
      TArray<FConsiderationInstanceData> Considerations;
    };
    Where FConsiderationInstanceData is:
    Code:
    USTRUCT()
    struct FConsiderationInstanceData
    {
      GENERATED_BODY()
    public:
      UPROPERTY(EditAnywhere, Category="LeetUtilityAi")
      TSubclassOf<ULeetUtilityConsideration> ConsiderationClass;
    
      UPROPERTY(EditAnywhere, Category="LeetUtilityAi")
      ULeetUtilityConsideration* Consideration = nullptr;
    
      UPROPERTY(EditAnywhere, Category = "LeetUtilityAi")
      ULeetUtilityConsiderationCurve* Curve = nullptr;
    };
    Where ULeetUtilityConsideration is:
    Code:
    UCLASS(Blueprintable, EditInlineNew, meta=(DisplayName="Base Consideration"))
    class UNCHAINED_API ULeetUtilityConsideration : public UObject
    {
    GENERATED_BODY()
    public:
      ULeetUtilityConsideration();
    
      UFUNCTION(BlueprintNativeEvent, Category = "LeetUtilityAi")
      float Evaluate(ULeetBlackboard* Ctx);
    
      UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "LeetUtilityAi")
      float Bonus = 0.0f;
    
      UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "LeetUtilityAi")
      float Multiplier = 1.0f;
    
      UPROPERTY(Instanced, BlueprintReadWrite, EditAnywhere, Category = "LeetUtilityAi")
      ULeetUtilityConsiderationCurve* DefaultCurve = nullptr;
    
    protected:
      virtual float Evaluate_Implementation(ULeetBlackboard*);
    };
    And LeetUtilityConsiderationCurve is:
    Code:
    UCLASS()
    class UNCHAINED_API ULeetUtilityConsiderationCurve : public UObject
    {
      GENERATED_BODY()
    public:
      UPROPERTY(EditAnywhere, Category = "Utility Curve")
      FRuntimeFloatCurve Curve;
    };
    The considerations are created from a custom editor.

    Blackboard:
    Code:
    UCLASS(BlueprintType)
    class UNCHAINED_API ULeetBlackboard : public UObject
    {
      GENERATED_BODY()
    public:
      [Functions here]
    
    private:
      UPROPERTY()
      TMap<FName, int32> m_DataIndex;
    
      UPROPERTY()
      TArray<uint8> m_Data;
    
      UPROPERTY()
      TArray<UObject*> m_Objects;
    };
    The blackboard is only used at run-time so all blackboard containers are empty at the point the issue occurs.

    Thanks for taking the time to read through this!

    #2
    Do not serialize the components pointers.
    Mark them as UPROPERTY (Transient) and fill them in from your initialization point.
    | Savior | USQLite | FSM | Object Pool | Sound Occlusion | Property Transfer | Magic Nodes | MORE |

    Comment


      #3
      Hi, and thanks for the reply!

      It seems I'd lose the instance data by not serializing the only reference to it? How would I go about filling in the subobjects exactly - from where would I get the instance data?

      Comment


        #4
        You are already creating them as default sub objects.
        There's no need to serialize those pointers, neither to be instanced then.
        | Savior | USQLite | FSM | Object Pool | Sound Occlusion | Property Transfer | Magic Nodes | MORE |

        Comment


          #5
          I'm probably a bit confused as to the basics here. Let's say I have these types:
          Code:
          UCLASS()
          class UTestMember : public UObject
          {
              GENERATED_BODY()
          public:
              UPROPERTY(EditAnywhere)
              int32 Value = 0;
          };
          
          UCLASS()
          class UTestContainer : public UObject
          {
              GENERATED_BODY()
          public:
              UTestContainer()
              {
                  MemberA = CreateDefaultSubobject<UTestMember>(FName(TEXT("MA")));
                  MemberB = CreateDefaultSubobject<UTestMember>(FName(TEXT("MB")));
              }
          
              UPROPERTY(Transient, VisibleAnywhere)
              UTestMember* MemberA;
          
              UPROPERTY(Transient, VisibleAnywhere)
              UTestMember* MemberB;
          };
          In some component, I create an instance of UTestContainer (at edit-time):
          Code:
              TestContainer = NewObject<UTestContainer>(this, UTestContainer::StaticClass(), FName(TEXT("TisAFineDay")));
          where TestContainer is declared as:
          Code:
              UPROPERTY(EditAnywhere, Category="Test")
              UTestContainer* TestContainer = nullptr;
          I edit the values in MemberA and MemberB in a few different components and note that they each hold unique values for UTestMember::Value. On save, however, they are not persisted since transient has been specified. On load they take their value from the CDO, which I guess is expected. This is the instance data I'm talking about. If I remove the transient specifier, data is persisted and all seems well. In this case I might have been lucky as there was no recycling happening in NewObject, bit hard to tell. But I suppose I could guarantee proper behavior by adding the DefaultToInstanced class specifier to UTestMember? (Just like UActorComponent.)

          There is likely something wrong with my reasoning, but what?

          Comment


            #6
            In that case your instanced members, without Transient, from original post probably reset because when you compile your class constructor will run again at some point and you assign default objects there.

            Maybe if you don't specify anything in constructor and set DefaultToInstanced then your glitch will stop happening on compile.
            | Savior | USQLite | FSM | Object Pool | Sound Occlusion | Property Transfer | Magic Nodes | MORE |

            Comment


              #7
              The scary part is that the references were nulled out. It would have made more sense if the subobjects reverted to CDO, but it seems they were dropped entirely somehow.

              At any rate, lesson learned and everything seems to work fine now. Thanks for taking the time to help out, much appreciated!

              Comment


                #8
                References in a CDO should be null (or invalid) shouldn't they?

                Comment


                  #9
                  Even references to default subobjects? What mechanism is responsible for life-timing and propagation to instances if it's not kept in the CDO?

                  I had a quick look at what I presume is the CDO of UTestContainer above by calling
                  Code:
                  UTestContainer* CDO = UTestContainer::StaticClass()->GetDefaultObject<UTestContainer>();
                  And the references appear valid:
                  Code:
                  -        CDO    0x0000000031475df0 (Name="Default__TestContainer")    UTestContainer *
                  +        UObject    (Name="Default__TestContainer")    UObject
                  +        MemberA    0x000000003147a900 (Name="MA")    UTestMember *
                  +        MemberB    0x000000003147a8c0 (Name="MB")    UTestMember *

                  Comment


                    #10
                    Doing nesting this complex in a component you add from blueprint is asking for trouble. Blueprint components get completely recreated during construction script execution, with a convoluted and not very reliable process that tries to carry across non-default property values. Definitely better to add the component natively - that way it will only be reregistered during actor construction script, not reinstanced.

                    Also, you say you don't really need Instanced, but I think you probably do, and you likely want it for the Program property too. UObject properties should only really be non-instanced if they're classes or asset references. It's very rare you'd want to assign with CreateDefaultSubobject and not mark it Instanced. Every instance of your component across all actors would end up referencing a single shared object.

                    Anyway, bottom line, add natively, and keep nesting to a minimum. The object reinstancing framework is riddled with bugs, they tend to show up as you do more complex and less standard things.

                    Comment


                      #11
                      Thanks so much for the information! Starting out with a new engine, you tend to think any strange behavior is caused not by the engine but by your own clumsy fingers. It's super-useful to know that reinstancing isn't stable. I am currently adding the components natively and did notice the problem went away, but was worried that there was still something wrong with my setup.

                      And yes, you are entirely correct about Instanced. I was pretty confused about the concept until I found out about DefaultToInstanced. Looking at simple actor-component setups it looks like the default is to instance subobjects per owner instance. Until you find out about DefaultToInstanced.

                      I really like the whole UObject ecosystem so I'm hoping I'm not taking it too far. You get so much for free. E.g., the program mentioned above is a simple sequence of various actions, where each action's arguments are defined by an UObject, so an NPC instance might carry quite a few of these. I was hoping there was no massive downside to this, since the upside is that I can easily define, create, edit (inline with other Slate stuff, thank you IDetailsView) and serialize actions with a minimal amount of work.

                      As a sidenote, this turns out to be the second time you've helped me out. I went through your Kantan Charts plugin a few weeks ago to brush up on graph drawing, so thanks for that too! Good stuff!

                      Comment

                      Working...
                      X