SPUD is a save game and streaming level persistence solution for Unreal Engine 5.
The 2 main core features:
- Save / Load game state easily
- Streamed levels retain their state as they unload / reload without a save needed
Some more details:
- Easily mark actors in your levels as persistent
- You implement ISpudObject, a marker interface with no required methods
- Pick properties to save
- By enabling the "SaveGame" option
- All property types supported, including arrays, references between objects, and custom structs
- You can also manually mark non-level UObjects (e.g. GameInstance) for inclusion in the save
- Dynamically spawned objects that exist at save are re-spawned on load
- Level objects which have been destroyed are automatically re-destroyed on level load
- Core details like transform, controller rotation and physics state are automatically saved
- Usable in C++ or Blueprints
An introduction video:
This project contains the master documentation for this library, but if you want to see examples of its use, see the SPUD Examples project.
The best way is to clone this repository as a submodule; that way you can contribute pull requests if you want. The project should be placed in your project's Plugins folder.
> cd YourProject
> git submodule add https://github.com/sinbad/SPUD Plugins/SPUD
> git add ../.gitmodules
> git commit
Alternatively you can download the ZIP of this repo and place it in
YourProject/Plugins/SPUD
.
Edit YourProject.Build.cs and do something similar to this:
using System.IO;
using UnrealBuildTool;
public class SPUDExamples : ModuleRules
{
private string PluginsPath
{
get { return Path.GetFullPath( Path.Combine( ModuleDirectory, "../../Plugins/" ) ); }
}
protected void AddSPUD() {
// Linker
PrivateDependencyModuleNames.AddRange(new string[] { "SPUD" });
// Headers
PublicIncludePaths.Add(Path.Combine( PluginsPath, "SPUD", "Source", "SPUD", "Public"));
}
public SPUDExamples(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
AddSPUD();
}
}
After adding this you should right-click your .uproject file and "Generate Visual Studio Project Files".
You MUST save all your levels before playing in editor (PIE). Failure to do so results in mis-categorisation of some level objects. The Output Log will dump an error about this when unsaved levels are detected.
To fix this, either:
- Use the menu option File -> Save All Levels before playing in the editor
- In Project Settings > Plugins > SPUD, enable the option Save All Levels On Play In Editor.
The latter option is your best bet for making sure you don't accidentally have strange bugs, at the expense of a slight delay to PIE if you have unsaved levels.
Do not report any bugs unless you've checked your levels are saved!!
The core functionality of SPUD is writing the state of chosen UObjects, including a chosen subset of their properties, to a persistent data format so it can be saved / loaded and kept consistent across map changes and streaming.
Actors present in your world can be picked up automatically. To do this,
you opt-in your classes by implementing the interface ISpudObject
. You can
do this in C++:
class AMyActor : public AActor, public ISpudObject
{
...
or in Blueprints:
You don't have to implement any methods on this interface, it is solely a marker so that SPUD will know to look at your object. Any actor marked this way will be automatically made persistent. This includes GameModes and GameStates.
Global objects like GameInstance won't be picked up for persistence even if
you implement ISpudObject
, because they're not included in the world. However,
you can opt these objects in to persistence so they also get saved:
GetSpudSubsystem(GetWorld())->AddPersistentGlobalObjectWithName(this, "MyGameInstance");
Global objects must always exist, SPUD won't re-create them on load, but it will re-populate their state.
Just by opting the class in to SPUD persistence, the following state is automatically saved:
- Hidden flag
- Transform (Movable objects only)
- Controller Rotation (Pawns only)
- Physics velocities (Physics objects only)
- Any Movement Component's velocity (e.g. player movement, projectile movement, if present)
In addition to the standard state, you can then tell SPUD to save additional properties of
the object. You use the "SaveGame" UPROPERTY
flag to do this.
In C++:
UPROPERTY(SaveGame)
int MySavedInt;
or Blueprints, in the advanced property details section:
For the most common case of an object in a level, that's it! Many types of property are supported. For more details, see Properties;
If a level actor that implements ISpudObject
is destroyed, that destruction will
be made persistent by SPUD. Re-loading a map will automatically re-create that actor,
but as part of the restore process SPUD will destroy it again, returning the
world to the correct state. You don't need to do anything extra to make this work.
Actors which are not part of the level but are spawned at runtime, that also
implement ISpudObject
, will be automatically re-spawned on load.
However, because these objects need to be uniquely identified, you must give
these classes a special FGuid property called SpudGuid
.
For example:
UPROPERTY()
FGuid SpudGuid;
You don't have to assign a value to this property, SPUD will generate a GUID if it's blank. Also you should NOT mark it as SaveGame. It's not your save state, just some metadata SPUD needs to uniquely identify this object.
Some actors are not stored in the level, and are spawned at runtime but not explicitly during game code; they're spawned automatically by the gameplay framework during the initialisation of the level. Examples include:
- Pawns / Characters
- PlayerState
- GameState
- GameMode
If you have state in these objects, then you need to implement the OverrideName
method in ISpudObject
, either in C++ or Blueprints, to give these instances
a unique, pre-defined name.
For example, in C++:
FString AMyPlayerState::OverrideName_Implementation() const
{
static const FString Name("PlayerState");
return Name;
}
In this case we're assuming there's only one player, so only one player state. If you had more than one player then they should each have a unique name. This just makes sure that when restoring these objects, the automatically created instances are correctly associated with the previous state.
The reason we don't use SpudGuid here is because you'd have to come up with a fixed unique GUID which is awkward. And really SpudGuid is for dealing with any number of runtime spawned objects, wheras in these cases there's always a known number of them (often just one), they're more akin to level objects, just automatically constructed ones.
You can call any of the save / load methods on USpudSubSystem
.
For example quick save/load In Blueprints:
Or in C++:
auto SpudSystem = GetSpudSubsystem(GetWorld());
SpudSystem->QuickSaveGame();
There are many other methods for saving to named slots, listing save games and so on. When loading a game, the current map will always be unloaded, and the game will travel to the map in the save game (even if it's the same one). This ensures things are reset correctly before restoring state. For this reason, loading is asynchronous (see events on USpudSubSystem if you want to listen in on when loading completes).
When it comes to streaming, persistence of level data happens automatically so
long as streaming requests are routed through USpudSubSystem
, which has
methods to request streamed levels, or to withdraw a request (levels are streamed
out when outstanding requests hit 0).
SPUD comes with a new streaming volume, ASpudStreamingVolume
to make this
easier to use. But you can call the streaming methods manually as well.
When you travel between maps, SPUD gets notified and will save state to the active game.
More information is available in Levels and Streaming
The MIT License (MIT) Copyright © 2021 Steve Streeting
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.