Download

Unreal Editor is not saving my UPROPERTY values

The Unreal Editor is not saving the values in one of my components.

First, here’s an [FONT=courier new]UActorComponent that’s attached to an Actor in my map. Its properties are saved.


/// WORKING EXAMPLE, component on Actor in scene
CLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UCandleFlameMovementComponent : public UActorComponent {

  GENERATED_BODY()

  /// Bones that will be moved by the candle flame movement component
  public: UPROPERTY(EditAnywhere, Category=Flames)
  TArray<FName> FlameBoneNames;

  /// Furthest distance the flame bones can move from their original position
  public: UPROPERTY(EditAnywhere, Category=Flames)
  float MovementRadius = 0.005f;

};

Now here’s an [FONT=courier new]UActorComponent that inherits from [FONT=courier new]UAudioComponent. It’s attached to a blueprinted pawn. Its properties get lost every time I close the editor:


/// NON-WORKING EXAMPLE, component on Actor in Blueprint asset
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UBreathingAudioComponent : public UAudioComponent {

  GENERATED_BODY()

  /// Audio clips the component can select from
  public: UPROPERTY(EditAnywhere, Category=Breathing)
  TArray<FBreathingAudioClip> BreathingAudioClips;

  /// Whether the audio component should continuously play breathing sounds
  public: UPROPERTY(EditAnywhere, Category=Breathing)
  bool AutoPlay;

};

Why does one get saved and not the other?

Could it be because I’m inheriting [FONT=courier new]UAudioComponent (maybe that uses “[FONT=courier new]native” serialization somewhere)?

One thing I just noticed is that other properties on the [FONT=courier new]UAudioComponent have that little yellow “Reset to Default Value” arrow next to them when they’re modified.

My properties on the second code snippet never get the little yellow arrow. I don’t see why, though. They’re not marked as [FONT=courier new]Transient or anything.

isuisoundwithundoarrow.png
autoplaywithoutundoarrow.png

One more. When I compile, these messages scroll by in the output window:


[2019.08.21-11.17.12:731][997]LogClass: BreathingAudioClip HotReload.
[2019.08.21-11.17.12:734][997]LogHotReload: Re-instancing BreathingAudioComponent after hot-reload.
[2019.08.21-11.17.12:736][997]LogOutputDevice: Warning:

Script Stack (0 frames):                                                                

Ensure condition failed: bArchetypeReinstanced [File:/opt/unreal-engine-4.22/Engine/Source/Editor/UnrealEd/Private/Kismet2/KismetReinstanceUtilities.cpp] [Line: 1627]          
Reinstancing non-actor (/Engine/Transient.World_1:PersistentLevel.MyActor_C_0.Audio); failed to resolve archetype object - property values may be lost.

Yes, dear error message, indeed, the property values are lost. But what I do against it?

It could be because you don’t set a default value for the properties. Not sure.

BUT there is a better way of doing this in general that you probably won’t have this issue with anyways. You can actually use a Sound Cue that will select a Wave from the selection you provide it:

UE4Editor_20190823_152805.png

For your implementation, you have a couple choices:

  1. Have an attachable component storing a pointer to a USoundBase (would function the same as I assume you want this to function)
  2. Store a pointer to a USoundBase in your actor that is going to breathe

Both of these will work very similarly. Just override BeginPlay (in either your Actor or your child of UActorComponent), and call SpawnSoundAttached, providing it your USoundBase*. Or if you still want the autoplay stuff you can make that a UPROPERTY as well and then only run SpawnSoundAttached at the right time.

My guess though is that there is something goofy going on with UAudioComponent. When you actually run SpawnSoundAttached, that’s the type it returns a pointer to, which just gives me the impression that is the way it’s meant to be used most of the time, but I could be wrong.

Had a similar problem with my RadialDamageComponent. I tried to visualize (editor only) the damaging area using USphereComponent. For me helped to move debug sphere creation from OnRegister to CTOR and using CreateEditorOnlyDefaultSubobject() instead of NewObject. I hope it helps you too.

RadialDamageComponent.h


#pragma once

#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "UObject/SparseDelegate.h"
#include "RadialDamageComponent.generated.h"

class URadialDamageComponent;

DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(
   FRadialDamageComponentAppliedDamageSignature, URadialDamageComponent, OnRadialDamageApplied, URadialDamageComponent*, Component);


UCLASS( ClassGroup=(Damaging), meta=(BlueprintSpawnableComponent) )
class TANKSVR_API URadialDamageComponent : public USceneComponent
{
   GENERATED_BODY()

public:
   URadialDamageComponent(const FObjectInitializer& ObjectInitializer);

   virtual void BeginPlay() override;
   virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
   virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
   virtual void OnRegister() override;
   virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override;

   UFUNCTION()
   void OnOwnerTookAnyDamage(AActor* DamagedActor, float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, AActor* DamageCauser);

#if WITH_EDITOR
   virtual void CheckForErrors() override; 
#endif

#if WITH_EDITORONLY_DATA
   static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector);
   virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
   virtual void RefreshVisualRepresentation();
#endif

private:
   void Validate_OwnerCanBeDamaged() const;
   FString GetErrorMsg_OwnerCanBeDamaged() const;

public:
   UPROPERTY(BlueprintAssignable, Category = "Components|RadialDamageComponent")
   FRadialDamageComponentAppliedDamageSignature OnRadialDamageApplied;

protected:
   UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(UIMin=0, ClampMin="0"), Category=Options)
   float DamageValue = 0;

   UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(UIMin=0, ClampMin="0"), Category=Options)
   float DamageRadius = 100;

   UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Options)
   TSubclassOf<UDamageType> DamageTypeClass;

private:

