0
\$\begingroup\$

Unity3D has a class for platform-independent data storage called PlayerPrefs. Supported data types: int, float and string can be retrieved and set using static methods, but no bool support exists, and you have to manually call Save() to make sure value changes persist.

I wrote a little helper class to cut down on the the boilerplate, and also add support for bool and enum datatypes. I'm looking for a general critique on style and efficiency, there's a fair amount of duplication, but I'm not sure how I can reduce that.

/// <summary>
/// Helper class that simplifies common PlayerPrefs getting and setting.
/// </summary>
public static class PlayerPrefsHelper
{

    /// <summary>
    /// Gets an int value from PlayerPrefs
    /// </summary>
    /// <remarks>
    /// This method has a side effect of creating a PlayerPrefs entry for the given key if one does not exist.
    /// </remarks>
    /// <param name="key">The key.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The int value stored in PlayerPrefs for the given key, or defaultValue if no such key exists</returns>
    public static int GetInt(string key, int defaultValue)
    {
        if (!PlayerPrefs.HasKey(key))
        {
            SetInt(key, defaultValue);
        }

        return PlayerPrefs.GetInt(key);
    }

    /// <summary>
    /// Sets an int value on PlayerPrefs
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    public static void SetInt(string key, int value)
    {
        PlayerPrefs.SetInt(key, value);
        PlayerPrefs.Save();
    }

    /// <summary>
    /// Gets a bool value from PlayerPrefs.
    /// </summary>
    /// <remarks>
    /// This method has a side effect of creating a PlayerPrefs entry for the given key if one does not exist.
    /// The value will be converted from an integer value, because PlayerPrefs does not support boolean values. 0 will be converted to false, all other values will be converted to true.
    /// </remarks>
    /// <param name="key">The key.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The bool value stored in PlayerPrefs for the given key, or defaultValue if no such key exists</returns>
    public static bool GetBool(string key, bool defaultValue)
    {
        return GetInt(key, defaultValue ? 1 : 0) != 0;
    }

    /// <summary>
    /// Sets a bool value on PlayerPrefs.
    /// </summary>
    /// <remarks>
    /// The value will be stored as an integer, because PlayerPrefs does not support boolean values. True will be stored as 1, false as 0.
    /// </remarks>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    public static void SetBool(string key, bool value)
    {
        SetInt(key, value ? 1 : 0);
    }

    /// <summary>
    /// Gets a float value from PlayerPrefs
    /// </summary>
    /// <remarks>
    /// This method has a side effect of creating a PlayerPrefs entry for the given key if one does not exist.
    /// </remarks>
    /// <param name="key">The key.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The float value stored in PlayerPrefs for the given key, or defaultValue if no such key exists</returns>
    public static float GetFloat(string key, float defaultValue)
    {
        if (!PlayerPrefs.HasKey(key))
        {
            SetFloat(key, defaultValue);
        }

        return PlayerPrefs.GetFloat(key);
    }

    /// <summary>
    /// Sets a float value on PlayerPrefs
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    public static void SetFloat(string key, float value)
    {
        PlayerPrefs.SetFloat(key, value);
        PlayerPrefs.Save();
    }

    /// <summary>
    /// Gets a string value from PlayerPrefs
    /// </summary>
    /// <remarks>
    /// This method has a side effect of creating a PlayerPrefs entry for the given key if one does not exist.
    /// </remarks>
    /// <param name="key">The key.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The string value stored in PlayerPrefs for the given key, or defaultValue if no such key exists</returns>
    public static string GetString(string key, string defaultValue)
    {
        if (!PlayerPrefs.HasKey(key))
        {
            SetString(key, defaultValue);
        }

        return PlayerPrefs.GetString(key);
    }

    /// <summary>
    /// Sets a string value on PlayerPrefs
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    public static void SetString(string key, string value)
    {
        PlayerPrefs.SetString(key, value);
        PlayerPrefs.Save();
    }

