Implementing a PlayerPrefs Manager with Reflection in Unity

Reflection Knowledge Primer

Determining if one type can be assigned to another (inheritance relationships):

public class ParentClass
{

}
public class ChildClass : ParentClass
{

}
...
Type parentType = typeof(ParentClass);
Type childType = typeof(ChildClass);
// Check if the specified type can be assigned to this type
if(parentType.IsAssignableFrom(childType))
{
    Debug.Log("Assignment possible");
    ParentClass parent = Activator.CreateInstance(childType) as ParentClass;
    Debug.Log(parent);
}
else
{
    Debug.Log("Assignment not possible");
}

Obtaining generic types through reflection

List<string> stringList = new List<string>();
Type listType = stringList.GetType();
Type[] genericArguments = listType.GetGenericArguments();
Debug.Log(genericArguments[0]);
Dictionary<int,string> intStringDict = new Dictionary<int,string>();
Type dictType = intStringDict.GetType();
genericArguments = dictType.GetGenericArguments();
Debug.Log(genericArguments[0]);
Debug.Log(genericArguments[1]);

Requirements Analysis

Project Implementation

Basic Class Structure (Singleton Pattern)

public class PersistentDataManager
{
    private static PersistentDataManager instance = new PersistentDataManager();
    public static PersistentDataManager Instance
    {
        get { return instance; }
    }
    private PersistentDataManager()
    {

    }

}

Storage Function

public void StoreObject(object dataObject, string identifier)//First parameter is the data object to store, second is its unique identifier
{

}

Retrieval Function

public object RetrieveData(Type dataType, string identifier)
{
    // Using Type instead of object parameter
    // This saves a line of code externally
    // If using an object parameter for modification, an object must be created externally first
    // Now we just pass a Type typeof(Player) and internally create and return an object
    return null;
}

Storing Data with Reflection

Storing Common Data Types with Reflection

Defining PlayerInfo class and PersistentDataManager usage

public class PlayerProfile
{
    public int age=10;
    public string characterName="TestPlayer";
    public float height=1.2f;
    public bool isMale = true;
}
void Start()
{
    PlayerProfile profile = new PlayerProfile();
    PersistentDataManager.Instance.StoreObject(profile, "PlayerProfile"); 
}


Core Implementation Functions

    public void StoreObject(object dataObject, string identifier)
    {
        Type objectType = dataObject.GetType();
        FieldInfo[] fields = objectType.GetFields();
        for(int i = 0; i < fields.Length; i++)
        {
            Debug.Log(fields[i]);
        }
        string storageKey;
        FieldInfo currentField;
        for (int i = 0; i < fields.Length; i++)
        {
            currentField = fields[i];
            storageKey = identifier + "_" + objectType.Name + "_" + currentField.FieldType.Name + "_" + currentField.Name;

            Debug.Log(storageKey);
            StoreFieldValue(currentField.GetValue(dataObject), storageKey);
        }
    }
    private void StoreFieldValue(object value, string key)
    {
         // Different APIs based on data type
         Type valueType = value.GetType();

        if (valueType == typeof(int))
        {
            Debug.Log("Storing int " + key);
            PlayerPrefs.SetInt(key, (int)value);
        }else if(valueType == typeof(float))
        {
            Debug.Log("Storing float " + key);
            PlayerPrefs.SetFloat(key, (float)value);
        }else if (valueType == typeof(string))
        {
            Debug.Log("Storing string " + key);
            PlayerPrefs.SetString(key, value.ToString());
        }else if( valueType == typeof(bool))
        {
            Debug.Log("Storing bool " + key);
            PlayerPrefs.SetInt(key, (bool)value ? 1 : 0);
        }
    }

Storing List Data Types with Reflection

List implements IList, and IList is non-generic, allowing us to use the inheritance check function to determine if it's a subclass of IList

else if(typeof(IList).IsAssignableFrom(valueType))
{
    IList list = value as IList;
    PlayerPrefs.SetInt(key, list.Count);
    int index = 0;
    foreach (object item in list)
    {
        StoreFieldValue(item, key + index);
        index++;
    }
}

Storing Dictionary Data Types with Reflection

Similar to list handling

