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
-
Concealment:
- Use nested folder structures to reduce name recognizability
- Not applicible to PlayerPrefs as its location is fixed
-
Obfuscation:
- Encrypt both keys and values
-
Complexity:
- Prevent reverse engineering of encryption algorithms
Package Generation
Right-click on PersistentDataManager and select "Export package".