Archivio ScriptsTips

Migliorare il PlayerPrefs

PlayerPrefs è la classe che ci permette di effettuare dei salvataggi permanenti. La locazione dei salvataggi varia a seconda del dispositivo per cui si è sviluppato il gioco ma il funzionamento a livello di codice è lo stesso. Se non conoscete il suo utilizzo, vi invito a leggere la lezione dedicata al PlayerPrefs.

Siamo qui per “potenziare” il metodo di salvataggio tramite la classe PlayerPrefs, o meglio, per creare una nuova classe che si appoggi sull’originale ma che ci permetta di salvare tipi di variabile come i Vector3, i Vector2, i Quaternioni e tutti quei dati che, se usassimo il normale PlayerPrefs necessiterebbero il salvataggio di ogni singolo elemento che lo compongono.
Badate bene che questo script non è un sostituto del PlayerPrefs originale, esso lo completa e si basa su di esso.

Come sappiamo, tramite PlayerPrefs potremmo salvare un dato di tipo float con la seguente istruzione:

PlayerPrefs.SetFloat("nomeNumero", mioFloat);//Salvo il numero "mioFloat" sotto il nome "nomeNumero"

così da poterlo caricare in qualsiasi momento con l’istruzione:

PlayerPrefs.GetFloat("nomeNumero");//Carico il numero salvato sotto il nome "nomeNumero"

Tutto molto semplice e funzionale… finchè si tratta di dati singoli, come un solo numero float, o int, ecc.

Come possiamo notare, il PlayerPrefs ci permette di salvare e caricare tre tipi di dati, float, int, e string. Poca roba insomma, non vengono menzionati i tipi di dati compositi, come i Vector3, che sono un tipo di dato molto usato in un gioco.
Se per esempio volessimo salvare la posizione del giocatore alla chiusura del gioco, in modo di poterlo riposizionare in quello stesso punto al riavvio, dovremmo salvare singolarmente le tre componenti X,Y,Z con tre righe diverse e lo stesso dovremmo fare per effettuare il caricamento, andando anche a “ricomporre” il Vector3 tramite i tre dati salvati.


Una bella rogna che possiamo evitare con questo splendido script, il PlayerPrefsX, dove vediamo presenti una nutrita serie di variabili composite, tutte salvabili esattamente con lo stesso metodo di prima. Ci sono perfino gli array e gli array di Vector3, i bool, i Color e molti altri tipi di variabile, tutti salvabili e ricaricabili con una singola riga! Possiamo intuire quanto diventi di importanza essenziale questa nuova classe.

Se per esempio volessimo salvare il Vector3 che rappresenta la posizione del player nel momento della chiusura del gioco (o della scena), non dovremmo far altro che usare l’istruzione SetVector3:

PlayerPrefsX.SetVector3( "posizionePlayer", playerTransform.position);

e per ricaricarla non dovremmo far altro che usare l’istruzione GetVector3:

playerTransform.position = PlayerPrefsX.GetVector3( "posizionePlayer");

Copiate e incollate questo lungo script e chiamatelo PlayerPrefsX.cs.

