Bug Report: DuplicateTransient Broken with FNames (PIE)

Hey guys,

We’ve just upgraded our version of the engine to 4.5.1 and i’ve run into an annoying issue with FNames and the
DuplicateTransient Tag.

I have a variable defined like so:

UPROPERTY(Category=General, DuplicateTransient, VisibleAnywhere, meta=(Tooltip="This should be a unique ID for each Object."))
FName BoostItemID;

This works fine in standalone (Same as it did in 4.4.1) where the variable is stored correctly.

However in the PIE the “BoostItemID” Gets wiped and becomes None, (the default) so it seems to be treating the objects as if they’re getting duplicated when we enter the PIE. (I’m guessing it duplicates the scene to PIE?)

It’s rather nasty behavior. Could this possibly be fixed soon?

Cheers guys

Hey -

There are a few questions I’d like to ask to help investigate this issue. Does this also happen in a new 4.5.1 project? Do you see the same behavior if you run PIE in 4.4.1? Do you see the expected behavior when running standalone in 4.5.1? Also, where do you have this variable defined (is it part of an actor class, pawn, character, etc.)?

Cheers

I’ve just gotten back from PAX, so sorry for the late reply:

This does happen in a brand new 4.5.1 project.

This works correctly in 4.1.1 PIE, it’s only started happening in 4.5.1 PIE.

It also works correctly in 4.5.1 Standalone.

It is on an AActor derived class. In it’s ‘private’ section.

Hopefully that helps. :slight_smile:

I’m trying to reproduce this on my machine and have created an actor class with the definition you give above. How/where do you implement the variable? Also, when you PIE are you seeing the same behavior across server and client if you have more than one client set?

I’m pasting the Class Definition here:

UCLASS()
class SUBMERGED_API ASubmergedBoatBoostPickup : public ASubmergedUseActor
{
	GENERATED_UCLASS_BODY()

public:

	UPROPERTY(Transient, Category=Use, BlueprintReadOnly, meta=(Tooltip="Used To tell if this object has been used at all."))
	bool bHasBeenUsed;

private:
	UPROPERTY(Category=SkeletalMeshActor, VisibleAnywhere, meta=(ExposeFunctionCategories="Mesh,Rendering,Physics,Components|SkeletalMesh,Animation"))
	TSubobjectPtr<class USkeletalMeshComponent> SkeletalMeshComponent;

    // I Am Here!
	UPROPERTY(Category=General, DuplicateTransient, VisibleAnywhere, meta=(Tooltip="This should be a unique ID for each Object."))
	FName BoostItemID;
	
	UPROPERTY(Category=General, EditAnywhere, meta=(Tooltip="This is the amount of boost to give the player. (In seconds)"))
	float BoostItemValue;

	UPROPERTY(Category=General, EditAnywhere, meta=(Tooltip="This is the map name we should be expecting to be inside. Will throw a warning if we are in the wrong place."))
	FString ExpectedMapName;

public:
	virtual void BeginPlay() override;
	virtual void OnConstruction(const FTransform& Transform) override;
	
	UFUNCTION()
	void OnMatchStarted();

//use component functions
	virtual bool CanBeUsedBy(ASubmergedCharacter* User) override;
	
	virtual void OnObjectUsed(ASubmergedCharacter* User) override;

	UFUNCTION()
	void OnMadeUseTarget(AController* Controller);
//end use component functions
	
#if WITH_EDITORONLY_DATA 
	static void CheckID(ASubmergedBoatBoostPickup* CurrentPickup, UWorld* CurrentWorld);
#endif //WITH_EDITORONLY_DATA
};

ASubmergedUseActor is Derived from AActor.

And the ID is Populated using the following:
(Next Comment)

#if WITH_EDITORONLY_DATA
int32 GetNextFreeID(TArray &IDList)
{
int32 ReturnValue = 0;

	while (IDList.Find(ReturnValue) != -1)
	{
		++ReturnValue;
	}

	return ReturnValue;
}

void ASubmergedBoatBoostPickup::CheckID(ASubmergedBoatBoostPickup* CurrentPickup, UWorld* CurrentWorld)
{
	TArray<int32> CurrentIDs;

	for (FActorIterator It( CurrentWorld ); It; ++It)
	{
		AActor* Actor = *It;
		if (Actor != NULL && !Actor->IsPendingKill() && Actor->IsA( ASubmergedBoatBoostPickup::StaticClass() ))
		{
			ASubmergedBoatBoostPickup* PickupItem = Cast<ASubmergedBoatBoostPickup>( Actor );

			if (PickupItem == NULL || CurrentPickup == PickupItem)
				continue;

			FString NextIDString = PickupItem->BoostItemID.ToString();

			NextIDString.RemoveFromStart(IDPrefix, ESearchCase::IgnoreCase);
			int32 NextID = FCString::Atoi(*NextIDString);

			if (PickupItem->GetWorld()->GetMapName().Compare(PickupItem->ExpectedMapName, ESearchCase::IgnoreCase) != 0)
				UE_LOG(LogTemp, Warning, TEXT("BoatPickup: '%s' is not in the expected map. ('%s' instead of '%s')"), *(PickupItem->BoostItemID.ToString()), *(PickupItem->GetWorld()->GetMapName()), *(PickupItem->ExpectedMapName));

			if (CurrentIDs.Find(NextID) != -1)
			{
				UE_LOG(LogTemp, Warning, TEXT("BoatPickup: '%s' Has a duplicate!!! Find and kill it!"), *(PickupItem->BoostItemID.ToString()));
			}
			else
			{
				CurrentIDs.Add(NextID);
			}
		}
	}

	if (CurrentPickup != NULL)
	{
		FString NewIDString = FString(IDPrefix);

		NewIDString.AppendInt( GetNextFreeID(CurrentIDs) );

		CurrentPickup->BoostItemID = FName(*NewIDString);
	}
}
#endif // WITH_EDITORONLY_DATA 

Also we don’t have multiple clients, the game is only Single-Player.

Hopefully that helps.

And it gets setup in the following:

void ASubmergedBoatBoostPickup::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

#if WITH_EDITORONLY_DATA
	if (BoostItemID.IsNone() && GetWorld() != NULL)
		CheckID(this, GetWorld());
#endif
}

Hey -

After investigation into this issue it appears that the DuplicateTransient in the UPROPERTY of your FName is causing the value to be reset to it’s default when it is duplicated for PIE. Removing this call should allow the FName to hold its value when in PIE mode.

Cheers

I’m aware that will bandaid -fix the issue.

However, it breaks the purpose of ‘DuplicateTransient’ in the code.

I use this so when the artists/designers create a duplicate in the level, the Unique identifier code is re-created. And not directly copied. (Which breaks things)

So that is NOT a valid fix.

As mentioned before, this is a regression, as it worked fine in 4.4.1.

The duplication for PIE is expected behavior. Without duplicating the level prior to PIE, any changes that are made during PIE would be saved to the level itself at the end. The alternative would be to save the level out and then load in the save for PIE which would be a slower process.

As for the construction script, the reason is that the objects already exist in the editor in the same form as they would in a finished product so by not running the construction script it allows PIE to perform with the same behavior as as a cooked build. This allows for faster and easier cleaning and maintenance of the project overall.

So after a bunch of searching around the code, I found out that 4.5.1 did indeed change the behavior of ‘DuplicateTransient’.

I ended up stumbling across ‘NonPIEDuplicateTransient’ by accident, which is the behavior I was expecting.

This was added in the 4.5.1 update, and I completely missed this.

Hope this helps someone,

Cheers