Smart user data management

There is many ways to work with user data. The basic method is to use a function for getting and posting data in the place you need it. You probably did it in your development beginnings, typically in a case of a game working with PlayerPrefs only. In that case, it was very effective for you, because PlayerPrefs api work on all devices and you had no interest about anything more - you did not think about adding online saving in the future, you did not care security, and saved data formats were simple. This article will try show you a better way.

Data groups

For simple orientation in the data, there is good to have them grouped under the object they are designed for. E.g. data related to character, data related to a car and so on.

Next step is solve how to simply transform these data between game and storage. Solving all single data separately gives big space for potential bugs from a side of inattention (You can have a typo in a key under which you save the data and so on). If you do this on more places (in more classes), the risk is increased. Also code becomes messy.

Our tip is to solve this within one dedicated class designed for the purpose of saving / getting data only.

Single class designed for data management

Separated space for management of all data provides advantage of simple and effective managament from one place. This reduces nuber of potential bugs from inattention and provide simple implementation of additional functionality as adding a security layer (auto encryption / decryption) and logic of where and under what format and structure the data will be saved.
NOTE: By using GameArter SDK, there is no need to solve security and targeted storage. It is solved automatically by SDK on the background.

This class has to be available in all scenes (either it can be static class or can be attached on DoNotDestroyOnLoad) and contain public functions by calling which there is possible to get and post data. Via additional specification, such function can be designed to take all data or only selected ones. The selection can be made by e.g. arguments. For better readibility, there is possible to have separated section for every data group. Data to post should not be received via arguments, but got directly. By this mechanism, this class will become less independent for changes.

Huge benefit of this is that all logic regarding saving is out of core-game code. Here's example of game-core code vs data management class.

copy


public static class Character : MonoBehaviour { 

	public int hunger;
	public int skill;
	public int fatigue;

	private void Cook(){
		skill ++;
		fatigue ++;
		hunger --;
		DataManagementClass._Inst.SaveAll(); // 1 call - 17 lines of logic
	}
}

public static class FridgeStock : MonoBehaviour { 
	public int meats;
	
	private void Durability(){}
}

public static class DataManagementClass : MonoBehaviour { 

	public void SaveAll(){ 
		int hunger = Character._Inst.hunger; // get hunger state
		int skill = Character._Inst.hunger; // get skill state
		int fatigue = Character._Inst.fatigue; // get skill state
		int meats = FridgeStock.Inst.meats; // get fridge stocks

		// save logic
		if(GameArter){
			Garter.I.PostData< int >("hunger", hunger); // encrypted and storage selected automatically by SDK
			Garter.I.PostData< int >("skill", skill); // encrypted and storage selected automatically by SDK
			Garter.I.PostData< int >("fatigue", fatigue); // encrypted and storage selected automatically by SDK
			Garter.I.PostData< int >("meats", meats); // encrypted and storage selected automatically by SDK
		} else if(GooglePlay){
			if(onlineStorage){
				//
			} else {
				PlayerPrefs.Set("hunger", EncryptData(hunger));
				PlayerPrefs.Set("skill", EncryptData(skill));
				PlayerPrefs.Set("fatigue", EncryptData(fatigue));
				PlayerPrefs.Set("meats", EncryptData(meats));
			}
		}
	}

	private string EncryptData(){
		return ...
	}

	private string DecryptData(){
		return ...
	}
}

Example above displayes only posting of simple data types. This can becoem messy if you have many objects in the game. Although you can say, you will save some transaction data by posting only really changed values, you will soonly find out, that it is not effective in a case of data nor productivity. On data, you will save few kilobytes comparing to tens of megabytes which was cost of downlading the game. The ideal datarpoductivity ratio is to have few keys with accessed more complex data types. Example: (Notice that keys "hunger","skill" and "fatigue" were replaced for key "character" containing all the properties.)

copy


// classes Character and FridgeStock remain same

public static class DataManagementClass : MonoBehaviour { 

	// Definition of own data structure for character
	[System.Serializable]
	private internal class CharacterData(){
		public int hunger;
		public int skill;
		public int fatigue;
	}

	public void SaveAll(){ 
		CharacterData chd = new CharacterData();
		chd.hunger = Character._Inst.hunger; // get hunger state
		ch.skill = Character._Inst.hunger; // get skill state
		ch.fatigue = Character._Inst.fatigue; // get skill state
		
		int meats = FridgeStock.Inst.meats; // get fridge stocks

		// save logic
		if(GameArter){
			string CharacterDataString = Garter.I.ToJson(chd);
			Garter.I.PostData< string >("character", CharacterDataString); // encrypted and storage selected automatically by SDK
			Garter.I.PostData< int >("meats", meats); // encrypted and storage selected automatically by SDK
		} else if(GooglePlay){
			if(onlineStorage){
				//
			} else {
				PlayerPrefs.Set("character", EncryptData(ConverDataToString(chd)));
				PlayerPrefs.Set("meats", EncryptData(meats));
			}
		}
	}

