Serialize save-load

Hello everybody!
Accordingly with the live training archive 20. Persistence - Saving and Loading Data
I’ve made this very basic script below and it works fine.

Abstract
In Awake() Load() is called, if not persistentData exists Load() calls InitializePersistentData(), then if you press button num value increase and gets stored.
Now I can exit and re-enter game and Awake() calls Load() and finds persistentData and load it.
But if in a next game update I’ll need some new variables to be persistent ( i.e. after public int num; add public string str; ), how can I handle it?
Thanks!

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

//BEGIN OF Test CLASS
public class Test : MonoBehaviour
{
   
   
    public int num;
   
    
    //.dat is arbitrary
    //You can use whatever file extension You like 
    //or not put an extension at all
    private const string _fileNameForPersistentData = "/whatEverNameIwant.dat";
                                                                                     
                         
    //BEGIN OF Awake() FUNCTION
    void Awake()
    { 
        Load();
    }
    //END OF Awake() FUNCTION
   
   
    //BEGIN OF OnGUI() FUNCTION
    void OnGUI()
    {
        if( GUI.Button( new Rect( 10, 10, 150, 100 ), "num = " + num.ToString() ) )
        {
            num++;
           
            Save();//call Save() to persistent store this value
        }
       
       
        if( GUI.Button( new Rect( 10, 130, 150, 100 ), "Reset num" ) )
        {
            InitializePersistentData();
        }
    }
    //END OF OnGUI() FUNCTION
   
       
    //BEGIN OF InitializePersistentData FUNCTION
    public void InitializePersistentData()
    {
        num = 0;
    }
    //END OF InitializePersistentData FUNCTION
   
   
    //BEGIN OF REGION Save() Load()
    #region Save() Load()
   
    //BEGIN OF Save() FUNCTION
    public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();

        FileStream file = File.Create( Application.persistentDataPath 
                                                      + _fileNameForPersistentData );
       
        GameData data = new GameData();
       
        data.numToSerialize = num;
       
        bf.Serialize( file, data );
       
        file.Close();
    }
    //END OF Save() FUNCTION

   
    //BEGIN OF Load() FUNCTION
    public void Load()
    {
        if( !File.Exists( Application.persistentDataPath 
                               + _fileNameForPersistentData ) )
        {
            InitializePersistentData();
        }
       
        if( File.Exists( Application.persistentDataPath
                              + _fileNameForPersistentData ) )
        {
            BinaryFormatter bf = new BinaryFormatter();
   
            FileStream file = File.Open( Application.persistentDataPath 
                                                        + _fileNameForPersistentData, FileMode.Open );
           
            GameData data = ( GameData )bf.Deserialize( file );
           
            file.Close();
           
            num = data.numToSerialize;               
        }
    }
    //END OF Load() FUNCTION
   
    #endregion Save() Load()
    //END OF REGION Save() Load()
       
   
}
//END OF Test CLASS


//BEGIN OF [Serializable] GameData CLASS
[Serializable]
class GameData
{
   
   
    public int numToSerialize;        
   
       
    //BEGIN OF NumToSerializeToSerialize PROPERTY
    bool NumToSerialize
    {   
        set
        {
            numToSerialize = 0;
        }
    }
    //END OF NumToSerializeToSerialize PROPERTY
   

}
//END OF [Serializable] GameData CLASS

This link should lay it out pretty clearly. Basically, in the next version, you mark it with [OptionalField].

1 Like

Do note that while using the default binary serialiser might be easy, it also ties close to your code structure, making later refactors of that code impractical to say the least. As an example, putting your class in a namespace makes reading back data serialised before the namespacing fail.

If you don’t have a lot of data I would still use PlayerPrefs to ensure that a healthy level of abstraction is kept, plus it handles storage for you as well.

If you do have a lot of data, just keep the above limitation in mind and make sure that your structure is abstract enough to most likely make sense in future iterations of your codebase.

1 Like

@StarManta
Thank you StarManta! Thumb up!
One note: [OptionalField] requires System.Runtime.Serialization namespace

@AngryAnt
Thank you too!
Good to know, I’ll take care!