#if WITH_EDITORONLY_DATA
   class USphereComponent* DamageRadiusDebugSphere = nullptr;
#endif
};

RadialDamageComponent.cpp


#include "RadialDamageComponent.h"
#include "DrawDebugHelpers.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Logging/MessageLog.h"
#include "Misc/UObjectToken.h"
#include "TanksVr/TanksVrCheatSheet.h"
#include "TanksVr/TanksVr.h"


static int32 GShowDebugSphere = 0;
FAutoConsoleVariableRef CVarDebug(
   TanksVrCheatSheet::RadialDamageComponent_ShowDebugSphere,  GShowDebugSphere,
   TanksVrCheatSheet::RadialDamageComponent_ShowDebugSphere_Description,  ECVF_Cheat);


URadialDamageComponent::URadialDamageComponent(const FObjectInitializer& ObjectInitializer)
   : Super(ObjectInitializer)
{
   PrimaryComponentTick.bCanEverTick = true;
   PrimaryComponentTick.SetTickFunctionEnable(true);
   bAutoActivate = true;

   DamageRadiusDebugSphere = ObjectInitializer.CreateEditorOnlyDefaultSubobject<USphereComponent>(this, TEXT("DamageRadiusSphere_EditorOnly"));
   DamageRadiusDebugSphere->SetupAttachment(this);
   DamageRadiusDebugSphere->SetIsVisualizationComponent(true);
   DamageRadiusDebugSphere->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
   DamageRadiusDebugSphere->CreationMethod = CreationMethod;
   DamageRadiusDebugSphere->CastShadow = false;
}


void URadialDamageComponent::BeginPlay()
{
   Super::BeginPlay();

   if (GetOwnerRole() == ROLE_Authority)
   {
      GetOwner()->OnTakeAnyDamage.AddDynamic(this, &URadialDamageComponent::OnOwnerTookAnyDamage);
   }
}


void URadialDamageComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
   GetOwner()->OnTakeAnyDamage.RemoveDynamic(this, &URadialDamageComponent::OnOwnerTookAnyDamage);

   Super::EndPlay(EndPlayReason);
}


void URadialDamageComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
   Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

   if (GShowDebugSphere)
   {
      DrawDebugSphere(GetWorld(), GetComponentLocation(), DamageRadius, 12, FColor::Red);
   }
}


void URadialDamageComponent::OnOwnerTookAnyDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType,
   AController* InstigatedBy, AActor* DamageCauser)
{
   ensure(DamagedActor == GetOwner());
   ensure(GetOwnerRole() == ROLE_Authority);

   UE_LOG(TanksVR, Log, TEXT("%s received damage. Apply self radial damage %f at %s] with radius %f"),
      *GetOwner()->GetName(), DamageValue, *GetComponentLocation().ToString(), DamageRadius);

   // TODO: Check Damage value
   // TODO: Here might be check for out health component. For now assume this object takes one damage to do own damage
   UGameplayStatics::ApplyRadialDamage(this, DamageValue, GetComponentLocation(),
      DamageRadius, DamageTypeClass, TArray<AActor*>(), GetOwner(), InstigatedBy, true);

   OnRadialDamageApplied.Broadcast(this);
}


#if WITH_EDITOR

void URadialDamageComponent::CheckForErrors()
{
   Super::CheckForErrors();

   const auto Owner = GetOwner();
   if (Owner && !Owner->CanBeDamaged())
   {     
      FMessageLog("MapCheck")
         .Warning()
         ->AddToken(FUObjectToken::Create(Owner))
         ->AddToken(FTextToken::Create(FText::FromString(GetErrorMsg_OwnerCanBeDamaged())));
   }
}

#endif


void URadialDamageComponent::OnRegister()
{
#if WITH_EDITORONLY_DATA
   {
      Validate_OwnerCanBeDamaged();
      RefreshVisualRepresentation();
   }
#endif

   Super::OnRegister();
}


void URadialDamageComponent::OnComponentDestroyed(const bool bDestroyingHierarchy)
{
   Super::OnComponentDestroyed(bDestroyingHierarchy);

#if WITH_EDITORONLY_DATA
   {
      if (DamageRadiusDebugSphere != nullptr)
      {
         DamageRadiusDebugSphere->DestroyComponent();
         DamageRadiusDebugSphere = nullptr;
      }
   }
#endif
}


void URadialDamageComponent::Validate_OwnerCanBeDamaged() const
{
   const auto Owner = GetOwner();
   if (Owner && !Owner->CanBeDamaged())
   {     
      UE_LOG(TanksVR, Warning, TEXT("%s"), *GetErrorMsg_OwnerCanBeDamaged());
   }
}


FString URadialDamageComponent::GetErrorMsg_OwnerCanBeDamaged() const
{
   const auto Owner = GetOwner();
   const FString Message = FString::Printf(
         TEXT("%s actor has disabled 'Can Be Damaged' option. Enabled it to activate RadialDamageComponent"),
         *Owner->GetName());
   return Message;
}


#if WITH_EDITORONLY_DATA

void URadialDamageComponent::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
   auto This = CastChecked<URadialDamageComponent>(InThis);
   Collector.AddReferencedObject(This->DamageRadiusDebugSphere);

   Super::AddReferencedObjects(InThis, Collector);
}


void URadialDamageComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
   Super::PostEditChangeProperty(PropertyChangedEvent);

   RefreshVisualRepresentation();
   Validate_OwnerCanBeDamaged();
}


void URadialDamageComponent::RefreshVisualRepresentation()
{
   if (DamageRadiusDebugSphere != nullptr)
   {
      DamageRadiusDebugSphere->SetSphereRadius(DamageRadius);
   }
}

#endif