	private void ConverDataToString(){
		...
	}

	private string EncryptData(){
		return ...
	}

	private string DecryptData(){
		return ...
	}
}

Can you explain more the internal class "CharacterData"?

The purpose of the class is "collect" more data types into one "single data type". C# is object-oriented language and uses classes for an option of serialization and deserialization of the associated data for move between game and server. Using classes for provide a necessary freedom to customize data class exactly on basis of needs. You can use also more complex structures as lists, arrays or even other nested classes. See example of more complex internal class designed for data management.

copy

public static class DataManagementClass : MonoBehaviour { 

	[System.Serializable]
    internal class JsonStructure
    {
        public string someStringData;
        public int someIntegerData;
        public float someFloatData;
        public MyTypeData someOwnTypeData; // nested class someOwnTypeData
        public JsonStructure(string stringData, int integerData, float floatData, MyTypeData ownTypeData)
        {
            this.someStringData = stringData;
            this.someIntegerData = integerData;
            this.someFloatData = floatData;
            this.someOwnTypeData = ownTypeData;
        }

        [System.Serializable]
        internal class MyTypeData
        {
            public List< byte > someListType;
            public bool[] someArrayType;
            public ulong[][] someJaggedArrayType;
            public MyTypeData(List< byte > list, bool[] array, ulong[][] jaggedArray)
            {
                this.someListType = list;
                this.someArrayType = array;
                this.someJaggedArrayType = jaggedArray;
            }
        }
    }
}

Let's look now how to "fill" the class with data and get the data back from the class. Because of this is something whic is usually done befor posting / after getting data from online storage, it will be attached in functions PostData() and GetData().

copy

public static class DataManagementClass : MonoBehaviour { 
	// data we will fill
	public string someStringData = "Some String Data";
    public int someIntegerData = 8;
    public float someFloatData = 1.3f;
    public List< byte > someListType = new List< byte >(5) { 0, 1, 2, 3, 4 };
    public bool[] someArrayType = new bool[] { true, false };
    public ulong[][] someJaggedArrayType = new ulong[2][] { new ulong[] { 0, 1, 2, 3 }, new ulong[] { 4, 5, 6 } }; // array composted of arrays
	
	public void PostData(){
		JsonStructure complexData = new JsonStructure (someStringData, someIntegerData, someFloatData, new JsonStructure.MyTypeData (someListType, someArrayType, someJaggedArrayType));
		string complexDataAsString = Garter.I.ToJson(complexData);
		Garter.I.PostData< string >("complexData", complexDataAsString, (error, response) => { // string (due to compatibility with saving in playerprefs)
			if (!string.IsNullOrEmpty (error)) {
				Debug.LogWarning (error);
			} else {
				Debug.Log (response);
			}
		});
	}

	public void GetData(){
		JsonStructure myComplexData = Garter.I.GetData< JsonStructure >("complexData",(error,value) => { // is deserialized automatically to JsonStructure
			if(!string.IsNullOrEmpty(error)){
				Debug.Log ("myComplexData callback - error: " + error);
			} else {
				someStringData = value.someStringData;
				someJaggedArrayType = value.someOwnTypeData.someJaggedArrayType;
			}
		});
	}
}

Note:
If you check typeof myComplexData, you will get "JsonStructure". After posting it to GameArter storage, it will still remain type of "JsonStructure" which is very effective for possibility of filltration in the data on server side. But:
If you do not use GameArter, you can notice, that you are not able to save "JsonStructure" data type. Do not worry, you can save it as a string. On basis of the class, there is possible to convert it to as well as from string json-based format. Here's a guide for it:

copy

public static class DataManagementClass : MonoBehaviour { 

	public string someStringData = "Some String Data";
    public int someIntegerData = 8;
    public float someFloatData = 1.3f;
    public List< byte > someListType = new List< byte >(5) { 0, 1, 2, 3, 4 };
    public bool[] someArrayType = new bool[] { true, false };
    public ulong[][] someJaggedArrayType = new ulong[2][] { new ulong[] { 0, 1, 2, 3 }, new ulong[] { 4, 5, 6 } }; // array composted of arrays
	
	public void SomeFunction(){
		JsonStructure complexData = new JsonStructure (someStringData, someIntegerData, someFloatData, new JsonStructure.MyTypeData (someListType, someArrayType, someJaggedArrayType));
		// Convert complexData to string
		string complexDataAsString = Garter.I.ToJson(complexData);
		// Convert string back to JsonStructure
		JsonStructure complexDataAscomplexData = Garter.I.FromJson< JsonStructure >(complexDataAsString);
	}
}

Hope you found this article useful. If anything unclear, let us know in GameArter community Slack.