Search code examples
c++unreal-engine4

UE4 C++ saving inventory TArray of item pointers across multiple levels


I have a basic inventory system in UE4 using a TArray of pointers to my custom Item class. It works fine in individual levels, but when I open a new level, the inventory disappears. I've looked at multiple tutorials and posts about this issue and tried various solutions including migrating my inventory array to the Game Instance, and creating a SaveGame class that holds a copy of the array that saves before and loads after opening a level

After all these, inventory still disappears. My code has changed a lot so it's probably not that helpful but here are some snippets of my current solution.

Declaration in character header

UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<AItem*> Inventory_Space;

Declaration in SaveGame

UPROPERTY(SaveGame)
    TArray<AItem*> Inventory_Save;

Save and load functions in character implementation

void ABatteryManPlayer::SaveInventory()
{
    
    UBatteryMan_SaveGame* SaveInstance = Cast<UBatteryMan_SaveGame>
        (UGameplayStatics::CreateSaveGameObject(UBatteryMan_SaveGame::StaticClass()));

    for (int i = 0; i < INVENTORY_SIZE; i++) {
        SaveInstance->Inventory_Save[i] = Inventory_Space[i];
    }
    
    UGameplayStatics::SaveGameToSlot(SaveInstance, TEXT("Slot0"), 0);
}

void ABatteryManPlayer::LoadInventory()
{
    
    UBatteryMan_SaveGame* SaveInstance = Cast<UBatteryMan_SaveGame>
        (UGameplayStatics::CreateSaveGameObject(UBatteryMan_SaveGame::StaticClass()));

    UBatteryMan_SaveGame* SaveInstance = Cast<UBatteryMan_SaveGame>
        (UGameplayStatics::LoadGameFromSlot("Slot0",0));

    for (int i = 0; i < INVENTORY_SIZE; i++) {
        Inventory_Space[i] = SaveInstance->Inventory_Save[i];
    }
}

Saving after game timer goes to 0 (character implementation)

CurrentTime--;
    if (CurrentTime == 0) {

        SaveInventory();
        Instance->Levels_Complete++;

        if (Instance->Levels_Complete < Instance->NUM_LEVELS) {

            FName Level_Name = FName(TEXT("Level_" + FString::FromInt(++Instance->Levels_Complete)));
            UGameplayStatics::OpenLevel(this, Level_Name, false);

        }

Loading back into player inventory in GameMode

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

    ABatteryManPlayer* Player = Cast<ABatteryManPlayer>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
    Player->LoadInventory();

    FTimerHandle UnusedHandle;
    GetWorldTimerManager().SetTimer(
        UnusedHandle, this, &ABatteryMan_GameMode::SpawnPlayerRecharge, FMath::RandRange(2,5), true);
}

Solution

  • I believe that you may have found out why the issue is occurring from our discussion in the comments, but I will try to finish our discussion with this answer. The problem is that you are trying to save an array of pointers to actors in a map. However, the actors in the map get destroyed once you call UGameplayStatics::OpenLevel in order to change the map. As a result, the pointers in that array end up pointing to garbage data, which is why your game is crashing.

    Now, there are many ways to go about this, but you're ultimately going to have to save information about the actors and respawn them. What I have found on Unreal Engine forums is that a common approach is to create a custom struct of type FArchive for information about these actors, in your case about instances of AItem. For example, a struct called AItemInfo which will store info such as the actor's class, the actor's transform, the actor's name, etc., as well as a TArray member representing a serialized bytestream of other data from an actor (AItem). Then, serialize the actor into that struct's TArray member variable using a FMemoryWriter object. Note that typically you wouldn't serialize all the information about actor, only specific variables/properties marked with the SaveGame property specifier when you set the ArIsSaveGame variable in your struct to true. After doing that for each AItem instance you want to keep track of, you can store each instance of this AItemInfo struct in an array defined in your custom USaveGame class. In your case, it's UBatteryMan_SaveGame. Then, you can call UGameplayStatics::SaveGameToSlot on your UBatteryMan_SaveGame instance that contains the array of information structs. When you load that UBatteryMan_SaveGame instance, you can deserialize the array/sequence of bytes in each AItemInfo struct in the array with a FMemoryReader object in order to get the actor information in addition to the other stuff already in the struct and use all of that information to recreate each actor you need from the original map.

    Here are a couple of good links that can help you get started: