Download

С++ Instanced variable and Data Assets are shared values - why?

Good day, community!
I’m working on creating quest system, everything works great, but I missed a point about Instanced UPROPERTY. Hope someone can give me an advice.
I have FMyQuest struct:


USTRUCT(BlueprintType)
struct FMyQuest
{
	GENERATED_BODY()

public:

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	int32 Id;

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	FString Name;

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	bool bIsCompleted;

	int32 CurrentTask;

	UPROPERTY(Instanced, EditInstanceOnly, BlueprintReadWrite, Category = "Quest system")
	TArray<class UMyQuestTask*> Tasks;
};

and UMyQuestTask class, parent class for different types of task (go to point, kill monster, etc):


UCLASS(Blueprintable, BlueprintType, EditInlineNew, abstract)
class MAVRINDIALOGUE_API UMyQuestTask : public UObject
{
	GENERATED_BODY()

public:
	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "Quest system")
	FString Name;

	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "Quest system")
	bool bIsCompleted;

	virtual bool UpdateTask(UObject* _TriggeredObject, const FString& _Message);
	
};

After that I’m creating Data Asset to store all of my quests:


UCLASS(BlueprintType)
class MAVRINDIALOGUE_API UMyQuestDataAsset : public UDataAsset
{
	GENERATED_BODY()
	
	
public:
	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "Quest system")
	TArray<FMyQuest> Quests;
}


Player have an array of current quests. When player talk to someone, in that array I’m adding quest simple by Quests.Add(_Quest).
Let’s for example make an quest with one task - go to point. I’m taking quest from NPC, going to point and UGoToQuestTask (which is inherits from UMyQuestTask), bIsCompleted of task sets “true”.
BUT, here is a problem: in Data Assets that Task’s bIsCompleted become “true” too! Why so? I thought when I’m adding to an array, it creates a copy of that quests, but in real life they are shares values of Tasks.
I also noticed that when Quest’s (not Task) bIsCompleted in array of character becomes “true”, Quest’s bIsCompleted in Data Assets not becomes true. What point I’m missed?

So the reason you’re seeing different behaviour here is because your FMyQuest struct is storing pointers to UMyQuestTask instances.

When you copy a FMyQuest from the DataAsset to add it to the player’s Quests, each of the variables are copied, but those pointer variables are just storing the address of the Tasks.
So now you have two different Quests which point at the same tasks. When the player’s quest update’s the completed state of the task, you see that in the other Quest

You could fix this by declaring a copy constructor for FMyQuest which makes actual copies of the tasks it points to, but my preferd approach would be to separate the offline and runtime data into different classes/structs. It wouldn’t make sense to mark a task or quest as already completed in the data asset, so there is no need for that value to be included there. Conversely you don’t want to change the Name or Id of a quest at runtime, so there is no need to copy that data.

Thank you, Andrew, for great answer! Can I ask a few questions?

  1. About copy constructor for FMyQuest: how to implement that in c++? I don’t actually get an idea on how to copy classes.
  2. About separate data - you mean to create FMyQuestData for ID, Name and Description and FMyQuestPlayer (which is child of FMyQuestData) with additional variables?
  1. So I’ve realised that because these are UObjects, you can’t actually declare your own copy constructors (The GENERATED_BODY() macro creates one automatically I think). But there is another approach we could take. The idea behind declaring our own copy constructor is would change the copy behaviour for the class from doing a shallow copy (only copy the variables in the class) to a deep copy (also copy all the objects owned by the class).
    Unreal Engine provides a function, called DuplicateObject(), which will do a deep copy of the object passed, which is what we would want. Usage is something like:

FMyQuest* QuestCopy = DuplicateObject<FMyQuest>(_Quest,this);
Quests.Add(*QuestCopy);

The second parameter is the Outer or Owner you want for the new object, which in this case I think is the player who is gaining the quest.
There’s some more details on DuplicateObject in this question post https://answers.unrealengine.com/questions/194922/duplicate-uobject-into-another-one.html

  1. I’d suggest each FMyQuestPlayer should have a pointer to a FMyQuestData. That way you avoid copying data you don’t need to change at runtime, but have easy access to it.

struct FMyQuestData
{
	GENERATED_BODY()

public:

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	int32 Id;

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	FString Name;

	UPROPERTY(Instanced, EditInstanceOnly, BlueprintReadWrite, Category = "Quest system")
	TArray<class UMyQuestTask*> Tasks;
};


USTRUCT(BlueprintType)
struct FMyQuestPlayer
{
	GENERATED_BODY()

public:

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	bool bIsCompleted;

	int32 CurrentTask;

	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Quest system")
	FMyQuestData* QuestData;

};

Adding the Quest to the Player would then be something like the following:


FMyQuestPlayer NewQuest;
NewQuest.QuestData = &_Quest;
Quests.Add(NewQuest);

The Player shouldn’t ever modify the variables of it’s FMyQuestPlayers’ QuestData, only read them.
For this to work well you’d also want to take the completed variable out of UMyQuestTask and store that seperatately, maybe as an array of bools in FMyQuestPlayer which is set to be the same size as the Tasks array in the player.

If you do approach 2. then you don’t need to worry about the DuplicateObject function referenced in point 1., as you then don’t need to duplicate anything.