TArray and nullptrs issues

I am working on Inventory System, which is divided into Inventory Component and UI, it works fine, until Items started to clean up from my Inventory suddenly (Checked this while playing editor, after certain item manipulations like equipping/unequipping or even dropping item) all items just gone.
Here’s the code of inventory (Component and UI)

#include "InventoryComponent.h"
#include "GameFramework/Actor.h"
#include "PlayerCharacter.h"
#include "PlayerCharacterHUD.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/Engine.h"
UInventoryComponent::UInventoryComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
    InventorySize = 16;
}
void UInventoryComponent::AddItem(AAbstractItemActor* Item)
{
    if (Item)
    {
        if (Items.Contains(Item))
        {
            UE_LOG(LogTemp, Warning, TEXT("Item already exists in inventory (pointer)!"));
            return;
        }

        if (this->bIsInventoryFull())
        {
            bool bCanStack = !this->bIsCellsInInventoryFull();
            if (!bCanStack)
            {
                UE_LOG(LogTemp, Warning, TEXT("Sorry, your inventory is full! :/"));
                return;
            }
        }

        TArray<int32> Indexes = this->FindItemIndexes(Item);
        TArray<AAbstractItemActor*> ItemsInInventory = this->GetItems();

        if (Indexes.Num() <= 0)
        {
            if (!this->bIsInventoryFull())
            {
                this->Items.Add(Item);
                Item->SetActorHiddenInGame(true);
                Item->SetActorEnableCollision(false);
                Item->SetLifeSpan(1.0f);
            }
            return;
        }

        for (int32 i = 0; i < Indexes.Num(); ++i)
        {
            int32 InvIndex = Indexes[i];
            uint8 AmountInInventory = ItemsInInventory[InvIndex]->GetCurrentItemAmount();
            uint8 MaxStack = ItemsInInventory[InvIndex]->GetMaxItemAmount();
            uint8 AmountOnGround = Item->GetCurrentItemAmount();

            if (AmountInInventory < MaxStack)
            {
                uint8 SpaceLeft = MaxStack - AmountInInventory;

                if (AmountOnGround <= SpaceLeft)
                {
                    ItemsInInventory[InvIndex]->SetCurrentItemAmount(AmountInInventory + AmountOnGround);
                    Item->SetActorHiddenInGame(true);
                    Item->SetActorEnableCollision(false);
                    Item->SetLifeSpan(1.0f);
                    return;
                }
                else
                {
                    ItemsInInventory[InvIndex]->SetCurrentItemAmount(MaxStack);
                    Item->SetCurrentItemAmount(AmountOnGround - SpaceLeft);
                }
            }

            if (Item->GetCurrentItemAmount() == 0)
            {
                Item->SetActorHiddenInGame(true);
                Item->SetActorEnableCollision(false);
                Item->SetLifeSpan(1.0f);
                return;
            }
        }

        if (Item->GetCurrentItemAmount() > 0 && !this->bIsInventoryFull())
        {
            this->Items.Add(Item);
            Item->SetActorHiddenInGame(true);
            Item->SetActorEnableCollision(false);
            Item->SetLifeSpan(1.0f);
        }
        else
        {
            return;
        }
    }
}
void UInventoryComponent::RemoveItem(AAbstractItemActor* Item)
{
    if (!Item) return;
    int32 FoundIndex = Items.IndexOfByPredicate([&](AAbstractItemActor* InItem){return InItem == Item;});
    if (FoundIndex != INDEX_NONE)
    {
        Items.RemoveAtSwap(FoundIndex);
    }
}
void UInventoryComponent::RemoveItemAtIndex(int32 Index)
{
    if (Items.IsValidIndex(Index))
    {   
        Items.RemoveAtSwap(Index);
    }
}
TArray<AAbstractItemActor*>& UInventoryComponent::GetItems()
{    return Items;
}

void UInventoryComponent::PrintInventory() 
{
    if (Items.Num() == 0)
    {
        UE_LOG(LogTemp, Display, TEXT("Inventory is empty"));
        return;
    }
    for (int32 Index = 0; Index < Items.Num(); ++Index)
    {
        AAbstractItemActor* Item = Items[Index];
        
        if (GEngine && Item)
        {
            // Using -1 as a key to allow multiple messages without overwriting
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("[%d] Item name: %s"), Index, *Item->GetItemName()));
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("\tItem description: %s"), *Item->GetItemDescription()));
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, FString::Printf(TEXT("\tItem Amount: %d"), Item->GetCurrentItemAmount()));
        }
    }
}

bool UInventoryComponent::bIsInventoryFull()
{
    return Items.Num()>=InventorySize;

}