A very quickly recap ( just to know if I get the point ):
If I’ll put my class in a new namespace I’ll get issues 'cause of you said, isn’t it?

This is why I always have a class to store data that is explicitly for serialization. If I’m saving a character’s attributes, then he will not store his HP, MP, strength, etc in his class, but will have a “PlayerData” class and a member of that type, and that’s the one that gets serialized.

I added an int[ ] array, everything works but [OptionalField] on that array.
Very frustrating → no chance for any game update.
Can someone help me how to figure out Version tolerance with array please?

Thanks Everybody :slight_smile:

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;//required by [OptionalField]

//BEGIN OF Test CLASS
public class Test : MonoBehaviour
{
    public int num;
    public string str;

    public int[] array;

    //.dat is arbitrary
    //You can use whatever file extension You like
    //or not put an extension at all
    private const string _fileNameForPersistentData = "/whatEverNameIwant.dat";
                                                                           
               
    //BEGIN OF Awake() FUNCTION
    void Awake()
    {
        array = new int[10];

        Load();
    }
    //END OF Awake() FUNCTION


    //BEGIN OF OnGUI() FUNCTION
    void OnGUI()
    {
        int i = 0;

        if( GUI.Button( new Rect( 10, 10, 250, 100 ), str
                                                      + " = "
                                                      + num.ToString()
                                                      +  " array = "
                                                      + array[i].Tostring() ) )
        {
            num++;
 
            str = "numFromStr";
    
            for( ; i < 10; i++ )
            {
                 array[i] =  ( num * 2 );
            }
 
            Save();//call Save() to persistent store this value
        }


        if( GUI.Button( new Rect( 10, 130, 250, 100 ), "Reset num" ) )
        {
            InitializePersistentData();
        }
    }
    //END OF OnGUI() FUNCTION


    //BEGIN OF InitializePersistentData FUNCTION
    public void InitializePersistentData()
    {
        num = 0;

        str = "";

        for( int i = 0; i < 10; i++ )
        {
              array[i] = 0;
        }
 
    }
    //END OF InitializePersistentData FUNCTION


    //BEGIN OF REGION Save() Load()
    #region Save() Load()
    //BEGIN OF Save() FUNCTION
    public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Create( Application.persistentDataPath
                                                      + _fileNameForPersistentData );

        GameData data = new GameData();

        data.numToSerialize = num;

        data.strToSerialize = str;

        data.arrayToSerialize = new int[10];

        for( int i = 0; i < 10; i++ )
        {
             data.arrayToSerialize[i] = array[i];
        }

        bf.Serialize( file, data );

        file.Close();
    }
    //END OF Save() FUNCTION


    //BEGIN OF Load() FUNCTION
    public void Load()
    {
        if( !File.Exists( Application.persistentDataPath
                               + _fileNameForPersistentData ) )
        {
            InitializePersistentData();
        }

        if( File.Exists( Application.persistentDataPath
                              + _fileNameForPersistentData ) )
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open( Application.persistentDataPath
                                                        + _fileNameForPersistentData, FileMode.Open );
 
            GameData data = ( GameData )bf.Deserialize( file );
 
            file.Close();
 
            num = data.numToSerialize;
 
            str = data.strToSerialize;
    
            for( int i = 0; i < 10; i++ )
            {
                 array[i] = data.arrayToSerialize[i];
            }
        }
    }
    //END OF Load() FUNCTION
    #endregion Save() Load()
    //END OF REGION Save() Load()

}
//END OF Test CLASS


//BEGIN OF [Serializable] GameData CLASS
[Serializable]
class GameData
{
    public int numToSerialize;

    [OptionalField]
    public string strToSerialize;

    [OptionalField]
    public int[] arrayToSerialize;


    //BEGIN OF NumToSerializeToSerialize PROPERTY
    int NumToSerialize
    {
        set
        {
            numToSerialize = 0;
        }
    }
    //END OF NumToSerializeToSerialize PROPERTY


    //BEGIN OF StrToSerialize PROPERTY
    string StrToSerialize
    {
        set
        {
            strToSerialize = "";
        }
    }
    //END OF StrToSerialize PROPERTY


