UObject persistence in GameInstance only works once

So this is my situation: I have a UObject class called Upgrade.
My player character stores its upgrades in a dedicated array.
Another array is used to store inventory items, but they are AActors.
So far so good.

Then Death comes. Or a level load I guess, but I’ll keep it to Death for dramatic effect.

When the character dies, the GameInstance is triggered, the character’s inventory is stored along with the upgrades. On BeginPlay, my character copies the inventory and the upgrades from GameInstance and everything proceeds as usual. And this works, the upgrade is working, I can see the effects of the upgrade being triggered.

This thing, however, works almost randomly. If I kill my character again, when the upgrades are copied, sometimes they are null pointers.

GameInstance code called on death

void UCustomGameInstance::ActorKilled(ABaseCharacter* deadCharacter)
{
    AProtagonistCharacter* protagonistCharacter = Cast<AProtagonistCharacter>(deadCharacter);
    if(protagonistCharacter)
    {
        UE_LOG(LogProcess, Display, TEXT("GameInstance: detected character's death. Copying inventory"));
        UInventoryComponent* playerInventory = protagonistCharacter->GetInventoryComponent();
        PlayerItems.Empty();
        PlayerUpgrades.Empty();
        // Copy the inventory
        for(auto& item: playerInventory->GetHeldItems())
        {
            UE_LOG(LogProcess, Display, TEXT("GameInstance: copying item %s"),*(item->GetName()));
            PlayerItems.Add(item->GetClass());
        }
        // Copy the upgrades
        for(auto& upgrade : playerInventory->GetUpgrades())
        {
            PlayerUpgrades.Add(upgrade->GetClass());
            UE_LOG(LogProcess, Display, TEXT("GameInstance: copying upgrade item %s"),*(upgrade->GetName()));
        }
    }
}

Inventory restore code on BeginPlay

void UInventoryComponent::GameInstanceInit()
{
	UCustomGameInstance* gameInstance = Cast<UCustomGameInstance>(GetWorld()->GetGameInstance());
	UE_LOG(LogTemp, Display, TEXT("Inventory: retrieving data from game instance"));
	AProtagonistCharacter* playerCharacter = Cast<AProtagonistCharacter>(GetOwner());
	for(TSubclassOf<UUpgrade> upgradeClass : gameInstance->GetPlayerUpgrades())
	{
                // This next line is where the crash will occur
		UUpgrade* newUpgrade = NewObject<UUpgrade>(this,upgradeClass);
		newUpgrade->SetMainCharacter(playerCharacter);
		AddUpgrade(newUpgrade);
		UE_LOG(LogTemp, Display, TEXT("Inventory: Adding upgrade: [%s]"),*(newUpgrade->GetName()));
	}
	for(TSubclassOf<AUsableObjects> itemClass : gameInstance->GetPlayerItems())
	{
		AUsableObjects* item = Cast<AUsableObjects>(GetWorld()->SpawnActor(itemClass));
		THeldItems.Emplace(item);
		UE_LOG(LogTemp, Display, TEXT("Inventory: Adding item: [%s]"),*(item->GetName()));	
	}
}

And this is what happens when we put all of this to work:

GameMode: detected character's death
LogProcess: Display: GameInstance: detected character's death. Copying inventory
LogProcess: Display: GameInstance: copying pocket item BP_OW_Sword_C_3
LogProcess: Display: GameInstance: copying backpack item BP_OW_Sword_C_4
LogProcess: Display: GameInstance: copying upgrade item BP_BackpackUpgrade_C_0
LogTemp: Display: He dead!
...
// This is a case where everything goes fine
LogTemp: Display: GameMode: BeginPlay
LogTemp: Display: Inventory: retrieving data from game instance
LogTemp: Display: Inventory: Adding upgrade: [BP_BackpackUpgrade_C_0]
LogTemp: Display: Inventory: Adding item: [BP_OW_Sword_C_3]
LogTemp: Display: Inventory: Adding item: [BP_OW_Sword_C_4]
// Now let's kill this guy again
...
LogTemp: Display: Current Health: 0.000000
LogProcess: Display: GameMode: detected character's death
LogProcess: Display: GameInstance: detected character's death. Copying inventory
LogProcess: Display: GameInstance: copying item BP_OW_Sword_C_3
LogProcess: Display: GameInstance: copying backpack item BP_OW_Sword_C_4
// And there it is:
LogProcess: Display: GameInstance: copying upgrade item None
[2022.10.24-21.43.50:878][346]LogTemp: Display: He dead!

And with that none, I’ll automatically go into segfault once my Inventory tries to create an instance of UObject

NB: This cannot be a case where the inventory is destroyed before the GameInstance manages to copy everything. The player destruction is invoked by GameMode AFTER the copy has been done.

What do the headers for your Inventory component and GameInstance look like? it sounds like you’re missing a UPROPERTY macro somewhere.