bool UInventoryComponent::bIsCellsInInventoryFull()
{
    bool IsSingularCellsAreFull = true;
    for (int32 Index = 0; Index < Items.Num(); ++Index)
    {
        if (Items[Index]->GetCurrentItemAmount()<Items[Index]->GetMaxItemAmount())
        {
            IsSingularCellsAreFull = false;
        }
    }
    return IsSingularCellsAreFull;
}

TArray<int32> UInventoryComponent::FindItemIndexes(AAbstractItemActor* Item)
{
    TArray<int32> FoundIndexes;
    if (!Item) return FoundIndexes;
    UClass* ItemClass = Item->GetClass();
    for (int32 Index = 0; Index < Items.Num(); ++Index)
    {
        if (Items[Index] && Items[Index]->GetClass() == ItemClass)
        {
            FoundIndexes.Add(Index);
        }
    }
    return FoundIndexes;
}

int32 UInventoryComponent::GetMaxSizeInventory()
{
    return InventorySize;
}

int64 UInventoryComponent::GetMoneyAmount()
{
    AActor* Player = UGameplayStatics::GetActorOfClass(GetWorld(), APlayerCharacter::StaticClass());
    if (Player)
    {
        if (IPlayerCharacterInterface* PlayerInterface = Cast<IPlayerCharacterInterface>(Player))
        {
            return PlayerInterface->GetMoney();
        }
    }
    return 0;
}

AAbstractItemActor* UInventoryComponent::GetItemAtIndex(int32 Index)
{
    if (Items.IsValidIndex(Index))
    {
        return Items[Index];
    }
    return nullptr;
}
// Fill out your copyright notice in the Description page of Project Settings.


#include "PlayerCharacterHUD.h"
#include "Components/WrapBox.h"
#include "Components/Overlay.h"	
#include "GameFramework/PlayerController.h"
#include "AbstractItemActor.h"
#include "Animation/WidgetAnimation.h"
#include "UMG.h"
#include "MovieScene.h" 
#include "Templates/Casts.h" 
#include "Components/Widget.h" 
#include "PlayerCharacter.h"
#include "PlayerCharacterInterface.h"
#include "Blueprint/UserWidget.h"

APlayerCharacterHUD::APlayerCharacterHUD() : Super()
{
	ConstructorHelpers::FClassFinder<UUserWidget> PlayerHUD(TEXT("/Game/Blueprints/PlayerCharacterWidget"));
	ConstructorHelpers::FClassFinder<UUserWidget> SlotClass(TEXT("/Game/Blueprints/InventorySlot"));
	MainHUD = PlayerHUD.Class;
	InventorySlotClass = SlotClass.Class;
}

void APlayerCharacterHUD::BeginPlay()
{
	Super::BeginPlay();
	UUserWidget* CharacterPlayerWidget = CreateWidget(GetWorld(), MainHUD);
	CharacterPlayerWidget->AddToViewport();
	InventoryOverlay = Cast<UOverlay>(CharacterPlayerWidget->GetWidgetFromName(FName("InventoryOverlay")));
	ActionOverlay = Cast<UOverlay>(CharacterPlayerWidget->GetWidgetFromName(FName("ActionOverlay")));
	InventoryBox = Cast<UWrapBox>(CharacterPlayerWidget->GetWidgetFromName(FName("InventoryBox")));
	EquipButton = Cast<UButton>(CharacterPlayerWidget->GetWidgetFromName(FName("EquipButton")));
	MoneyInfo = Cast<UTextBlock>(CharacterPlayerWidget->GetWidgetFromName(FName("MoneyInfo")));
	if (InventoryOverlay)
	{
		InventoryOverlay->SetVisibility(ESlateVisibility::Hidden);
		if (InventoryBox)
		{
			InventoryBox->ClearChildren();
			InventoryBox->InvalidateLayoutAndVolatility();
			InventoryBox->SetVisibility(ESlateVisibility::Hidden);
		}
	}
	if (ActionOverlay)
	{
		ActionOverlay->SetVisibility(ESlateVisibility::Hidden);
	}
	if (EquipButton)
	{
		EquipButton->OnClicked.AddDynamic(this, &APlayerCharacterHUD::OnEquipButtonClicked);
	}
}

