Search code examples
c#unity-game-engineregistry

How does Unity save data in PlayerPrefs?


I want to read data from the registry in my standalone WPF application, which is created inside a game on Unity using PlayerPrefs. The question is how to read and write float to the registry as DWORD32.

public static float Int64ToFloat(long value)
{
    byte[] bytes = BitConverter.GetBytes(value);
    byte[] floatBytes = new byte[4];
        
    Array.Copy(bytes, floatBytes, 4);

    if (BitConverter.IsLittleEndian)
                Array.Reverse(floatBytes);

    float floatValue = BitConverter.ToSingle(floatBytes, 0);

    return floatValue;
}

This code returns extremely small values, such as 1.35E-43.


Solution

  • Unity does not save a float but rather a double! It is indeed quite a hack but if you look closely you will find that your DWORD (32bit) contains indeed a 64-bit long! (no clue why they didn't properly go for QWORD or directly binary there)

    As mentioned already for floating point types you can't simply cut the bytes and expect the value to remain the same as you could do with long -> int.

    You can simply use

    // Have in mind that the key here is NOT the one you use in Unity but also contains a certain trailing hash string
    public static float GetFloat(string key, float fallback = 0f)
    {
        using (RegistryKey registryKey = Registry.CurrentUser.OpenSubKey(@"Software\Unity\UnityEditor\" + Application.companyName + "\\" + Application.productName))
        {
            var value = registryKey?.GetValue(key);
            if (value is long longValue)
            {                      
                // Convert the 64-bit value to a double
                var doubleValue = BitConverter.Int64BitsToDouble(longValue);
                // then simply cast to cut its precision down
                var floatValue = (float)doubleValue;
                return floatValue;
            }
        }
    
        return fallback;
    }
    

    For you to verify

    var input = 12345678901234567890.1234567890f;
    PlayerPrefs.SetFloat("TestFloat", input);
    var output = GetFloat("TestFloat_h1435782019");
    Assert.AreApproximatelyEqual(input,output, "Value retrieved from registry does not match the value stored in PlayerPrefs.");
    

    I just also stumbled over this snippet

    If we can trust it the hash string is basically calculated as

    public class PlayerPrefsHash
    {
      // Returns the string property name Unity would generate for a player prefs registry key value 
      // e.g., "PlayerGold" -> "PlayerGold_hXXXXXXXXXXXXXX"
      public static string Hash(string name)
      {
        uint hash = 5381;
        foreach (char c in name)
          hash = hash * 33 ^ c;
        string key = name + "_h" + hash;
        return key;
      }
    }
    

    So you could use this in my method above and be able to use the same key constant on Unity as in WPF side.

    And at least in my quick test it seams to be accurate

    var key = PlayerPrefsHash.Hash("TestFloat");
    Assert.AreEqual("TestFloat_h1435782019", key, "PlayerPrefsHash.Hash does not match PlayerPrefs generated hash.");