2

I apologize if this seems trivial, but I've searched for an answer for a while now, and I can't seem to find a solution.

I have a list of structs (TArray of structs to be exact.) that represent high scores in my game. (Each struct represents a high score, the fields are something like "Name, Date, Player Score, Game Mode etc" I use this SaveGame method to save a load my array of high scores to and from a file.

I used this with mock values and the system works, now I want to create a UMG widget that will display the list of high scores and this is where I ran into a snag.

I want to use a ListView to show each struct as a ListViewEntry. As far as I understand(I was following this tutorial), A UMG List View needs it's entry widgets to implement the IUserObjectListEntry specifically one has to implement the OnListItemObjectSet (UObject* ListItemObject) method. This method is responsible for assigning an object to the Listview entry and mapping its fields to the various visual widgets. You can see my problem now, I have a list of UStructs and this method needs a UObject pointer.

I'm really at a loss at what I need to do to make this work with a UStruct. Short of creating a dummy UObject that's pretty much identical to my struct and before passing the struct to this function I need to copy its fields into the dummy UObject and pass it instead. I think this method is very inelegant. There has to be a better way. Do you know any?

I wanted to avoid creating a dummy UObject just for the sake of passing it to this function.

I tried to use an array of UObjects instead of an array of Structs but the problem is, an array of UObjects is always an array of pointers, and when it gets saved, the pointers getting saved and not the actual data, so when it's loaded the data is useless.

Maybe there is a Struct-specific interface one can implement for a ListViewEntry widget? Or maybe there is a way to dereference the pointers of the array of Uobjects before saving them?

TL;DR

I have the following stuct:

c++

USTRUCT()
class FHighScoreEntry
{
    GENERATED_BODY()

public:
    //Player name
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString PlayerName;

    //Player Score
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 PlayerScore;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FDateTime CurrentDateTime;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TEnumAsByte<EGameType> GameType;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 AccuracyTrialMaxTries;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 TimeTrialMaxTime;
}

In the following array;

c++

TArray<FHighScoreEntry> HighScores;

I want to show the array of high scores in a UMG ListView. The ListView requires its entries to implement the User List Object interface, which has this function:

Interface event to implment

As you can see, the event only accepts UObjects. Hence my problem.

Curtwagner1984
  • 1,908
  • 4
  • 30
  • 48
  • @JeJo If I understand correctly, you're suggesting to derive `FHighScoreEntry` from `UObject`. I've tried that, as I explained in the post, I've run into a problem that a `TArray` of `UObject` has to be an array of pointers `TArray FHighScoreEntries`. And if it's an array of pointers, I'm having problems saving it to file, like I explained in the post, it just saves the pointers and not the data. – Curtwagner1984 Sep 15 '21 at 10:06

2 Answers2

2

This was asked 8 months ago, so may no longer be useful to you. But this post is the only thing I could find when searching for this issue on the Internet, so I am posting my solution for the next person researching this.

At a high level, create a UObject wrapper for the struct.

In my USaveGame class I have an array of structs because as you mentioned, an array of UObject pointers does not actually save any data. I created an UObject derived class that simply contains the same struct as the sole UPROPERTY.

UCLASS(Blueprintable, BlueprintType)
class PORTALTEST_API UHighScoreObject : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Score")
    FHighScore HighScore;   
};

In my Game Instance class, I have an array of pointers to this UObject

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Score")
TArray<UHighScoreObject*> HighScoreArray;

I use this array of UObject pointers for the List View of the widget. HighScoreWidgetBlueprint

In the Save function of my Game Instance class I clear the struct of arrays and fill it with with the data contained in the array of UObject pointers. (I am only keeping the top ten high scores, so this seemed more efficient than keeping track of changes in both arrays.)

bool UMyGameInstance::SaveHighScore()
{
    // Call SaveGameToSlot to serialize and save our SaveHighScoreObject with 
    //name HighScoreSaveSlot.sav  
    // Retrieve save values
    SaveHighScoreObject->HighScoreArray.Empty();
    for (auto HighScore : HighScoreArray)
    {
        SaveHighScoreObject->HighScoreArray.Add(HighScore->HighScore);
    }
    // Save game to file
    const bool IsSaved = UGameplayStatics::SaveGameToSlot(SaveHighScoreObject, 
    UNIQUE_HIGHSCORE_SLOT, 0);
    return IsSaved;
}

And in the Load function of my game instance I read in the array of structs and populate the array of UObjects.

bool UMyGameInstance::LoadHighScore()
{
    // Try to load a saved game file with "HighScoreSaveSlot.sav if it exists
    USaveGame* LoadedHighScore = 
    UGameplayStatics::LoadGameFromSlot(UNIQUE_HIGHSCORE_SLOT, 0);
    SaveHighScoreObject = Cast<UHighScoreSaveGame>(LoadedHighScore);

    //If the file does not exist, create a new one
    if (!SaveHighScoreObject)
    {
        // Instantiate a new SaveGame object
        SaveHighScoreObject = Cast<UHighScoreSaveGame> 
(UGameplayStatics::CreateSaveGameObject(UHighScoreSaveGame::StaticClass()));
        // Call SaveGameToSlot to serialize and save our game object with name 
        // "HighScoreSaveSlot.sav"
        const bool IsSaved = 
UGameplayStatics::SaveGameToSlot(SaveHighScoreObject, UNIQUE_HIGHSCORE_SLOT, 0);
        return IsSaved;
    }
    else
    {
        for (auto HighScore : SaveHighScoreObject->HighScoreArray)
        {
            UHighScoreObject* HighScoreObj = NewObject<UHighScoreObject> 
((UObject*)GetTransientPackage(), UHighScoreObject::StaticClass());
            HighScoreObj->HighScore = HighScore;
            HighScoreArray.Add(HighScoreObj);
        }
        return true;
    }
}
Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
Alton Lord
  • 21
  • 2
-1

ListView is made to represent unique objects, so its items need to be UObject, that’s the way the list view class is made.

That’s because adding/removing/looking up the widget for an item needs to be very fast. An object pointer is just a memory address, so it’s fast to find an item, and you can be sure they’re unique (your list won’t accidentally show two widget for the same object). Structs on the other hand, are any arbitrary data, which can be very long (depends on what they contain). So unless you make a hashing algorithm, it’s very expensive to look up if a struct is already in the list.

So for your needs, you can use objects instead of structs to show high scores. For example, objects for each player, since the players are probably already objects. The widget can then cast to the player class when on item object set, and take the high score variable of the player to show it.

If you want to use structs though, you can create a custom widget to show the high scores. To make a vertical list, just make a VerticalBox in your parent widget and a number of widgets for each item in your list, using the create widget from class node (or NewObject in cpp). Then, add your widgets as children of vertical box using the add child to vertical box function.

goose_lake
  • 847
  • 2
  • 15