Jump to content

  • Log In with Google      Sign In   
  • Create Account

Calling all IT Pros from Canada and Australia.. we need your help! Support our site by taking a quick sponsored surveyand win a chance at a $50 Amazon gift card. Click here to get started!






A Unity PlayerPrefs wrapper

Posted by Gamieon, 30 November 2013 · 862 views

A Unity PlayerPrefs wrapper Apologies for the terribly boring journal entry thumbnail image. Anyway...

With every new project I try to make better tools to carry into my future projects. One of them has to do with the local game configuration and PlayerPrefs. There are two issues I have with PlayerPrefs:
  • Using PlayerPrefs.Get... is slow in an Update() or an OnGUI() function (at least it was for me on mobile), and you have to grab the value in Awake() or Start() into a member variable and use that to avoid the issue.
  • No PlayerPrefs.GetBool() for yes/no toggling.
So I decided to make my own static class that not only addresses both issues, but does so in a way that I find convenient for organization. I call this class the "ConfigurationDirector."


The Preference Cache

For those of you who learn by staring at code first, or just want to copy it into your project, here you go (C# version):
public static class ConfigurationDirector 
{
  #region Preference Cache
  
  static Dictionary<string,float> cachedFloatProps = new Dictionary<string, float>();
  static Dictionary<string,string> cachedStringProps = new Dictionary<string, string>();
  static Dictionary<string,int> cachedIntProps = new Dictionary<string, int>();
  
  public static float GetFloat(string prefName, float defaultValue)
  {
    if (!cachedFloatProps.ContainsKey(prefName))
    {
      cachedFloatProps.Add(prefName, PlayerPrefs.GetFloat(prefName, defaultValue));
      PlayerPrefs.SetFloat(prefName, PlayerPrefs.GetFloat(prefName, defaultValue));
    }
    return cachedFloatProps[prefName];
  }
  
  public static void SetFloat(string prefName, float newValue)
  {
    PlayerPrefs.SetFloat(prefName, newValue);
    if (!cachedFloatProps.ContainsKey(prefName))
    {
      cachedFloatProps.Add(prefName, newValue);
    }
    else
    {
      cachedFloatProps[prefName] = newValue;
    }
  }
  
  public static string GetString(string prefName, string defaultValue)
  {
    if (!cachedStringProps.ContainsKey(prefName))
    {
      cachedStringProps.Add(prefName, PlayerPrefs.GetString(prefName, defaultValue));
      PlayerPrefs.SetString(prefName, PlayerPrefs.GetString(prefName, defaultValue));
    }
    return cachedStringProps[prefName];
  }
  
  public static void SetString(string prefName, string newValue)
  {
    PlayerPrefs.SetString(prefName, newValue);
    if (!cachedStringProps.ContainsKey(prefName))
    {
      cachedStringProps.Add(prefName, newValue);
    }
    else
    {
      cachedStringProps[prefName] = newValue;
    }
  }
  
  public static int GetInt(string prefName, int defaultValue)
  {
    if (!cachedIntProps.ContainsKey(prefName))
    {
      cachedIntProps.Add(prefName, PlayerPrefs.GetInt(prefName, defaultValue));
      PlayerPrefs.SetInt(prefName, PlayerPrefs.GetInt(prefName, defaultValue));
    }
    return cachedIntProps[prefName];
  }
  
  public static void SetInt(string prefName, int newValue)
  {
    PlayerPrefs.SetInt(prefName, newValue);
    if (!cachedIntProps.ContainsKey(prefName))
    {
      cachedIntProps.Add(prefName, newValue);
    }
    else
    {
      cachedIntProps[prefName] = newValue;
    }
  }
  
  public static bool GetBool(string prefName, bool defaultValue)
  {
    return (GetInt(prefName, (defaultValue) ? 1 : 0) == 0) ? false : true;
  }
  
  public static void SetBool(string prefName, bool newValue)
  {
    SetInt(prefName, newValue ? 1 : 0);
  }
  
  #endregion
In short, what I do is call PlayerPrefs functions when I need values that are not cached in memory, and just read from memory at each subsequent Get. When I Set values however, I have to write to both cache and PlayerPrefs. Setting is, however, a fairly rare event.

Doing a dictionary lookup at each frame is of course not as fast as just grabbing the preference value in Awake() or Start() into a member variable and using that member variable in Update() or OnGUI(); but I don't notice the speed hit even on an iPhone 3GS and I like knowing that the preference is always up to date so long as I use the ConfigurationDirector properly.


Contexts

So you need to track your player's name, their high score, the class they're using for the current game, the color of their uniform, the game difficulty level, the music volume, the IP address of the most recent server they played on....and that's just the beginning!

I chose to bring order to that chaos by having my own system which I'll explain as a C# code snippet:
public static class ConfigurationDirector 
{

  /// <summary>
  /// Player configuration settings
  /// </summary>
  public static class Player
  {
    /// <summary>
    /// Gets or sets the player name.
    /// </summary>
    /// <value>
    /// The name.
    /// </value>
    public static string Name
    {
      get {
        return GetString("Player.Name", "");
      }
      set {
        SetString("Player.Name", value);
      }
    }
    
    /// <summary>
    /// Gets the default hue.
    /// </summary>
    /// <value>
    /// The default hue.
    /// </value>
    public static float DefaultHue { get { return 62f; } }

    /// <summary>
    /// Gets or sets the player hue. This is a floating precision number between and including
    /// 0 and 360.
    /// </summary>
    /// <value>
    /// The player hue.
    /// </value>
    public static float Hue
    {
      get {
        return GetFloat("Player.Hue", DefaultHue);
      }
      set {
        SetFloat("Player.Hue", value);
      }
    } 
    
    /// <summary>
    /// Sets the color of the player.
    /// </summary>
    /// <value>
    /// The color of the player.
    /// </value>
    public static Color PlayerColor
    {
      get 
      {
        return ColorDirector.HSL2RGB(Hue / 360.0, 0.7, 0.5);
      }
    }
    
    /// <summary>
    /// Gets or sets a value indicating whether this player has seen tutorial.
    /// </summary>
    /// <value>
    /// <c>true</c> if this player has seen tutorial; otherwise, <c>false</c>.
    /// </value>
    public static bool HasSeenTutorial
    {
      get {
        return GetBool("Player.HasSeenTutorial", false);
      }
      set {
        SetBool("Player.HasSeenTutorial", value);
      }
    }
  }
  
  /// <summary>
  /// Audio configuration settings
  /// </summary>
  public static class Audio
  {
    /// <summary>
    /// Gets the default SFX volume.
    /// </summary>
    /// <value>
    /// The default SFX volume.
    /// </value>
    public static float DefaultSFXVolume { get { return 0.5f; } }

    /// <summary>
    /// Gets or sets the SFX volume.
    /// </summary>
    /// <value>
    /// The SFX volume.
    /// </value>
    public static float SFXVolume
    {
      get {
        return GetFloat("Audio.SFXVolume", DefaultSFXVolume);
      }
      set {
        SetFloat("Audio.SFXVolume", value);
      }
    }
    
    /// <summary>
    /// Gets the default music volume.
    /// </summary>
    /// <value>
    /// The default music volume.
    /// </value>
    public static float DefaultMusicVolume { get { return 0.4f; } }
    
    /// <summary>
    /// Gets or sets the music volume.
    /// </summary>
    /// <value>
    /// The music volume.
    /// </value>
    public static float MusicVolume
    {
      get {
        return GetFloat("Audio.MusicVolume", DefaultMusicVolume);
      }
      set {
        SetFloat("Audio.MusicVolume", value);
      }
    }
  }
  
  /// <summary>
  /// Network configuration settings
  /// </summary>
  public static class NetworkSettings
  {
    /// <summary>
    /// Gets or sets the game name.
    /// </summary>
    /// <value>
    /// The name.
    /// </value>
    public static string GameName
    {
      get
      {
        return GetString("Network.GameName", "My Game");
      }
      set
      {
        SetString("Network.GameName", value);
      }     
    }
    
    /// <summary>
    /// Gets the default port.
    /// </summary>
    /// <value>
    /// The default port.
    /// </value>
    public static int DefaultPort { get { return 12345; } }
  
    /// <summary>
    /// Gets or sets the port.
    /// </summary>
    /// <value>
    /// The port.
    /// </value>
    public static int Port
    {
      get
      {
        return GetInt("Network.Port", DefaultPort);
      }
      set
      {
        SetInt("Network.Port", value);
      }
    }
  }
    
  /// <summary>
  /// Unlocks.
  /// </summary>
  public static class Unlocks
  {
    /// <summary>
    /// Determines whether unlocks are enabled
    /// </summary>
    /// <value>
    /// True if unlocks are enabled
    /// </value>
    public static bool Enabled
    {
      get
      {
        return GetBool("Unlocks.Enabled", false);
      }
      set
      {
        SetBool("Unlocks.Enabled", value);
      }     
    }
  }
  
}
If I want to get the player's name for example, I do:

string name = ConfigurationDirector.Player.Name;

The call is generally ConfigurationDirector.<category name>.<property name>

The PlayerPrefs key name is generally "<category name>.<property name>"

Doing this has these advantages:
  • Developers can use Intellisense to look up existing preferences as they add new code instead of surfing around the project to find existing preference key strings.
  • Every preference key string is defined in a single file.
  • There is consistency between the preference key string and the call to get or set a preference (and therefore predictability); and I don't think the compiler would let you have any duplicate preference key strings if you follow that consistency.
  • If you're new to the project (or are going back to it after a while), it's a faster learning curve to implement new preference-related code than if there were PlayerPref calls scattered everywhere.

Conclusion

This is how I manage configurations in my recent games, and it works well for my purposes. If you're not happy with how you maintain your game's configuration, or you forgot the preference key for the player skin for the 8th time, it might be worth looking into doing something like this.




Thanks for sharing -- I'm not a Unity user, but it sounds like something people may find useful! :)

Most of the get/set methods have duplicated code. Why not use generics to define it once for all types?

Most of the get/set methods have duplicated code. Why not use generics to define it once for all types?

 

In the end, for me, it came down to coding style and level of importance. The short answer is: PlayerPrefs does not use generics.

 

If you call Get the first time, and the value is not in the cache, then you have to call PlayerPrefs.Get to pull the value into the cache. Assuming you're doing this in a function declared like "static int Get<T>(string prefName, T defaultValue)," you'd have to look at the type of T to figure out which PlayerPrefs.Get to call (I have not tried it but maybe you could cleverly use reflection or Invoke?). The same is true in the Set function to decide which PlayerPrefs.Set to call. If the type of T was unsupported, I would throw an exception. That should never happen; but there has to be a code path for such a case regardless.

 

That's where I thought "You know...I would rather duplicate a small number of code fragments than deal with generics, and I don't know a superior third alternative."

 

 

I would like to note that the way I did it does have a flaw, which is that I use a different dictionary for each type. That is not how Unity does it. If you call PlayerPrefs.SetString("mykey"), then the return value of PlayerPrefs.GetInt("mykey") is affected. I can't think of a good reason why one would call different PlayerPrefs get/set functions for the same key, though.

PARTNERS