GameInstance

class AUsableObjects;
class UUpgrade;
class AProtagonistCharacter;
class ABaseCharacter;
class AInnerSanctumGameModeBase;
UCLASS()
class PRJ_API UCustomGameInstance : public UGameInstance
{
	GENERATED_BODY()
	protected:
		// Player inventory
		bool bPlayerHasBackpack = true;
		TArray<TSubclassOf<AUsableObjects>> PlayerItems;
        TArray<TSubclassOf<UUpgrade>> PlayerUpgrades;
		
	public:
		virtual void Init() override;
		
		// Inventory management
		TArray<TSubclassOf<AUsableObjects>> GetPlayerItems() 	const { return PlayerPocketsItems; }
        TArray<TSubclassOf<UUpgrade>> 		GetPlayerUpgrades() 		const { return PlayerUpgrades;}
		bool					GetPlayerHasBackpack()		const { return bPlayerHasBackpack;}
		
		UFUNCTION()
		void ActorKilled(ABaseCharacter* deadCharacter);
};

Inventory

class PRJ_API UInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

    private:
        UPROPERTY(EditDefaultsOnly, Category="Pockets")
        int PocketsInventorySize = 3;
        UPROPERTY(VisibleAnywhere, Category="Pockets")
        int fAvailablePocketsSpace;
        UPROPERTY(EditAnywhere, Category="Backpack")
        bool bHasBackpack = true;
        UPROPERTY(EditAnywhere, Category="Backpack")
        int BackpackInventorySize = 4;
        UPROPERTY(VisibleAnywhere, Category="Backpack")
        int fAvailableBackpackSpace;
        UStaticMeshComponent* CharacterBackpackMesh;
        UPROPERTY(EditAnywhere, Category="Backpack")
        UStaticMesh* BackpackMesh;
        TArray<AUsableObjects*> THeldItems;
        TArray<UUpgrade*> TUpgrades;
protected:
        // Called when the game starts
        virtual void BeginPlay() override;
        void GameInstanceInit();

Pretty much every single method for the inventory here is also a UFUNCTION()

The BP classes are getting unloaded on level change.
Mark both your TArray<TSubclassOf<>> as UPROPERTY in GameInstance.

That’s what I thought, but it doesn’t explain why only the Upgrades could become None.
I assume there’s some difference between the classes AUsableObjects and UUpgrades, the former being a child of Actor and the latter a child of UObject, and I’m trying to understand what it is.

Setting my arrays as UPROPERTY() both in GameInstance and InventoryComponent seems to have solved the issue, but my doubts remain.

Maybe because the actor classes have direct or indirect references from the newly loaded level so they’re not unloaded, whereas upgrade classes aren’t referenced.

In my experience it’s not really consistent what gets unloaded on level change. There are probably many factors at play.

The Actors are kind of referenced, but not really.

It’s a bit complex.

I have PickupActors, placed in the world, and each one creates another actor (my UsableObjects). They are spawned at runtime though. When the player picks up the pickup, it’s destroyed but the referenced UsableObjects actor is passed to the player inventory.

So even when the game is restarted, there can’t really be any direct link between the UsableObjects contained in my inventory (and GameInstance) and the game world, because they are always spawned anew when the game is started.

Unless Unreal can do some magic where the reference is kept because someone in the world will spawn the actor at BeginPlay

I’m not sure if it’s what is in play here, but there are differences in garbage collection between actors and non-actor UObjects that are worth knowing about. See:

1 Like

Makes perfect sense now.
Every day I think I’ve understood something, Unreal throws a curve ball at me.
Thanks, I’m terrible at navigating Epic’s documentation, I would’ve probably wasted hours trying to find that page


<div class='container-fluid language-javascript' >

  <!-- tabs -->
  <ul class="nav nav-pills">
    <li class="active">
      <a href="#specs" data-toggle="tab">Specs</a>
    </li>
    <li><a href="#readme" data-toggle="tab">Readme</a>
    </li>
  </ul>

  <!-- tab content -->
  <div class="tab-content clearfix" style="margin-top: 20px">
    <div class="tab-pane active" id="specs">
    </div>
    <div class="tab-pane" id="readme">
    </div>
  </div>
</div>
<script type="text/markdown" id="readme-md">
  ### Only One

Write a function `onlyOne` that accepts three arguments of any type.

`onlyOne` should return true only if exactly one of the three arguments are
truthy. Otherwise, it should return false.

Do not use the equality operators (`==` and `===`) in your solution.

```javascript
onlyOne(false, false, true); // => true
onlyOne(0, 1, 2) // => false

// YOUR CODE BELOW
function onlyOne (a,b,c) {
if (
(!!a && !b && !c) || (!a && !!b && !c) || (!a && !b && !!c)
) {
return true
}
return false
}