// ArrayPrefs2 v 1.4

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class PlayerPrefsX
{
static private int endianDiff1;
static private int endianDiff2;
static private int idx;
static private byte[] byteBlock;

enum ArrayType { Float, Int32, Bool, String, Vector2, Vector3, Quaternion, Color }

public static bool SetBool(String name, bool value)
{
try
{
PlayerPrefs.SetInt(name, value ? 1 : 0);
}
catch
{
return false;
}
return true;
}

public static bool GetBool(String name)
{
return PlayerPrefs.GetInt(name) == 1;
}

public static bool GetBool(String name, bool defaultValue)
{
return (1 == PlayerPrefs.GetInt(name, defaultValue ? 1 : 0));
}

public static long GetLong(string key, long defaultValue)
{
int lowBits, highBits;
SplitLong(defaultValue, out lowBits, out highBits);
lowBits = PlayerPrefs.GetInt(key + "_lowBits", lowBits);
highBits = PlayerPrefs.GetInt(key + "_highBits", highBits);

// unsigned, to prevent loss of sign bit.
ulong ret = (uint)highBits;
ret = (ret << 32);
return (long)(ret | (ulong)(uint)lowBits);
}

public static long GetLong(string key)
{
int lowBits = PlayerPrefs.GetInt(key + "_lowBits");
int highBits = PlayerPrefs.GetInt(key + "_highBits");

// unsigned, to prevent loss of sign bit.
ulong ret = (uint)highBits;
ret = (ret << 32); return (long)(ret | (ulong)(uint)lowBits);
}
private static void SplitLong(long input, out int lowBits, out int highBits)
{ // unsigned everything, to prevent loss of sign bit.
lowBits = (int)(uint)(ulong)input; highBits = (int)(uint)(input >> 32);
}

public static void SetLong(string key, long value)
{
int lowBits, highBits;
SplitLong(value, out lowBits, out highBits);
PlayerPrefs.SetInt(key + "_lowBits", lowBits);
PlayerPrefs.SetInt(key + "_highBits", highBits);
}

public static bool SetVector2(String key, Vector2 vector)
{
return SetFloatArray(key, new float[] { vector.x, vector.y });
}

static Vector2 GetVector2(String key)
{
var floatArray = GetFloatArray(key);
if (floatArray.Length < 2)
{
return Vector2.zero;
}
return new Vector2(floatArray[0], floatArray[1]);
}

public static Vector2 GetVector2(String key, Vector2 defaultValue)
{
if (PlayerPrefs.HasKey(key))
{
return GetVector2(key);
}
return defaultValue;
}

public static bool SetVector3(String key, Vector3 vector)
{
return SetFloatArray(key, new float[] { vector.x, vector.y, vector.z });
}

public static Vector3 GetVector3(String key)
{
var floatArray = GetFloatArray(key);
if (floatArray.Length < 3)
{
return Vector3.zero;
}
return new Vector3(floatArray[0], floatArray[1], floatArray[2]);
}

public static Vector3 GetVector3(String key, Vector3 defaultValue)
{
if (PlayerPrefs.HasKey(key))
{
return GetVector3(key);
}
return defaultValue;
}

public static bool SetQuaternion(String key, Quaternion vector)
{
return SetFloatArray(key, new float[] { vector.x, vector.y, vector.z, vector.w });
}

public static Quaternion GetQuaternion(String key)
{
var floatArray = GetFloatArray(key);
if (floatArray.Length < 4)
{
return Quaternion.identity;
}
return new Quaternion(floatArray[0], floatArray[1], floatArray[2], floatArray[3]);
}

public static Quaternion GetQuaternion(String key, Quaternion defaultValue)
{
if (PlayerPrefs.HasKey(key))
{
return GetQuaternion(key);
}
return defaultValue;
}

public static bool SetColor(String key, Color color)
{
return SetFloatArray(key, new float[] { color.r, color.g, color.b, color.a });
}

public static Color GetColor(String key)
{
var floatArray = GetFloatArray(key);
if (floatArray.Length < 4)
{
return new Color(0.0f, 0.0f, 0.0f, 0.0f);
}
return new Color(floatArray[0], floatArray[1], floatArray[2], floatArray[3]);
}

public static Color GetColor(String key, Color defaultValue)
{
if (PlayerPrefs.HasKey(key))
{
return GetColor(key);
}
return defaultValue;
}

public static bool SetBoolArray(String key, bool[] boolArray)
{
// Make a byte array that's a multiple of 8 in length, plus 5 bytes to store the number of entries as an int32 (+ identifier)
// We have to store the number of entries, since the boolArray length might not be a multiple of 8, so there could be some padded zeroes
var bytes = new byte[(boolArray.Length + 7) / 8 + 5];
bytes[0] = System.Convert.ToByte(ArrayType.Bool); // Identifier
var bits = new BitArray(boolArray);
bits.CopyTo(bytes, 5);
Initialize();
ConvertInt32ToBytes(boolArray.Length, bytes); // The number of entries in the boolArray goes in the first 4 bytes

return SaveBytes(key, bytes);
}

public static bool[] GetBoolArray(String key)
{
if (PlayerPrefs.HasKey(key))
{
var bytes = System.Convert.FromBase64String(PlayerPrefs.GetString(key));
if (bytes.Length < 5)
{
Debug.LogError("Corrupt preference file for " + key);
return new bool[0];
}
if ((ArrayType)bytes[0] != ArrayType.Bool)
{
Debug.LogError(key + " is not a boolean array");
return new bool[0];
}
Initialize();

// Make a new bytes array that doesn't include the number of entries + identifier (first 5 bytes) and turn that into a BitArray
var bytes2 = new byte[bytes.Length - 5];
System.Array.Copy(bytes, 5, bytes2, 0, bytes2.Length);
var bits = new BitArray(bytes2);
// Get the number of entries from the first 4 bytes after the identifier and resize the BitArray to that length, then convert it to a boolean array
bits.Length = ConvertBytesToInt32(bytes);
var boolArray = new bool[bits.Count];
bits.CopyTo(boolArray, 0);

return boolArray;
}
return new bool[0];
}

public static bool[] GetBoolArray(String key, bool defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetBoolArray(key);
}
var boolArray = new bool[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
boolArray[i] = defaultValue;
}
return boolArray;
}

