Skip to content

Program_SaveLoad

bozar42 edited this page Feb 23, 2021 · 2 revisions

How to Save And Load Game Data

< Back to Programming >

Foreword

In order to save and load the game, we need to build a pipeline between data source and game objects. Generally speaking, the pipeline is composed of three parts:

  • [ Data source ] <--> [ Data hub ] <--> [ Game object ]

Game data is usually stored in an external file. But we can retrieve data from various sources. It might be a binary or XML file, an internal dictionary, or a random number which is generated on the fly.

The data hub does two things:

  • Read data from or write data to the source.
  • Collect data from or send data to the object.

Now let's dive a little deeper into the code. In Fungus Cave, XML and binary data goes through two slightly different pipelines:

  • [ XML file <--> SaveLoadFile ] <--> [ XData ] <--> [ GameObjectX ]
  • [ Binary file <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ GameObjectY ]

First let's talk about saving and loading XML files.

XML File

As mentioned above, a XML file goes through this pipeline.

  • [ XML file <--> SaveLoadFile ] <--> [ XData ] <--> [ GameObjectX ]

SaveLoadFile has two public methods to handle XML files:

XElement LoadXML(string path)
void SaveXML(XElement data, string path)

This class interacts with files directly and outputs or requires a complete data object. By complete, I mean LoadXML() reads the whole XML file and outputs everything unabridged.

The XData class extracts a portion of data from the return value of LoadXML(). Some XData class implements ISaveLoadXML and/or IGetData:

ISaveLoadXML simply wraps LoadXML(string path) and SaveXML(XElement data, string path). They interact with a specific file.

public interface ISaveLoadXML
{
    void Load();
    void Save();
}

IGetData, which is defined in GameData, handles data extraction.

public interface IGetData
{
    XElement GetData<T, U>(T t, U u);
    int GetIntData<T, U>(T t, U u);
    string GetStringData<T, U>(T t, U u);
}

My XML files usualy has two levels:

<ActorTag>
  <HP></HP>
  <Name></Name>
</ActorTag>

So in this case t refers to ActorTag and u refers to HP or Name.

ActorData is one of the XData classes. It reads data from Data/actorData.xml.

GameObjectX is the final stop in our XML data trip. It calls XData's methods to get data. For example,

Person.HP.MaxHP = ActorData.GetIntData("Person", "HP");

The three segements, file, data hub and game object are independent of each other. We always need the game object, Person, but we can set MaxHP in different ways:

  • Set it directly: MaxHP = 10.
  • Use XData to read an XML file.
  • Use XData to get the value from an internal dictionary.

Binary File

As mentioned above, a binary file goes through this pipeline.

  • [ Binary file <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ GameObjectY ]

SaveLoadFile has two public methods to handle binary files:

IDataTemplate[] LoadBinary(string path)
void SaveBinary(IDataTemplate[] data, string path)

Binary data is stored in an array. It must implements IDataTemplate:

public interface IDataTemplate
{
    DataTemplateTag DTTag { get; }
}

Every element in the array has a unique DTTag. The tag is critical to loading data. More on this below.

We do not need to serialize a complete Person object, because his MaxHP, Name and some other data can be obtained from XML files. Instead, we can create a smaller class that stores this Person's Salary. Yes, not all people are born equal:

[Serializable]
public class DTPerson : IDataTemplate
{
    public int Salary;
    DataTemplateTag DTTag { get { return DataTemplateTag.Person; } }
}

SaveLoadGame is XData's counterpart:

  • It collects data from objects all over the game and then calls SaveLoadFile.SaveBinary().
  • It calls SaveLoadFile.LoadBinary() and then sends the data to objects.

But how does the collecting and sending work? Inside SaveLoadGame, there are two events:

public event EventHandler<LoadEventArgs> LoadingGame;
public event EventHandler<SaveEventArgs> SavingGame;

The event args are defined like this:

public class LoadEventArgs : EventArgs
{
    public IDataTemplate[] GameData;
    public LoadEventArgs(IDataTemplate[] gameData)
    {
        GameData = gameData;
    }
}

public class SaveEventArgs : EventArgs
{
    public Stack<IDataTemplate> GameData;
    public SaveEventArgs(Stack<IDataTemplate> gameData)
    {
        GameData = gameData;
    }
}

Objects that need to be saved and loaded subscribe these two events and implement ISaveLoadBinary, which is defined in SaveLoadFile:

public interface ISaveLoadBinary
{
    void Load(IDataTemplate data);
    void Save(Stack<IDataTemplate> data);
}

Let's take Person for an example.

// Subscribe the saving event.
SaveLoadGame.SavingGame += Person_SavingGame;
private void Person_SavingGame(object sender, SaveEventArgs e)
{
    SaveBinary(e.GameData);
}

// Implement ISaveLoadBinary.SaveBinary().
public void SaveBinary(Stack<IDataTemplate> data)
{
    DTPerson save = new DTPerson
    {
        Salary = this.salary
    };
    data.Push(data);
}

When loading the game, we need to find out this object's data based on DTTag.

// Subscribe the loading event.
SaveLoadGame.LoadingGame += Person_LoadingGame;
private void Person_LoadingGame(object sender, LoadEventArgs e)
{
    LoadBinary(e.GameData);
}

// Implement ISaveLoadBinary.LoadBinary().
public void LoadBinary(IDataTemplate[] data)
{
    foreach (IDataTemplate idt in data)
    {
        if (idt.DTTag == DataTemplateTag.Person)
        {
            DTPerson value = idt as DTPerson;
            this.salary = value.Salary;
            return;
        }
    }
}

There is one last thing worth mentioning. An object's loaded data might be overwritten by Unity's Start() method. Refer to this article for more information.

< Back to Programming >