Having issues with Saving/Loading and memory access violations/exceptions?

Hi, I’m relatively new to Unreal, and I’m trying to learn by prototyping a basic game purely in C++. I’m nearly done, except I’m trying to implement a basic local high score system that can be saved between playthroughs. So far, I’ve created a custom SaveGame class that just has an array that will hold the top 5 scores, and I’ve tried to implement it in my GameMode by calling a function whenever the player game overs. The function ‘sorts’ the current score into the array, saves the array, and sends the high scores to the UserWidget as an FText to be displayed in the UI.

For the most part, it works perfectly, except every once in a while, the game will throw a memory access violation. The violation varies from having an issue in SaveGameToMemory() in GameplayStatics, in IsChildOf() in UClass, and at one point Array.h. The violation is also inconsistent, as it doesn’t seem to be predictable as when it is thrown, just that if I play test the game long enough, it will throw an exception. I’ve narrowed it down to the issue mostly happening in the sorting part of my function, but no matter what I do, it either doesn’t work how I want it to, or it will throw an exception. I’m at my wit’s end and would like any help in either finding out what can cause the exceptions, or alternatively, another way to sort my high scores. Currently, the latter is called SetScoreBoard(), and it iterates through the high score array starting from the highest, until it finds a score that the current score is higher than, where then it will insert the new score at that index, then reduce the 6 elements in that array down to 5.

Here is my code:
RunnerSaveGame.h
[SPOILER]


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "RunnerSaveGame.generated.h"

/**
 *
 */
UCLASS()
class RUNNER_API URunnerSaveGame : public USaveGame
{
    GENERATED_BODY()

public:

    URunnerSaveGame();

    UPROPERTY(EditAnywhere)
        TArray<int> HighScoreBoard;

};


[/SPOILER]

RunnerSaveGame.cpp:
[SPOILER]


// Fill out your copyright notice in the Description page of Project Settings.


#include "RunnerSaveGame.h"

URunnerSaveGame::URunnerSaveGame()
{
    HighScoreBoard.Init(0, 5);
}

[/SPOILER]

RunnerGameModeBase.h:
[SPOILER]


// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "RunnerGameModeBase.generated.h"

UENUM(BlueprintType)
enum class ERunnerState:uint8
{
    EStartMenu,
    EInstructions,
    EPlaying,
    EGameOver,
    EUnknown
};

class AMainCharacter;
class AObstacleSpawn;

/**
 *
 */
UCLASS()
class RUNNER_API ARunnerGameModeBase : public AGameModeBase
{
    GENERATED_BODY()

public:

    ARunnerGameModeBase();

    UFUNCTION(BlueprintPure, Category="Score")
        int GetScore() const;

    UFUNCTION(BlueprintCallable, Category = "Score")
        void AddScore(int ScoreToAdd);

    UFUNCTION(BlueprintPure, Category="State")
        ERunnerState GetCurrentState() const;

    UFUNCTION(BlueprintCallable, Category = "State")
        void SetCurrentState(ERunnerState NewState);

    virtual void Tick(float DeltaTime) override;

    virtual void BeginPlay() override;

protected:

    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Score", Meta=(BlueprintProtected="true"))
        int Score;

    UPROPERTY(EditDefaultsOnly, BluePrintReadonly, Category = "Player UI", Meta = (BlueprintProtected = "true"))
        TSubclassOf<class URUserWidget> RunnerWidget;

    UPROPERTY()
        class URUserWidget* CurWidget;

    class AMainCharacter* Main;

    class AObstacleSpawn* ObSpawn;

    UPROPERTY(EditAnywhere)
        TArray<int> ScoreBoard;

private:

    ERunnerState CurrentState;

    void HandleNewState(ERunnerState NewState);

    class URunnerSaveGame* RunSave;

    void SetScoreBoard();
};


[/SPOILER]

RunnerGameModeBase.cpp:
[SPOILER]


// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.


#include "RunnerGameModeBase.h"
#include "Blueprint/UserWidget.h"
#include "RUserWidget.h"
#include "Kismet/GameplayStatics.h"
#include "Maincharacter.h"
#include "Components/TextBlock.h"
#include "ObstacleSpawn.h"
#include "EngineUtils.h"
#include "Components/PanelWidget.h"
#include "GameFramework/Pawn.h"
#include "RunnerSaveGame.h"

ARunnerGameModeBase::ARunnerGameModeBase()
{
    PrimaryActorTick.bCanEverTick = true;

    Main = Cast<AMainCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));

    RunSave = Cast<URunnerSaveGame>(UGameplayStatics::CreateSaveGameObject(URunnerSaveGame::StaticClass()));
    if (UGameplayStatics::DoesSaveGameExist("SlotOne", 0)) {
        RunSave = Cast<URunnerSaveGame>(UGameplayStatics::LoadGameFromSlot("SlotOne", 0));
        ScoreBoard = RunSave->HighScoreBoard;
    }
    else {
        ScoreBoard.Init(0, 5);
    }
}

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

    for (TActorIterator<AObstacleSpawn> StageItr(GetWorld()); StageItr; ++StageItr) {
        ObSpawn = Cast<AObstacleSpawn>(*StageItr);
    }

    if (Main)
    {
        Score = 0;
    }

    if (CurWidget != nullptr)
    {
        CurWidget->RemoveFromViewport();
        CurWidget = nullptr;
    }

    if (RunnerWidget != nullptr)
    {
        CurWidget = CreateWidget<URUserWidget>(GetWorld(), RunnerWidget);
        if (CurWidget != nullptr)
        {
            CurWidget->ScoreText = FText::FromString(FString::FromInt(Score));
            CurWidget->AddToViewport();


        }
    }
    SetCurrentState(ERunnerState::EStartMenu);
}