void APlayerCharacterHUD::ToggleInventory(UInventoryComponent* Inventory)
{
	if (!InventoryOverlay || !Inventory) return;

	isShouldShow = InventoryOverlay->GetVisibility() != ESlateVisibility::Visible;
	InventoryOverlay->SetVisibility(isShouldShow ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
	InventoryBox->SetVisibility(InventoryOverlay->GetVisibility());
	APlayerController* PC = Cast<APlayerController>(GetOwningPlayerController());
	if (!PC) return;

	if (isShouldShow)
	{
		UpdateInventoryUI(Inventory);
		PC->bShowMouseCursor = true;

		FInputModeGameAndUI InputMode;
		InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
		PC->SetInputMode(InputMode);

		MoneyInfo->SetText(FText::Format(NSLOCTEXT("Inventory", "MoneyDisplay", "Money: {0}"), FText::AsNumber(Inventory->GetMoneyAmount())));
		ActionOverlay->SetVisibility(CurrentlySelectedSlot ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
	}
	else
	{
		PC->bShowMouseCursor = false;
		PC->SetInputMode(FInputModeGameOnly());

		ActionOverlay->SetVisibility(ESlateVisibility::Hidden);

		if (CurrentlySelectedSlot)
		{
			CurrentlySelectedSlot->SetSelectedItem(false);
			CurrentlySelectedSlot = nullptr;
		}

		SelectedItemRef = nullptr;
	}
}

void APlayerCharacterHUD::UpdateInventoryUI(UInventoryComponent* Inventory)
{
	if (!InventoryBox || !Inventory) return;

	InventoryBox->ClearChildren();

	for (AAbstractItemActor* Item : Inventory->GetItems())
	{
		if (!Item) continue;

		if (UInventorySlot* Slot = CreateWidget<UInventorySlot>(GetWorld(), InventorySlotClass))
		{
			Slot->SetupSlot(Item);
			Slot->OnSlotClicked.AddDynamic(this, &APlayerCharacterHUD::OnInventorySlotClicked);
			Slot->SetVisibility(ESlateVisibility::Visible);
			InventoryBox->AddChildToWrapBox(Slot);
		}
	}
	InventoryBox->InvalidateLayoutAndVolatility();
}

void APlayerCharacterHUD::OnInventorySlotClicked(AAbstractItemActor* Item)
{
	if (!Item) return;

	if (SelectedItemRef == Item && CurrentlySelectedSlot)
	{
		CurrentlySelectedSlot->SetSelectedItem(false);
		CurrentlySelectedSlot = nullptr;
		SelectedItemRef = nullptr;
		ActionOverlay->SetVisibility(ESlateVisibility::Hidden);
		return;
	}

	if (CurrentlySelectedSlot)
	{
		CurrentlySelectedSlot->SetSelectedItem(false);
	}

	// Find the clicked slot by iterating through children
	for (UWidget* Widget : InventoryBox->GetAllChildren())
	{
		if (UInventorySlot* Slot = Cast<UInventorySlot>(Widget))
		{
			if (Slot->GetItem() == Item)
			{
				CurrentlySelectedSlot = Slot;
				break;
			}
		}
	}

	if (CurrentlySelectedSlot)
	{
		CurrentlySelectedSlot->SetSelectedItem(true);
		SelectedItemRef = Item;
		ActionOverlay->SetVisibility(ESlateVisibility::Visible);
	}
	else
	{
		ActionOverlay->SetVisibility(ESlateVisibility::Hidden);
		SelectedItemRef = nullptr;
	}
}

void APlayerCharacterHUD::OnEquipButtonClicked()
{
    if (!SelectedItemRef) return;

    AActor* Player = UGameplayStatics::GetActorOfClass(GetWorld(), APlayerCharacter::StaticClass());
    if (!Player) return;

    if (IPlayerCharacterInterface* PlayerInterface = Cast<IPlayerCharacterInterface>(Player))
    {
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, TEXT("Item Equipped!"));
        PlayerInterface->EquipItem(SelectedItemRef);

        if (!PlayerInterface->GetEqupiedItem())
        {
            if (CurrentlySelectedSlot)
            {
                CurrentlySelectedSlot->SetSelectedItem(false);
            }
            SelectedItemRef = nullptr;
        }
    }
}

bool APlayerCharacterHUD::bIsHUDActive()
{
	return isShouldShow;
}

I would glad for any explanation of why it’s not working properly (because I struggling with that for couple of day)

Make sure the array has the UPROPERTY macro in case they get cleaned up by the garbage collection.

1 Like

Works better now, I guess. So, UPROPERTY macro works also as some kind off as GC marker? Maybe switching to USTRUCTS would be better?

yeah any UObject if it’s missing the UPROPRTY macro the garbage collection system will not set it to null when it’s destroyed and so you run the risk of using dangling pointers and in some cases the GC will clean up your UObjects without your consent.

UPROPERTY ensures you keep your object alive in memory and it gets set to null if the object is destroyed in some way or another.

I personally prefer structs as well, but if those structs container any UObjects then the same thing applies.

1 Like

Thanks for the helping, man.
Good luck with you!

1 Like