public static bool SetStringArray(String key, String[] stringArray)
{
var bytes = new byte[stringArray.Length + 1];
bytes[0] = System.Convert.ToByte(ArrayType.String); // Identifier
Initialize();

// Store the length of each string that's in stringArray, so we can extract the correct strings in GetStringArray
for (var i = 0; i < stringArray.Length; i++)
{
if (stringArray[i] == null) { Debug.LogError("Can't save null entries in the string array when setting " + key); return false; }
if (stringArray[i].Length > 255)
{
Debug.LogError("Strings cannot be longer than 255 characters when setting " + key);
return false;
}
bytes[idx++] = (byte)stringArray[i].Length;
}

try
{
PlayerPrefs.SetString(key, System.Convert.ToBase64String(bytes) + "|" + String.Join("", stringArray));
}
catch
{
return false;
}
return true;
}

public static String[] GetStringArray(String key)
{
if (PlayerPrefs.HasKey(key))
{
var completeString = PlayerPrefs.GetString(key);
var separatorIndex = completeString.IndexOf("|"[0]);
if (separatorIndex < 4)
{
Debug.LogError("Corrupt preference file for " + key);
return new String[0];
}
var bytes = System.Convert.FromBase64String(completeString.Substring(0, separatorIndex));
if ((ArrayType)bytes[0] != ArrayType.String)
{
Debug.LogError(key + " is not a string array");
return new String[0];
}
Initialize();

var numberOfEntries = bytes.Length - 1;
var stringArray = new String[numberOfEntries];
var stringIndex = separatorIndex + 1;
for (var i = 0; i < numberOfEntries; i++)
{
int stringLength = bytes[idx++]; if (stringIndex + stringLength > completeString.Length)
{
Debug.LogError("Corrupt preference file for " + key);
return new String[0];
}
stringArray[i] = completeString.Substring(stringIndex, stringLength);
stringIndex += stringLength;
}

return stringArray;
}
return new String[0];
}

public static String[] GetStringArray(String key, String defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetStringArray(key);
}
var stringArray = new String[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
stringArray[i] = defaultValue;
}
return stringArray;
}

public static bool SetIntArray(String key, int[] intArray)
{
return SetValue(key, intArray, ArrayType.Int32, 1, ConvertFromInt);
}

public static bool SetFloatArray(String key, float[] floatArray)
{
return SetValue(key, floatArray, ArrayType.Float, 1, ConvertFromFloat);
}

public static bool SetVector2Array(String key, Vector2[] vector2Array)
{
return SetValue(key, vector2Array, ArrayType.Vector2, 2, ConvertFromVector2);
}

public static bool SetVector3Array(String key, Vector3[] vector3Array)
{
return SetValue(key, vector3Array, ArrayType.Vector3, 3, ConvertFromVector3);
}

public static bool SetQuaternionArray(String key, Quaternion[] quaternionArray)
{
return SetValue(key, quaternionArray, ArrayType.Quaternion, 4, ConvertFromQuaternion);
}

public static bool SetColorArray(String key, Color[] colorArray)
{
return SetValue(key, colorArray, ArrayType.Color, 4, ConvertFromColor);
}

private static bool SetValue<T>(String key, T array, ArrayType arrayType, int vectorNumber, Action<T, byte[], int> convert) where T : IList
{
var bytes = new byte[(4 * array.Count) * vectorNumber + 1];
bytes[0] = System.Convert.ToByte(arrayType); // Identifier
Initialize();

for (var i = 0; i < array.Count; i++)
{
convert(array, bytes, i);
}
return SaveBytes(key, bytes);
}

private static void ConvertFromInt(int[] array, byte[] bytes, int i)
{
ConvertInt32ToBytes(array[i], bytes);
}

private static void ConvertFromFloat(float[] array, byte[] bytes, int i)
{
ConvertFloatToBytes(array[i], bytes);
}

private static void ConvertFromVector2(Vector2[] array, byte[] bytes, int i)
{
ConvertFloatToBytes(array[i].x, bytes);
ConvertFloatToBytes(array[i].y, bytes);
}

private static void ConvertFromVector3(Vector3[] array, byte[] bytes, int i)
{
ConvertFloatToBytes(array[i].x, bytes);
ConvertFloatToBytes(array[i].y, bytes);
ConvertFloatToBytes(array[i].z, bytes);
}

