Exception Thrown: Read Access Violation. This Was Nullptr.
Introduction
For your game you'll eventually demand to write some kind of relieve system. To store role player information, unlocks, achievements, etc. In some cases, you will need to relieve the world state such as looted chests, unlocked doors, dropped player items, etc.
In this tutorial (UE4 & UE5 Compatible) we will become through the setup of your very own C++ SaveGame organization. Different types of games will have their own save systems needs. Apply this article and code every bit a starting point for whatever game you're edifice. You'll demand to exist fairly familiar with Unreal Engine C++ to build this organization.
This won't be a step-by-step tutorial. Instead, it'southward more than of a system breakdown with explanations. The full source code is available for the unabridged project. If you do wish for a more guided approach, I teach this concept and many others in my Unreal Engine C++ Course.
We'll exist creating a save system similar to Dark Souls with a bonfire interaction that saves the globe state. We will be saving a few actors and some histrion information. The blaze itself is a thematic interaction, with the existent interesting bits existence the actual world country that nosotros relieve/load. Such every bit the moved detail locations, previously opened treasure chests, and obtained credits (aka "Souls").
Activeness Roguelike (Reference Project)
The unabridged project is available through GitHub! I recommend you download that and scan through it. It includes boosted details such as the required #includes for each class used.
This project was created for Stanford University Informatics classes (CS193U) that I taught in late 2020. It is the reference project used in my Unreal Engine C++ online course!
SaveGame System Pattern
First, allow's briefly talk about the system design so y'all accept a better understanding of intend one time we go into the lawmaking.
Unreal has a built-in SaveGame UObject that we inherit from and add variables to be written to deejay. Another powerful feature is the Serialize() function available in every UObject/Histrion to catechumen our variables to a binary array and dorsum into variables again. To decide which variables to store, Unreal uses a 'SaveGame' UPROPERTY specifier. The resulting binary array per Actor tin exist added to the SaveGame object simply earlier writing to disk.
Loading the game volition basically practice the inverse operations. We load the SaveGame UObject from deejay, all the variables become restored in this SaveGame object. We then pass all these variables back into the Objects/Actors they originated from such as Role player position, Credits earned, and individual Actor's country (matched by the Actor'southward Name in our example) such every bit whether a treasure chest was looted in our previous session.
To place which Actors we wish to save land for nosotros use an Interface. We as well utilize this interface to allow Actors to reply to a game load (OnActorLoaded) and so he may run some role player-specific lawmaking to properly restore blitheness state etc. In the Action Roguelike project I re-used my GameplayInterface, but I would recommend y'all make a fresh interface specifically for marking objects/actors as savable (eg. SavableObjectInterface)
SaveGame files will be placed nether ../MyProject/Saved/SaveGames./
Saving Globe Land
In order to relieve the world land, we must determine which variables to store for each Histrion and what misc. info we demand to be saved to disk such as earned Credits by each role player. Credits aren't actually office of the world state and belong to the PlayerState grade instead. Even though PlayerState exists in the world and is in fact an Role player, nosotros handle them separately so we tin properly restore information technology based on which Role player information technology belonged to previously. One reason to handle this manually is so we tin can store a unique ID for each player to know who the stats belong to when a player re-joins the server at a later time.
Player Data
For Actor variables nosotros store its Proper noun, Transform (Location, Rotation, Calibration) and an array of byte data which will contain all variables marked with 'SaveGame' in their UPROPERTY.
USTRUCT() struct FActorSaveData { GENERATED_BODY() public: /* Identifier for which Actor this belongs to */ UPROPERTY() FName ActorName; /* For movable Actors, continue location,rotation,scale. */ UPROPERTY() FTransform Transform; /* Contains all 'SaveGame' marked variables of the Actor */ UPROPERTY() TArray<uint8> ByteData; };
Converting Variables to Binary
To catechumen variables into a binary array we demand an FMemoryWriter and FObjectAndNameAsStringProxyArchive which is derived from FArchive (Unreal's data container for all sorts of serialized data including your game content).
Nosotros filter by Interface to avert calling Serialize on potentially thousands of static Actors in the world nosotros don't wish to save. Storing the Actor's proper noun will exist used later to identify which Role player to deserialize (load) the data for. You could come up with your own solution such as an FGuid (mostly useful for runtime spawned Actors that might not have a consistent Proper noun)
The rest of the code is pretty straightforward (and explained in the comments) thanks to the built-in systems.
To know which #includes to use in C++ for our FMemoryWriter and all other classes in this blog, make sure to cheque out the source cpp files.
void ASGameModeBase::WriteSaveGame() { // ... < playerstate saving lawmaking ommitted > // Articulate all actors from whatsoever previously loaded save to avoid duplicates CurrentSaveGame->SavedActors.Empty(); // Iterate the entire globe of actors for (FActorIterator It(GetWorld()); It; ++Information technology) { AActor* Role player = *It; // Only interested in our 'gameplay actors', skip actors that are beingness destroyed // Notation: You might instead use a defended SavableObject interface for Actors you lot want to salve instead of re-using GameplayInterface if (Role player->IsPendingKill() || !Actor->Implements<USGameplayInterface>()) { continue; } FActorSaveData ActorData; ActorData.ActorName = Actor->GetFName(); ActorData.Transform = Actor->GetActorTransform(); // Pass the array to fill with information from Actor FMemoryWriter MemWriter(ActorData.ByteData); FObjectAndNameAsStringProxyArchive Ar(MemWriter, true); // Discover only variables with UPROPERTY(SaveGame) Ar.ArIsSaveGame = true; // Converts Actor's SaveGame UPROPERTIES into binary assortment Actor->Serialize(Ar); CurrentSaveGame->SavedActors.Add(ActorData); } UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0); }
Treasure Chest Example
Now information technology's time to prepare our Actors to be serialized…
Beneath is the TreasureChest code taken directly from the project. Note the ISGameplayInterface inheritance and 'SaveGame' marked on the bLidOpened variable. That will exist the only variable saved to disk. By default, nosotros store the FTransform of the Role player every bit well. So we can push the treasure breast effectually the map (Simulate Physics is enabled) and on next Play the Location and Rotation will be restored along with the lid state.
UCLASS() class ACTIONROGUELIKE_API ASItemChest : public AActor, public ISGameplayInterface { GENERATED_BODY() public: UPROPERTY(EditAnywhere) float TargetPitch; void Interact_Implementation(APawn* InstigatorPawn); void OnActorLoaded_Implementation(); protected: UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly, SaveGame) // RepNotify bool bLidOpened; UFUNCTION() void OnRep_LidOpened(); UPROPERTY(VisibleAnywhere) UStaticMeshComponent* BaseMesh; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UStaticMeshComponent* LidMesh; public: // Sets default values for this thespian'due south backdrop ASItemChest(); };
Finally we take the OnActorLoaded_Implementation() office to implement. This tin can be useful to handle load-specific logic. In the example below nosotros simply call the existing functions that update the state of the Lid to be opened/closed.
Go on in mind however that often you lot can rely on BeginPlay() as your 'OnActorLoaded' replacement. And so long as y'all load the saved data into each Actor BEFORE BeginPlay() has been triggered. This is why we handle the loading logic very early in the process inside our GameMode form (more on that in 'Loading Game State' below)
void ASItemChest::Interact_Implementation(APawn* InstigatorPawn) { bLidOpened = !bLidOpened; OnRep_LidOpened(); } void ASItemChest::OnActorLoaded_Implementation() { OnRep_LidOpened(); } void ASItemChest::OnRep_LidOpened() { bladder CurrPitch = bLidOpened ? TargetPitch : 0.0f; LidMesh->SetRelativeRotation(FRotator(CurrPitch, 0, 0)); }
That takes intendance of the Actor states, all that's left is to iterate PlayerState instances and let them store information too. While PlayerState is derived from Role player and could in theory be saved during the iteration of all globe actors, information technology's useful to exercise it separately so we can match them to Histrion ID's (eg. Steam user ID) instead of a constantly changing Actor name that we did not decide/control for this blazon of runtime spawned Thespian.
Saving Player Data
In my example I chose to fetch all information from PlayerState just before saving the game. We do so by calling SavePlayerState(USSaveGame* SaveObject); This lets us pass in any data is relevant into the SaveGame object, such as the PlayerId and Transform of the Pawn (if the thespian is currently live)
You lot *could* choose to utilize SaveGame properties here also and store some of that thespian data automatically by converting it to binary assortment only like nosotros do with Actors instead of manually writing it into SaveGame, only you'd still need to manually handle the PlayerID and Pawn Transform.
void ASPlayerState::SavePlayerState_Implementation(USSaveGame* SaveObject) { if (SaveObject) { // Gather all relevant data for histrion FPlayerSaveData SaveData; SaveData.Credits = Credits; SaveData.PersonalRecordTime = PersonalRecordTime; // Stored equally FString for simplicity (original Steam ID is uint64) SaveData.PlayerID = GetUniqueId().ToString(); // May not be live while we save if (APawn* MyPawn = GetPawn()) { SaveData.Location = MyPawn->GetActorLocation(); SaveData.Rotation = MyPawn->GetActorRotation(); SaveData.bResumeAtTransform = true; } SaveObject->SavedPlayers.Add together(SaveData); } }
Make sure you lot call these on all PlayerStates earlier saving to disk. It'south of import to notation that GetUniqueId is only relevant/consistent if yous have an Online Subsystem loaded such as Steam or EOS.
Loading Player Data
To retrieve the Thespian Data we do the opposite and have to manually assign the histrion's transform once the pawn has spawned and is ready to do then. Yous could override the histrion spawn logic in gamemode more than seamlessly to use the saved transform instead. For the example I stuck with a more simple approach of handling this during HandleStartingNewPlayer.
void ASPlayerState::LoadPlayerState_Implementation(USSaveGame* SaveObject) { if (SaveObject) { FPlayerSaveData* FoundData = SaveObject->GetPlayerData(this); if (FoundData) { //Credits = SaveObject->Credits; // Makes sure we trigger credits changed event AddCredits(FoundData->Credits); PersonalRecordTime = FoundData->PersonalRecordTime; } else { UE_LOG(LogTemp, Alarm, TEXT("Could not observe SaveGame data for player id '%i'."), GetPlayerId()); } } }
Unlike loading Actor data which is handled on initial level load, for histrion states we want to load them in one-by-one as players join the server that might have previously played with u.s.a.. We tin can exercise so during HandleStartingNewPlayer in the GameMode class.
void ASGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) { // Calling Before Super:: so we set variables before 'beginplayingstate' is called in PlayerController (which is where we instantiate UI) ASPlayerState* PS = NewPlayer->GetPlayerState<ASPlayerState>(); if (ensure(PS)) { PS->LoadPlayerState(CurrentSaveGame); } Super::HandleStartingNewPlayer_Implementation(NewPlayer); // At present we're gear up to override spawn location // Alternatively we could override core spawn location to utilize store locations immediately (skipping the whole 'find player beginning' logic) if (PS) { PS->OverrideSpawnTransform(CurrentSaveGame); } }
As you lot can run across it's even split up into two pieces. The main data is loaded and assigned as before long as possible to make certain information technology'due south ready for our UI (which is created during "BeginPlayingState" in our specific implementation inside of PlayerController) and wait for the Pawn to be spawned before nosotros handle the location/rotation.
This is where you could probably implement it and so that during the creation of the Pawn you use the loaded data instead of looking for a PlayerStart (equally if the default Unreal behavior) I chose to keep things elementary.
GetPlayerData()
The function below looks for the Actor id and uses fall-dorsum while in PIE assuming we have no online subsystem loaded and then. This function is used by Loading of the actor land in a higher place.
FPlayerSaveData* USSaveGame::GetPlayerData(APlayerState* PlayerState) { if (PlayerState == nullptr) { return nullptr; } // Will not give unique ID while PIE so we skip that stride while testing in editor. // UObjects don't have access to UWorld, so we grab it via PlayerState instead if (PlayerState->GetWorld()->IsPlayInEditor()) { UE_LOG(LogTemp, Log, TEXT("During PIE nosotros cannot use PlayerID to recall Saved Actor information. Using first entry in array if available.")); if (SavedPlayers.IsValidIndex(0)) { return &SavedPlayers[0]; } // No saved player data available return nullptr; } // Easiest way to deal with the different IDs is every bit FString (original Steam id is uint64) // Proceed in heed that GetUniqueId() returns the online id, where GetUniqueID() is a function from UObject (very confusing...) FString PlayerID = PlayerState->GetUniqueId().ToString(); // Iterate the array and friction match by PlayerID (eg. unique ID provided by Steam) return SavedPlayers.FindByPredicate([&](const FPlayerSaveData& Data) { return Data.PlayerID == PlayerID; }); }
Loading World State
Ideally you can load your earth state one time while loading your persistent level. This way you can easily load in the level information and and so deserialize whatever Actor Data from deejay BEFORE BeginPlay() is called on annihilation. Your use-case might be more than complex with streaming in/out additional levels on the fly that contain a savable world state. That's a bit out of the scope for at present, specially as my ain games thankfully don't require such functionality. I recommend checking out Steve'southward library equally he does handle such circuitous cases.
Converting Binary back to Variables
To restore our earth state we do somewhat of the opposite every bit before. We load from disk, iterate all actors, and finally use an FMemoryReader to convert each Actor'due south binary data back into "Unreal" Variables. Somewhat confusingly we still utilise Serialize() on the Thespian, but because we pass in an FMemoryReader instead of an FMemoryWriter the function tin be used to pass saved variables back into the Actors.
void ASGameModeBase::LoadSaveGame() { if (UGameplayStatics::DoesSaveGameExist(SlotName, 0)) { CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0)); if (CurrentSaveGame == nullptr) { UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data.")); return; } UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data.")); // Iterate the unabridged globe of actors for (FActorIterator It(GetWorld()); Information technology; ++It) { AActor* Actor = *Information technology; // Only interested in our 'gameplay actors' if (!Histrion->Implements<USGameplayInterface>()) { continue; } for (FActorSaveData ActorData : CurrentSaveGame->SavedActors) { if (ActorData.ActorName == Role player->GetFName()) { Actor->SetActorTransform(ActorData.Transform); FMemoryReader MemReader(ActorData.ByteData); FObjectAndNameAsStringProxyArchive Ar(MemReader, true); Ar.ArIsSaveGame = true; // Convert binary array dorsum into actor's variables Actor->Serialize(Ar); ISGameplayInterface::Execute_OnActorLoaded(Actor); interruption; } } } OnSaveGameLoaded.Broadcast(CurrentSaveGame); } else { CurrentSaveGame = Bandage<USSaveGame>(UGameplayStatics::CreateSaveGameObject(USSaveGame::StaticClass())); UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data.")); } }
Selecting Specific SaveGame from Deejay
To load a specific Save file that might have been selected in a previous level (such as your primary menu) you can easily laissez passer information between levels using GameMode URLs. These URLs are the 'Options' parameter and you probably used them already for things like "?heed" when hosting a multiplayer session.
void ASGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) { Super::InitGame(MapName, Options, ErrorMessage); FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, "SaveGame"); if (SelectedSaveSlot.Len() > 0) { SlotName = SelectedSaveSlot; } LoadSaveGame(); }
Now while loading a level you should laissez passer in ?savegame=MySaveFile in the options. "savegame" as an choice is fabricated up, you can type whatever as your option, simply be sure to parse that same 'option' in C++.
Loading SaveGame before BeginPlay
In the code example prior I showed loading the data during InitGame() which happens pretty early on during the loading phase. That means that we have our level data available and notwithstanding non called BeginPlay() nonetheless on anything. That let's us deserialize variables and use BeginPlay() as a manner to react as if those saved variables are their blueprint originals.
This could be useful to initialize with the relevant saved data or skipping entire blocks of lawmaking in BeginPlay by saving a specific bool such as bHasSpawnedLoot (make certain you mark this with SaveGame) to not accidentally re-run this logic if it has already done then in the previous session and should exercise so only once.
The Blaze
In the previous sections we fix up the entire save/load organization. Now to finish it off, I'll break down how to make a uncomplicated bonfire-style interaction. I'thousand skipping all the steps specific to interacting with the Player itself, you can view the source code for more details.
At present to create the actual Blaze in Blueprint it's super uncomplicated and fast to exercise because nosotros did most of the hard piece of work already. Here are the basic steps required including the Pattern Node below.
- New Role player Design with a mesh and particle organisation (the fire)
- Disable 'Auto Activate' on the particle system, we'll only turn it on after interacting with it in one case (and storing this as bool in the Player for later loads)
- Add the Interface (GameplayInterface in our case) to marker information technology for the save system.
- Add a bool bFireActive and marker it as SaveGame (find it in the variable details, you lot will need to open up upwardly the Advanced options – see below for image)
- Setup the graph similar below – we interact with the burn (Event Interact) which updates bFireActive and so saves the game. We and then update the particle state.
Once interacted with once, the bFireActive is now saved into the bonfire and on the next game load the particle organization will actuate through OnActorLoaded (our own interface function) You can do the same through BeginPlay() equally we'll have loaded our Actor data before that is called every bit mentioned earlier in this postal service.
As you tin see there isn't a lot of complexity involved in this basic SaveGame system. And even setting up savable variables in Blueprint is quite easy once the average has been implemented. There is a lot more to consider and required for your own complete organization that covers all cases and will depend on your game mechanics. Perhaps you'll demand to relieve the state of the ActorComponents likewise, or UObjects that concord info almost abilities and/or attributes. I'll briefly discuss these in the next paragraph, merely all are exterior of the scope of this tutorial.
Limitations & Improvements
Of course, this system is just a starting point for your own fully-featured save system. There are some things to consider when building your own system that I encountered so far including respawning Actors that were spawned during the previous session instead of loaded from a Map/level file.
You also should runway which Actors got destroyed in the previous session. For this you can brand assumptions based on the SaveGame data. When in that location is no SavedActorData in your SaveGame merely your Actor (loaded from level) does accept a savable interface, you should be able to immediately call Destroy on it.
Yous might want to consider placing all this logic in a Game Subsystem which more neatly splits your relieve/loading logic out from the GameMode class.
For the demo project we only every assume a unmarried persistent level and don't save any LevelName with the Actor or accept a specific Array of Actors per (Streaming)Level. That would likely be something you need depending on your game's blueprint.
It'south a great idea to include a version number in your salvage file as a sort of header. This mode you can observe out incompatible (old) SaveGames and even handle conversions from former to new save versions when possible (eg. when a data format has changed just tin be converted with some code) Epic'southward ActionRPG Example has an implementation of this.
Closing
That's all for the basics of your SaveGame system! This can be molded in all directions to fit your needs, and there is certainly a lot that can be added. I strongly recommend checking out the additional resource below for more than information on the subject.
Also, don't forget to sign-upwards for my newsletter beneath for whatsoever new content I postal service and news on when my course launches! Or follow me on Twitter!
References & Farther Reading
- My New C++ Course handling this in more detail (and video!)
- Saving and Loading your game (Docs BP / C++)
- Saving & Loading Actor Data
- Saving/Loading Assortment of Object (Answerhub)
- Tater: Steve's Persistent Unreal Information library (Complete Relieve System Plugin)
Source: https://www.tomlooman.com/unreal-engine-cpp-save-system/
0 Response to "Exception Thrown: Read Access Violation. This Was Nullptr."
Post a Comment