Writing a simple save/load system with backups


Why have a persistence system?

So saving/loading is a key thing with any game that wants to have state beyond running the app once. You have a wide variety of options for doing this, and how it resolves in the app itself - be it a profile that loads to a specific checkpoint/revival area, or by reproducing all your choices to the current state of a story.

Of course, there's also like a 1000 ways to implement them!

Ok, so how do I make a simple one?

In my case, I wanted something brutally simple: I want to save key variables to a single, anywhere-accessible class, and save/load that class on a whim to a file that I can easily parse for skimming through when testing/implementing/fixing a save file.

Now what did that translate into?

  • A Singleton class which manages access to a data class, with Load/Save/Reset calls
  • A serialisable Data class which stores all the variables in simple types, inheriting from a base class to simplify doing multiple of these
  • (And then my scripts call the singleton to change the data, saving at various points)

In terms of saving - good news! Unity has a JsonUtility which handles the conversion for you. It's very simple (compared to the Newtonsoft library which has a lot more going on), but it does the job, and for me that job is: write to/read from a file, done.

One last thing before getting into the scripts themselves - you'll need a save location for these files. Now again: loads of options on how you want to do that. For me, again I picked the easiest option, Application.persistentDataPath as it's consistent, easy to find, and should be where all your projects end up.

Sweet, how do I do it?

Ok so the full suite of code (minus some MDay story spoilers ofc) can be found here:

https://gist.github.com/BigHandInSky/673670343b42ed9e43b892daf24d095f

As said above, it revolves around 3 'things':

  • There's a PersistenceModel class, which provides a singleton access from anywhere, and can be started at any point (so you can be in your game scene & get running, or can go from the splash scene too)
  • There's a PersistenceData class, which acts as the data container to handle all the variables to be saved, and is kept with simple public types for writing with the JsonUtility (more complex forms are kept private and used with methods)
  • SaveLoadUtility, a custom helper class to do all the writing, checking, and loading in two defensive methods to help make the load safe to call

~

In my case - as I have a bunch of personal projects (🤞more to come) I wanted to be able to keep the base layer all stashed away, so that the games can just focus on what data needs to be saved.

This translates into the Model/Data being generic classes, and then you need to make your game-specific ones inherit the bases.

i.e:

PersistenceData : PersistenceDataBase
PersistenceModel : PersistenceModelBase<PersistenceData> 

& the game calls PersistenceModel to do saving/loading. This has the iffy side-effect of making it such that it's hard to get at any methods on PersistenceModel - the base is designed to expose the Data as a static accessor, not as a way to get at the Model itself. But for me that works as I've rarely needed the model for any reason - albeit with MDay, needing to reset act-specific variables was a reason for this. I'm not sure of a decent way to solve this atm.

How to use all this? Easy:

// whenever a story is played, it's saved to persistence
// so the game doesn't play it again if you reload it
public bool CheckThenPersistKnot( string knot )
{
     // however, there _are_ certain stories we want
     // to allow the player to replay, like the hints
     if ( knot.Contains( _ignoredHintKeyword ) )
     {
         return false;
     }
     if ( _repeatableStoryKnots.Contains( knot ) )
     {
         return false;
     }
     
     // try to add something, check if it _has_ added,
     // and only Save if there was a change, basically it does:
     // PersistenceModel.Data.GetPersistForAct(<act index>)
     var added = GameManager
                 .Library
                 .GetCurrentPersist()
                 .AddStory( knot );
     if(added)
         PersistenceModel.Save();
     return added;
} 

One big ??? of all this is I've made/tested it for just PC. Other platforms (e.g. mobile) I'm unsure of, as that's not my focus for my personal work. What I expect is a problem is you may run into write/load delays that hold up stuff, but YMMV. On PC, this File write is basically instant when you have a data file that's some 50 variables large, so at great complexity/density this setup might too run into a few headaches.

~

When it comes to displaying a "this game is saving" animation, that you can do by hooking onto one of the Base's PersistenceEvents, then triggering something in your UI.

And you mentioned backups?

Yes, so: I ran into a problem with Forgetful Loop where someone's file was just... gone? As if it wrote but with no data. My understanding is this was a one-in-a-<big number> chance to happen, buuuuut it was on my mind to have some form of backups implemented.

The above gist has it already implemented for you whenever you call Save (and vice versa, when you call Load it checks and logs an error when it has to use a backup)

But how did I implement it? Well that's why SaveLoadUtility exists - it's there to wrap up the various checks in order to then choose when to look for the backup file. Additionally, there's a sneaky gotcha when saving: if, for some reason, a save is invalid, you don't want to backup that invalid save, instead (to my sensibilities) you want to overwite it instead.

See SaveLoadUtility.Save() (some of this has been trimmed to fit the devlog window, so read the gist for the full meat of this method:

// takes a generic class to write as JSON
// here it assumes we'll use Application.persistentDataPath,
// and then takes in a filename to save/backup with
public static bool Save<t>(T data, string filename)
                   where T : class
{
     if ( data == null )
     {
         // <log an exception that this is null>
         return false;
     }
     // wrap all this in a try-catch,
     // so that the game can still function,
     // -however let it be known that this will cause
     // your Data to reset to 0 if so
     try
     {
         var path = GetPathFor( filename, false );
         var backupPath = GetPathFor( filename, true );
 
         Debug.Log( $"SaveLoadUtility.Save saving to {path}" );
         CheckAndMakeDirectory();
 
         var write = JsonUtility.ToJson( data );
         // if the default save is already there,
         // copy it to a backup first
         if ( File.Exists( path ) )
         {
             // if the file exists, but is empty, ignore it
             if (string.IsNullOrEmpty(File.ReadAllText(path)))
             {
                 // do nothing
             }
             // <other checks here>
             else
             {
                 File.Copy( path, backupPath, true );
             }
         }
         File.WriteAllText( path, write );
     }
     catch ( Exception e )
     {
         Debug.LogException( e );
         return false;
     }
     
     return true;
}

And when Loading, it does a similar gist: do a File.Read within a Try-catch, throw an exception if the data is invalid in some way.

In the case of Loading, the Persistence system I've written will throw out an error when attempting to do so from the Model, if this happens, you got multiple options for how to handle it - but to me, you gotta show an error ocurred, and at least alert the player that their progress may have changed a bit. For me, this is handled in the Splash scene (see SplashSceneExample), where it calls the persistence/settings to attempt a Load, stack up any errors into a list, then iterate through those to the player until clear. (Ideally: yeah this never shows, but it's safer than other errors appearing later in-game due to an invalid save file)

~

Aaaaaand that's it, a very simple save/load system that can be used in-editor from anywhere in the project 👍 YMMV with more complex save data than a bunch of ints/strings, but for my purposes, this is doing the job nicely.

Get A Day of Maintenance

Buy Now$20.00 USD or more

Leave a comment

Log in with itch.io to leave a comment.