void ARunnerGameModeBase::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (Main) {
        if (Main->IsAlive == false && GetCurrentState() != ERunnerState::EGameOver)
        {
            SetCurrentState(ERunnerState::EGameOver);
        }
    }
}


int ARunnerGameModeBase::GetScore() const
{
    return Score;
}

void ARunnerGameModeBase::AddScore(int ScoreToAdd)
{
    Score += ScoreToAdd;
    CurWidget->ScoreText = FText::FromString(FString::FromInt(Score));
    CurWidget->ScoreBlock->SetText(CurWidget->ScoreText);
}

ERunnerState ARunnerGameModeBase::GetCurrentState() const
{
    return CurrentState;
}

void ARunnerGameModeBase::SetCurrentState(ERunnerState NewState)
{
    CurrentState = NewState;
    HandleNewState(CurrentState);
}

void ARunnerGameModeBase::HandleNewState(ERunnerState NewState)
{
    //Not the most optimal, but works for now
    switch (NewState) {

    case ERunnerState::EStartMenu: {
        CurWidget->StartPanel->SetVisibility(ESlateVisibility::Visible);
        CurWidget->InstructPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->ScorePanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->GameOverPanel->SetVisibility(ESlateVisibility::Collapsed);
        Score = 0;
        ObSpawn->SetSpawnActive(false);
        Main->SetActorEnableCollision(false);
        break;
    }
    case ERunnerState::EInstructions: {
        CurWidget->InstructPanel->SetVisibility(ESlateVisibility::Visible);
        CurWidget->StartPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->ScorePanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->GameOverPanel->SetVisibility(ESlateVisibility::Collapsed);
        break;
    }
    case ERunnerState::EPlaying: {
        CurWidget->ScorePanel->SetVisibility(ESlateVisibility::Visible);
        CurWidget->StartPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->InstructPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->GameOverPanel->SetVisibility(ESlateVisibility::Collapsed);
        ObSpawn->SetSpawnActive(true);
        Main->SetActorEnableCollision(true);
        break;
    }
    case ERunnerState::EGameOver: {
        CurWidget->GameOverPanel->SetVisibility(ESlateVisibility::Visible);
        CurWidget->StartPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->InstructPanel->SetVisibility(ESlateVisibility::Collapsed);
        CurWidget->ScorePanel->SetVisibility(ESlateVisibility::Collapsed);
        SetScoreBoard();
        ObSpawn->SetSpawnActive(false);
        CurWidget->UpdateFinalScore();
        break;
    }
    case ERunnerState::EUnknown:
    default: {

        break;
    }
    }

}

void ARunnerGameModeBase::SetScoreBoard()
{
    if (!RunSave) {
        RunSave = Cast<URunnerSaveGame>(UGameplayStatics::CreateSaveGameObject(URunnerSaveGame::StaticClass()));
    }

    for (int i = 0; i < ScoreBoard.Num(); i++) {
        if (Score > ScoreBoard*) {
            ScoreBoard.Insert(Score, i);
            break;
        }
    }
    ScoreBoard.SetNum(5);
    if (UGameplayStatics::DoesSaveGameExist("SlotOne",0))
    {
        RunSave->HighScoreBoard = ScoreBoard;
        UGameplayStatics::SaveGameToSlot(RunSave, TEXT("SlotOne"), 0);
    }
    else {
        RunSave = Cast<URunnerSaveGame>(UGameplayStatics::CreateSaveGameObject(URunnerSaveGame::StaticClass()));
        RunSave->HighScoreBoard = ScoreBoard;
        UGameplayStatics::SaveGameToSlot(RunSave, TEXT("SlotOne"), 0);
    }

    FString HSList;
    for (int i=0; i<ScoreBoard.Num();i++)
    {
        HSList.Append(FString::FromInt(i + 1) + "- " + FString::FromInt(ScoreBoard*)+"
");
    }
    CurWidget->HighScoreList = FText::FromString(HSList);
    CurWidget->HighScoreText->SetText(CurWidget->HighScoreList);
}


[/SPOILER]
Note: I know a lot of this code is not optimal, but some are for precaution, and others I may try to optimize later.

Hi.

Looks like the garbage-collector could delete your object while you are sorting.
You may need to place an “UPROPERTY()” macro in the header above the following properties:

class AMainCharacter* Main;

class AObstacleSpawn* ObSpawn;

class URunnerSaveGame* RunSave;

Otherwise, your created object is not referenced anywhere and will be destroyed in the next garbage collection cycle, which is every 60 seconds or so.

Good luck

Unfortunately, adding the macros doesn’t seem to help, as the game still throws an exception in the same spot in GameplayStatics. It’s commonly in SaveGameToMemory() at the line that says


SaveGameObject->Serialize(Ar);

Don’t know if that helps.

So, after a bit of moving stuff around, I think I managed to get it to work. Originally, I had casted Main and RunSave in BeginPlay(), but moved them to the constructor to test something out. I moved them back into BeginPlay(). @djchase You were right in that adding UPROPERTY() helped, specifically with RunSave. Main also had/started having a problem where it was having issues with casting, so I added a check to make sure it would cast. So, at least for now, it works perfectly.

Thanks man! You saved me.

I had the same problem, but i think it is just enough if you put UPROPERTY inside of the SaveGame class:


UCLASS()
class SIDERUNNER_API USideRunnerSaveGame : public USaveGame
{
    GENERATED_BODY()

public:

    //
    UPROPERTY()
    int HighScore = 0;
};