    //BEGIN OF ArrayToSerialize PROPERTY
    int[] ArrayToSerialize
    {
        set
        {
            arrayToSerialize = new int[10];
        }
    }
    //END OF ArrayToSerialize PROPERTY

}
//END OF [Serializable] GameData CLASS

“I added an int[ ] array, everything works but [OptionalField] on that array.”

Are you getting an error? What happens?

Hi Mike!
Yes, I’ve got null ref etc. Error 'cause deserialize does not find the new array in the binary and does not ignore it even if it is signed as [OptionalField].
It happens on every new field as long as it is an array, no matter which type of course.
Any help will be appreciated!
Thanks :slight_smile:

You can do version control of the GameData class(f.e. GameData00, GameData01, …)

Depending on the (Update) version of the program, you can access various types of variables.

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;//required by [OptionalField]

#region Versions-Updates GameData

[Serializable]
public class GameData00{
  
    public int numToSerialize;

}

[Serializable]
public class GameData01 : GameData00{

    //New variable added
    public string strToSerialize;

}

[Serializable]
public class GameData02 : GameData01{

    //New variable added
    public int[] arrayToSerialize;

}
#endregion

public class Test : MonoBehaviour {

    public bool ResetInit = false;
    public enum Version{_00,_01,_02};

    public Version version = Version._00;
    public int num;
    public string str;
    public int[] array = new int[10]; //Fixed length array
      
    private string _fileNameForPersistentData = "/whatEverNameIwant.dat";
      
    void Awake() {

        if(ResetInit) DeletePersistentData(); //Reset GameData type file
        Load();
  
    }

    void OnGUI()
    {

        if( GUI.Button( new Rect( 10, 10, 250, 100 ), "Save GameData" ) ) Save();//call Save() to persistent store this value
        if( GUI.Button( new Rect( 10, 130, 250, 100 ), "Delete GameData" ) ) DeletePersistentData();
      
    }
  
    void InitializePersistentData()
    {
        num = 0;          
        str = "";          
        for( int i = 0; i < 10; i++ ) array[i] = 0;
          
    }

    void DeletePersistentData(){

        if( File.Exists( Application.persistentDataPath + _fileNameForPersistentData ) ){

            File.Delete(Application.persistentDataPath + _fileNameForPersistentData);
            InitializePersistentData();

        }

    }
      

    #region Save() Load()
    //BEGIN OF Save() FUNCTION
    public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Create( Application.persistentDataPath
                                      + _fileNameForPersistentData );
          
        switch (version){

        case Version._00:

            GameData00 gameData00 = new GameData00();
            gameData00.numToSerialize = this.num;
            bf.Serialize( file, gameData00 );
            break;

        case Version._01:

            GameData01 gameData01 = new GameData01();
            gameData01.numToSerialize = this.num;
            gameData01.strToSerialize = this.str;
            bf.Serialize( file, gameData01 );
            break;

        case Version._02:

            GameData02 gameData02 = new GameData02();
            gameData02.numToSerialize = this.num;
            gameData02.strToSerialize = this.str;
            gameData02.arrayToSerialize = this.array;
            bf.Serialize( file, gameData02 );
            break;

        }          

        file.Close();
    }
    //END OF Save() FUNCTION
      
      
    //BEGIN OF Load() FUNCTION
    void Load()
    {
        if( !File.Exists( Application.persistentDataPath + _fileNameForPersistentData ) ) InitializePersistentData();
        else {

            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open( Application.persistentDataPath
                                        + _fileNameForPersistentData, FileMode.Open );

            System.Object data = bf.Deserialize(file);

            if(data is GameData00){

                GameData00 gameData00 = (GameData00)data;
                this.num = gameData00.numToSerialize;

            } else if(data is GameData01){

                GameData01 gameData01 = (GameData01)data;
                this.num = gameData01.numToSerialize;
                this.str = gameData01.strToSerialize;

            } else if(data is GameData02){

                GameData02 gameData02 = (GameData02)data;
                this.num = gameData02.numToSerialize;
                this.str = gameData02.strToSerialize;
                this.array = gameData02.arrayToSerialize;
            }

            file.Close();

        }
    }

    //END OF Load() FUNCTION
    #endregion Save() Load()

}

Download link.

Thank you.
Very interesting approach, but unfortunately it seems to save only the first variable: int num, no matter which version you choose in the Inspector.
I made a couple of changes in your code in order to get a little more verbose OnGUI button ( line 55-61 ),
and Debug.Log in Save() and Load() just to see if it load and save the right version ( and it does! )

Thanks :slight_smile:

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;//required by [OptionalField]

#region Versions-Updates GameData

[Serializable]
public class GameData00{

    public int numToSerialize;

}

[Serializable]
public class GameData01 : GameData00{

    //New variable added
    public string strToSerialize;

}

[Serializable]
public class GameData02 : GameData01{

    //New variable added
    public int[] arrayToSerialize;

}
#endregion

public class Test : MonoBehaviour {

    public bool ResetInit = false;
    public enum Version{_00,_01,_02};

    public Version version = Version._00;
    public int num;
    public string str;
    public int[] array = new int[10]; //Fixed length array
    
    private string _fileNameForPersistentData = "/whatEverNameIwant.dat";
    
    void Awake() {

        if(ResetInit) DeletePersistentData(); //Reset GameData type file
        Load();

    }

    void OnGUI()
    {
        if( GUI.Button( new Rect( 10, 10, 250, 100 ), "save " + str + " + " + num ) )
        {
            num++;
            str = "TEST";
            for( int i = 0; i < 10; i++ ) array[i] = i;
            Save();//call Save() to persistent store this value
        }

        if( GUI.Button( new Rect( 10, 130, 250, 100 ), "Delete GameData" ) ) DeletePersistentData();
    
    }

    void InitializePersistentData()
    {
        num = 0;        
        str = "";        
        for( int i = 0; i < 10; i++ ) array[i] = 0;
        
    }

    void DeletePersistentData(){

        if( File.Exists( Application.persistentDataPath + _fileNameForPersistentData ) ){

            File.Delete(Application.persistentDataPath + _fileNameForPersistentData);
            InitializePersistentData();

        }

    }
    

    #region Save() Load()
    //BEGIN OF Save() FUNCTION
    public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Create( Application.persistentDataPath
                                      + _fileNameForPersistentData );
        
        switch (version){

        case Version._00:
                
            GameData00 gameData00 = new GameData00();
            gameData00.numToSerialize = this.num;
            bf.Serialize( file, gameData00 );
            break;

        case Version._01:
                
            GameData01 gameData01 = new GameData01();
            gameData01.numToSerialize = this.num;
            gameData01.strToSerialize = this.str;
            bf.Serialize( file, gameData01 );
            break;

        case Version._02:
              
            GameData02 gameData02 = new GameData02();
            gameData02.numToSerialize = this.num;
            gameData02.strToSerialize = this.str;
            gameData02.arrayToSerialize = this.array;
            bf.Serialize( file, gameData02 );
            break;

        }        

        file.Close();
        
        Debug.Log ( "saving version = " + version );
    }
    //END OF Save() FUNCTION
    
    
    //BEGIN OF Load() FUNCTION
    void Load()
    {
        if( !File.Exists( Application.persistentDataPath + _fileNameForPersistentData ) ) InitializePersistentData();
        else {

            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open( Application.persistentDataPath
                                        + _fileNameForPersistentData, FileMode.Open );

            System.Object data = bf.Deserialize(file);

            if(data is GameData00){
                
                GameData00 gameData00 = (GameData00)data;
                this.num = gameData00.numToSerialize;

            }
            else if(data is GameData01){
                    
                GameData01 gameData01 = (GameData01)data;
                this.num = gameData01.numToSerialize;
                this.str = gameData01.strToSerialize;

            }
            else if(data is GameData02){
                   
                GameData02 gameData02 = (GameData02)data;
                this.num = gameData02.numToSerialize;
                this.str = gameData02.strToSerialize;
                this.array = gameData02.arrayToSerialize;
            }

            file.Close();
            
            Debug.Log ( "loading version = " + version );
        }
    }

    //END OF Load() FUNCTION
    #endregion Save() Load()

}