private static void ConvertFromQuaternion(Quaternion[] array, byte[] bytes, int i)
{
ConvertFloatToBytes(array[i].x, bytes);
ConvertFloatToBytes(array[i].y, bytes);
ConvertFloatToBytes(array[i].z, bytes);
ConvertFloatToBytes(array[i].w, bytes);
}

private static void ConvertFromColor(Color[] array, byte[] bytes, int i)
{
ConvertFloatToBytes(array[i].r, bytes);
ConvertFloatToBytes(array[i].g, bytes);
ConvertFloatToBytes(array[i].b, bytes);
ConvertFloatToBytes(array[i].a, bytes);
}

public static int[] GetIntArray(String key)
{
var intList = new List<int>();
GetValue(key, intList, ArrayType.Int32, 1, ConvertToInt);
return intList.ToArray();
}

public static int[] GetIntArray(String key, int defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetIntArray(key);
}
var intArray = new int[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
intArray[i] = defaultValue;
}
return intArray;
}

public static float[] GetFloatArray(String key)
{
var floatList = new List<float>();
GetValue(key, floatList, ArrayType.Float, 1, ConvertToFloat);
return floatList.ToArray();
}

public static float[] GetFloatArray(String key, float defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetFloatArray(key);
}
var floatArray = new float[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
floatArray[i] = defaultValue;
}
return floatArray;
}

public static Vector2[] GetVector2Array(String key)
{
var vector2List = new List<Vector2>();
GetValue(key, vector2List, ArrayType.Vector2, 2, ConvertToVector2);
return vector2List.ToArray();
}

public static Vector2[] GetVector2Array(String key, Vector2 defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetVector2Array(key);
}
var vector2Array = new Vector2[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
vector2Array[i] = defaultValue;
}
return vector2Array;
}

public static Vector3[] GetVector3Array(String key)
{
var vector3List = new List<Vector3>();
GetValue(key, vector3List, ArrayType.Vector3, 3, ConvertToVector3);
return vector3List.ToArray();
}

public static Vector3[] GetVector3Array(String key, Vector3 defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))

{
return GetVector3Array(key);
}
var vector3Array = new Vector3[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
vector3Array[i] = defaultValue;
}
return vector3Array;
}

public static Quaternion[] GetQuaternionArray(String key)
{
var quaternionList = new List<Quaternion>();
GetValue(key, quaternionList, ArrayType.Quaternion, 4, ConvertToQuaternion);
return quaternionList.ToArray();
}

public static Quaternion[] GetQuaternionArray(String key, Quaternion defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetQuaternionArray(key);
}
var quaternionArray = new Quaternion[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
quaternionArray[i] = defaultValue;
}
return quaternionArray;
}

public static Color[] GetColorArray(String key)
{
var colorList = new List<Color>();
GetValue(key, colorList, ArrayType.Color, 4, ConvertToColor);
return colorList.ToArray();
}

public static Color[] GetColorArray(String key, Color defaultValue, int defaultSize)
{
if (PlayerPrefs.HasKey(key))
{
return GetColorArray(key);
}
var colorArray = new Color[defaultSize];
for (int i = 0; i < defaultSize; i++)
{
colorArray[i] = defaultValue;
}
return colorArray;
}

private static void GetValue<T>(String key, T list, ArrayType arrayType, int vectorNumber, Action<T, byte[]> convert) where T : IList
{
if (PlayerPrefs.HasKey(key))
{
var bytes = System.Convert.FromBase64String(PlayerPrefs.GetString(key));
if ((bytes.Length - 1) % (vectorNumber * 4) != 0)
{
Debug.LogError("Corrupt preference file for " + key);
return;
}
if ((ArrayType)bytes[0] != arrayType)
{
Debug.LogError(key + " is not a " + arrayType.ToString() + " array");
return;
}
Initialize();

var end = (bytes.Length - 1) / (vectorNumber * 4);
for (var i = 0; i < end; i++)
{
convert(list, bytes);
}
}
}

private static void ConvertToInt(List<int> list, byte[] bytes)
{
list.Add(ConvertBytesToInt32(bytes));
}

private static void ConvertToFloat(List<float> list, byte[] bytes)
{
list.Add(ConvertBytesToFloat(bytes));
}

private static void ConvertToVector2(List<Vector2> list, byte[] bytes)
{
list.Add(new Vector2(ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes)));
}

private static void ConvertToVector3(List<Vector3> list, byte[] bytes)
{
list.Add(new Vector3(ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes)));
}

private static void ConvertToQuaternion(List<Quaternion> list, byte[] bytes)
{
list.Add(new Quaternion(ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes)));
}