    /// <summary>
    /// Gets a value from PlayerPrefs as enumerated type T.
    /// </summary>
    /// <remarks>
    /// This method has a side effect of creating a PlayerPrefs entry for the given key if one does not exist.
    /// </remarks>
    /// <typeparam name="T">The type of enum expected.</typeparam>
    /// <param name="key">The key.</param>
    /// <param name="defaultValue">The default value if no PlayerPrefs setting exists for the key.</param>
    /// <returns>The value stored in PlayerPrefs for the given key, converted to type T, or defaultValue if no such key exists</returns>
    /// <exception cref="ArgumentException">T must be an enumerated type</exception>
    /// <exception cref="InvalidOperationException">Cannot get an enum that doesn't have underlying type Int32.</exception>
    public static T GetEnum<T>(string key, T defaultValue) where T : struct, IConvertible
    {
        var enumType = typeof(T);

        if (!enumType.IsEnum)
        {
            throw new ArgumentException("T must be an enumerated type");
        }

        if (Enum.GetUnderlyingType(enumType) == typeof(int))
        {
            var value = GetInt(key, defaultValue.ToInt32(CultureInfo.InvariantCulture));
            return (T)Enum.ToObject(enumType, value);
        }

        throw new InvalidOperationException("Cannot get an enum that doesn't have underlying type Int32.");
    }

    /// <summary>
    /// Sets an int value on PlayerPrefs based on an enumerated value.
    /// </summary>
    /// <typeparam name="T">The type of enum.</typeparam>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    /// <exception cref="ArgumentException">T must be an enumerated type</exception>
    /// <exception cref="InvalidOperationException">Cannot store an enum that doesn't have underlying type Int32.</exception>
    public static void SetEnum<T>(string key, T value) where T : struct, IConvertible
    {
        var enumType = typeof(T);

        if (!enumType.IsEnum)
        {
            throw new ArgumentException("T must be an enumerated type");
        }

        if (Enum.GetUnderlyingType(enumType) == typeof(int))
        {
            SetInt(key, value.ToInt32(CultureInfo.InvariantCulture));
        }

        throw new InvalidOperationException("Cannot store an enum that doesn't have underlying type Int32.");
    }
}
\$\endgroup\$
2
\$\begingroup\$

Get*() methods

Checking if preference is exist and creating it is not necessary. You can get the same (from game's perspective) behavior by shorter code:

public static int GetInt(string key, int defaultValue)
{
    return PlayerPrefs.GetInt(key, defaultValue);
}

Entries won't be created on getting anymore, obviously, but you probably don't need such behavior at all, since you'll likely interact with Preferences only through PlayerPrefs and PlayerPrefsHelper classes.

Duplication

Since you want to reduce duplication, here are two ways I see to do it:

*Enum() duplication

Why not extract same code to separate method?

public static void SetEnum<T>(string key, T value) where T : struct, IConvertible
{
    CheckEnum<T>();
    SetInt(key, value.ToInt32(CultureInfo.InvariantCulture));
}

private static void CheckEnum<T>()
{
    var enumType = typeof(T);

    if (!enumType.IsEnum)
    {
        throw new ArgumentException("T must be an enumerated type");
    }

    if (Enum.GetUnderlyingType(enumType) != typeof(int))
    {
        throw new InvalidOperationException("Cannot store an enum that doesn't have underlying type Int32.");
    }
}

Save() duplication

public static void SetInt(string key, int value)
{
    DoAndSave(() => PlayerPrefs.GetInt(key, value));
}

private static void DoAndSave(Action action)
{
    action();
    PlayerPrefs.Save();
}

I'm not sure if it's worth it though.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the advice! As for the creating a key if it doesn't exist bit, that was actually my main motivation for creating this helper class, since that way I can define defaults at the start and not have to program in hard defaults in every script that access a particular key. \$\endgroup\$
    – Nick Udell
    May 11 '15 at 8:56
  • 1
    \$\begingroup\$ But all your methods require a default value as the second parameter, don't they? \$\endgroup\$ May 11 '15 at 12:39
  • \$\begingroup\$ Ah good point, I don't think I've thought this design all the way through... \$\endgroup\$
    – Nick Udell
    May 11 '15 at 13:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.