else if (typeof(IDictionary).IsAssignableFrom(valueType))
{
    IDictionary dictionary = value as IDictionary;
    PlayerPrefs.SetInt(key, dictionary.Count);
        int index = 0;
    foreach(object dictKey in dictionary.Keys)
    {
        StoreFieldValue(dictKey, key + "_key_" + index);
        StoreFieldValue(dictionary[dictKey], key + "_value_" + index);
        index++;
    }
}

Storing Custom Class Members with Reflection
else
{
    StoreObject(value, key);
}

This calls the parent function, which can further decompose this data type for storage

Example

public class ItemData
{
    public int id;
    public int quantity;
    public ItemData()
    {

    }
    public ItemData(int id, int quantity)
    {
        this.id = id;
        this.quantity = quantity;

    }
}
class ...{
    public List<ItemData> inventory = new List<ItemData>() { new ItemData(1, 10), new         ItemData(2, 20) };
    public Dictionary<int ,ItemData> itemDictionary = new Dictionary<int, ItemData>() 
    {
    {3,new ItemData(3,30) },
        {4,new ItemData(4,40) }
    };
}

This will recursively store the List and Dictionary components of the class

Retrieving Data with Reflection

Retrieving Common Data Types with Reflection
private object RetrieveFieldValue(Type fieldType, string key)
{
    // Use appropriate API based on field type
    if(fieldType == typeof(int))
    {
        return PlayerPrefs.GetInt(key,0);
    }else if(fieldType == typeof(float))
    {
        return PlayerPrefs.GetFloat(key, 0);
    }
    else if( fieldType == typeof(string)) 
    {
        return PlayerPrefs.GetString(key, "");
    }
    else if(fieldType == typeof(bool))
    {
        return PlayerPrefs.GetInt(key, 0) == 1 ? true : false;
    }
    return null;
}

Retrieving List Datta Types with Reflection

Utilizes the function for obtaining generic type information

else if (typeof(IList).IsAssignableFrom(fieldType))
        {
            int count = PlayerPrefs.GetInt(key, 0);
            IList list = Activator.CreateInstance(fieldType) as IList;
            for(int i = 0; i < count; i++)
            {
                list.Add(RetrieveFieldValue(fieldType.GetGenericArguments()[0], key + i));
            }
            return list;
        }

Retrieving Dictionary Data Types with Reflection
else if (typeof(IDictionary).IsAssignableFrom(fieldType))
        {
            int count = PlayerPrefs.GetInt(key, 0);
            IDictionary dictionary = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] genericTypes = fieldType.GetGenericArguments();
            for (int i=0; i < count; i++)
            {
                dictionary.Add(RetrieveFieldValue(genericTypes[0], key +"_key_"+ i), 
                              RetrieveFieldValue(genericTypes[1], key + "_value_" + i));
            }
            return dictionary;
        }

Retrieving Custom Data Types with Reflection
else
{
    return RetrieveData(fieldType, key);
}

Final Testing

First, delete previously saved data

PlayerPrefs.DeleteAll();

Writing data:

PlayerProfile profile = PersistentDataManager.Instance.RetrieveData(typeof(PlayerProfile), "PlayerProfile") as PlayerProfile;
profile.age = 14;
profile.characterName = "TestPlayer";
profile.height = 100;
profile.isMale = true;
profile.inventory.Add(new ItemData(1,99));
profile.inventory.Add(new ItemData(2, 199));

profile.itemDictionary.Add(3, new ItemData(3, 1));
profile.itemDictionary.Add(4, new ItemData(4, 2));
PersistentDataManager.Instance.StoreObject(profile, "PlayerProfile");

On first execution, the loaded profile will be empty. After storing the data, subsequent executions will successfully retrieve the stored information.

Ancryption Approaches

  1. Concealment:

    • Use nested folder structures to reduce name recognizability
    • Not applicible to PlayerPrefs as its location is fixed
  2. Obfuscation:

    • Encrypt both keys and values
  3. Complexity:

    • Prevent reverse engineering of encryption algorithms

Package Generation

Right-click on PersistentDataManager and select "Export package".

Tags: unity PlayerPrefs reflection singleton data persistence

Posted on Mon, 02 Nov 2026 16:48:51 +0000 by 4rxsid