Jump to content

Saving & Loading - High Level View


Recommended Posts

The goal of this post will be to cover saving and loading. I sometimes forget how I do it, which might sound ridiculous to you, but when you have as many systems as I've already got then it gets a bit complicated. What we are going to do is break it down, where I need to put data and how the storing, loading, and accessing is done.

Player Handler -> Player Datum

One thing we have to know is that our serialization system is limited when it comes to what we can serialize. It is highly advised to not serialize Unity Objects, Prefabs or Scriptable Objects. However, I really really want the data off of these silly things. So I started thinking about the best route to take for this. Ultimately I realized that there is one thing that we know will always exist and that is the player. So we could trivially consolidate information through the player handler.

This does not mean that our player handler is enormous with a bunch of functions and variables to cover everything about the game. Instead the handler contains two concepts; the first is the Player Datum, this holds all of the saveable data for the player, the second is the artifacts associated with all this data. These are scriptable object containers that any asset in the entire project can reference.

/// <summary>
/// On awake we update all the artifacts to reference the saveable data.
/// </summary>
private void Awake()
{
    _artifactPlayer.Value = this;
    _inventoryArtifact.Value = PlayerInfo.Inventory;
    _moosecatsCollectionArtifact.Value = PlayerInfo.MoosecatsCollection;
    _inventoryArtifact.Value = PlayerInfo.Inventory;
    _unlockedAdventuresArtifact.Value = PlayerInfo.UnlockedAdventures;
    _worldItems.Value = PlayerInfo.WorldItems;
}

So as we can see here, we've got our various artifacts, and I'm noticing I need to update the name of a couple, but regardless, we have them connect with our player info via reference. This way anything that updates any of these artifacts is automatically saved and loaded when the player is saved and loaded.

Could we split this up further down the road? Of course! Will we? If we find that it is helpful. But for now this is the simple solution for making sure to save all of the data. But then how does loading work?

Savers and Loaders

We have the concept of saveable and loadable objects. These implement the IOdinSerializeable<TDataToSerialize> interface. This is an interface I created that will save and load your data to and from binaries as long as you define the type.

There are two virtual functions that will need to be overwritten on any of our savers or loaders. These functions are what handle the object specific saving rules and the object specific loading rules. For saving, for instance, I have a rotating saver mechanism for the player data. This will save your data to a specific index each time you save, rotating by one up to the maximum number of backed up saves you are allowing.

This way if data is corrupted on a particular save, or if you made a mistake, you can just load a prior save.

For our loader in this example, it loads the data then raises an event to tell the state machine that it has loaded. This allows us to move from say, initialization state, into the intro or gameplay state. This way we never have to worry about race conditions, objects loading before the game is ready to handle them.

Caveats

So the one thing to remember with this current system is that we need to remember to create an artifact for the data that will be saving and we need to make sure that we add that artifact to our player data. Finally we need to make sure that on awake (or elsewhere later in the game's life) that we assign a reference from that artifact to the data sitting on the player's datum. This allows us to have artifacts that also serialize without issue AND without creating custom savers and loaders for every little thing. It is fast and efficient which is quite nice.

Steps in a Nutshell

  1. Create Datum
  2. Create Database to contain all variants of that Datum
  3. Create an artifact that houses that Database.
  4. Add an instance of that Database to the Player Datum.
  5. Add a reference to that Artifact to our Player Handler.
  6. On Awake, point that Database Artifact to the Database stored in the player info.
  7. Data now saves and loads without any additional work!

It looks like a lot but honestly steps 3-7 take a minute or two and we never have to think about it again which is nice. In the future I might write an editor script to create and assign all of these pieces. Maybe that could be a fun project for a weekend. Though the further along into the project we get the fewer and fewer of these that we'll be making.

Link to comment
Share on other sites

What I Forgot

Usually these won't be the day after I write things, I hope, but we got some things that were missed! Remember when I said there is a save function and a load function we need to think about? Well I totally forgot! Lets talk about it a little bit.

/// <summary>
/// When requested, will load the player data into the appropriate sections of the player class.
/// </summary>
/// <param name="data">The incoming data to load into our player.</param>
public void Load(DatumPlayer data)
{
    _playerInfoArtifact.Value = data;
    transform.position = PlayerInfo.Position;    
    _inventoryArtifact.Value = PlayerInfo.Inventory;
    _moosecatsCollectionArtifact.Value = PlayerInfo.MoosecatsCollection;
    _unlockedAdventuresArtifact.Value = PlayerInfo.UnlockedAdventures;
    _worldItemsArtifact.Value = PlayerInfo.WorldItems;
}

/// <summary>
/// Returns the data that we want to save for our player between sessions.
/// </summary>
/// <returns>The player data to save.</returns>
public DatumPlayer GetSaveData()
{
    return PlayerInfo;
}

When we load we need to apply the loaded data into the related artifacts. This also means that I need to check the Artifact for the Player Datum and make sure I have no null fields there. Otherwise I will save and load a null field. Realistically what I SHOULD DO, right now, before I forget (and I won't) is setup the loader to populate a fresh data body when the information is null, rather than loading null.

Someday man, someday we'll actually do things the moment we should. Today will not be that day. But now we know what I forgot!

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...