private static void ConvertToColor(List<Color> list, byte[] bytes)
{
list.Add(new Color(ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes), ConvertBytesToFloat(bytes)));
}

public static void ShowArrayType(String key)
{
var bytes = System.Convert.FromBase64String(PlayerPrefs.GetString(key));
if (bytes.Length > 0)
{
ArrayType arrayType = (ArrayType)bytes[0];
Debug.Log(key + " is a " + arrayType.ToString() + " array");
}
}

private static void Initialize()
{
if (System.BitConverter.IsLittleEndian)
{
endianDiff1 = 0;
endianDiff2 = 0;
}
else
{
endianDiff1 = 3;
endianDiff2 = 1;
}
if (byteBlock == null)
{
byteBlock = new byte[4];
}
idx = 1;
}

private static bool SaveBytes(String key, byte[] bytes)
{
try
{
PlayerPrefs.SetString(key, System.Convert.ToBase64String(bytes));
}
catch
{
return false;
}
return true;
}

private static void ConvertFloatToBytes(float f, byte[] bytes)
{
byteBlock = System.BitConverter.GetBytes(f);
ConvertTo4Bytes(bytes);
}

private static float ConvertBytesToFloat(byte[] bytes)
{
ConvertFrom4Bytes(bytes);
return System.BitConverter.ToSingle(byteBlock, 0);
}

private static void ConvertInt32ToBytes(int i, byte[] bytes)
{
byteBlock = System.BitConverter.GetBytes(i);
ConvertTo4Bytes(bytes);
}

private static int ConvertBytesToInt32(byte[] bytes)
{
ConvertFrom4Bytes(bytes);
return System.BitConverter.ToInt32(byteBlock, 0);
}

private static void ConvertTo4Bytes(byte[] bytes)
{
bytes[idx] = byteBlock[endianDiff1];
bytes[idx + 1] = byteBlock[1 + endianDiff2];
bytes[idx + 2] = byteBlock[2 - endianDiff2];
bytes[idx + 3] = byteBlock[3 - endianDiff1];
idx += 4;
}

private static void ConvertFrom4Bytes(byte[] bytes)
{
byteBlock[endianDiff1] = bytes[idx];
byteBlock[1 + endianDiff2] = bytes[idx + 1];
byteBlock[2 - endianDiff2] = bytes[idx + 2];
byteBlock[3 - endianDiff1] = bytes[idx + 3];
idx += 4;
}
}

Se avete dato uno sguardo al codice, vi sarete accorti che PlayerPrefsX è costruito su PlayerPrefs.SetString.
I lavoro che fa PlayerPrefsX non è altro che quello che dovreste fare voi, ogni volta che vi avrete la necessità di salvare una serie di dati compositi, come array e vettori.

Una volta creato questo nuovo script, potrete usare la classe PlayerPrefsX (con la X finale) ogni qual volta avrete la necessità di salvare un array, un Vector o un altro tipo di dato non presente tra le opzioni di salvataggio del PlayerPrefs.
Per le tre variabili string, int e float, potrete usare normalmente il PlayerPrefs.
Stesso dicasi per il metodo HasKey().

Ricordiamoci che esistono altri metodi per il salvataggio dei dati in modo permanente.
Se volete esplorare le altre tecniche vi invito a leggere la spiegazionedei tre metodi di serializzazione trattati in questo articolo.

 
 
 

6 pensieri su “Migliorare il PlayerPrefs

  1. Ciao, intanto ti ringrazio per tutto il lavoro che hai fatto per la creazione di queste lezioni.
    Quando creo lo script PlayerPrefsX, mi da i seguenti errori, ma non capisco a quale punto dello script si riferiscano:
    Assets\Scripts\PlayerPrefsX.cs(62,25): error CS0177: The out parameter ‘lowBits’ must be assigned to before control leaves the current method
    Assets\Scripts\PlayerPrefsX.cs(62,25): error CS0177: The out parameter ‘highBits’ must be assigned to before control leaves the current method

    Non so perché venga generato questo errore. Ti ringrazio in anticipo per l’aiuto.

  2. Ciao Federica, l’errore è dato da una banalissima svista.
    Ti basta andare a capo nel commento alla riga 63, dopo il punto della parola “bit”.
    Comunque, ho aggiornato lo script.

    private static void SplitLong(long input, out int lowBits, out int highBits)
    {
    // unsigned everything, to prevent loss of sign bit.
    lowBits = (int)(uint)(ulong)input; highBits = (int)(uint)(input >> 32);
    }

Rispondi a Luca Paroli Annulla risposta

Il tuo indirizzo email non sarà pubblicato.