数据存储与读取

游戏中通常需要实现“保存进度”“读取进度”之类的功能,那么在开发中,就需要对数据进行存储和读取等工作。可以用PlayerPrefs实现对简单数据的处理以及用JSON实现对对象结构的数据的处理。

PlayerPrefs

PlayerPrefs是Unity自带的数据结构,位于UnityEngine命名空间下。它可以对整数、浮点数、字符串3种类型的数据进行存取。它是持久存储于设备上的,例如安卓,只要用户没有删除应用或者手动去清除应用数据,PlayerPrefs的数据就会一直保留。

整数的存取

一般脚本都会使用UnityEngine命名空间,所以可以在脚本里直接使用PlayerPrefs。
PlayerPrefs使用“键/值”配对的规则,如下:

1
2
int num = 10; // 定义一个整型变量num
PlayerPrefs.SetInt("Number", num); // 存储该变量

第一行代码定义了一个整型变量num,第二行代码存储该变量。第一个参数Number传递了键,第二个参数num传递了这个键所要存储的值。

下面是如何读取这个整数:

1
int num = PlayerPrefs.GetInt("Number");

浮点数的存取

浮点数的存取和整数类似,如下:

1
2
3
float PI = 3.14f; // 定义一个浮点数
PlayerPrefs.SetFloat("PI", PI); // 存储该浮点数
PI = PlayerPrefs.GetFloat("PI"); // 读取该浮点数

字符串的存取

字符串的存取也是一样的处理方法:

1
2
3
string str = "Hello World!"; // 定义一个字符串
PlayerPrefs.SetString("HW", str); // 存储该字符串
str = PlayerPrefs.GetString("HW"); // 读取该字符串

其他PlayerPrefs接口

PlayerPrefs还有一些其他的接口,如下所示:

1
2
3
4
5
PlayerPrefs.Save(); // 保存PlayerPrefs数据
PlayerPrefs.Haskey("HW"); // 是否存在该键

PlayerPrefs.DeleteKey("HW"); // 删除键
PlayerPrefs.DeleteAll(); // 删除所有PlayerPrefs数据

需要注意的是,3种数据结构的键是公用的,比如有一个键“Somekey”,先用它来存整数10,再用它来存字符串“Hello”,最后读取整数。那么结果是0,因为之前存的10被覆盖了。

属性访问器get和set

属性的访问器包含与获取(读取或计算)或设置(写)属性有关的可执行语句。访问器声明可以包含get或set访问器,或者两者均包含。简而言之,就是把一个变量拆成get读取部分和set写入部分,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}

上面代码中的get部分返回一个字符串,而set部分把value赋给name,用起来就跟普通的变量一样,例如:

1
2
Name = "Hi";
Debug.Log(Name);

任何类型的变量都可以使用属性访问器。

属性访问器与PlayerPrefs

思考一下就会发现,PlayerPrefs包含读写两部分,刚好跟get和set对应。用get和set包装一下,PlayerPrefs不就可以当作普通变量使用了吗?
确实是这样。两者配合后可以很方便地使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public string Name
{
get
{
return PlayerPrefs.GetString("Name");
}

set
{
PlayerPrefs.SetString("Name", value);
PlayerPrefs.Save();
}
}

一个自动保存的变量就这么创建出来了,是不是很方便呢?

JSON

JSON是一种轻量级的数据交换和存储格式,可以用于对数据的设备(如手机的本地存储)和向Web服务器上传,并且符合面向对象编程的思想。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C++、C#、Java、JavaScript、Perl、Python等)。这些特性使JSON成为理想的数据交换语言,易于人阅读和编写,同时也易于机器解析和生成。
下面用JSON这个轻量级的数据格式来实现跨平台的存取数据功能。

JSON数据格式

JSON基本数据书写格式是:名称/值,如”name”:”李四”。JSON基本结构主要有以下两种。

对象

用{}包裹,用名称/值来表示对象中的一个属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
public string name;
public int age;
public Person()
{

}

public Person(string _name, int _age)
{
name = _name;
age = _age;
}
}

对象Person john = new Person(“John”, 19);用JSON表示就是:
{“name”:”John”, “age”:”19”}

数组

用[]包裹,用“,”表示并列关系,如:

1
{"people":[{"name":"John", "age":"19"}, {"name":"Tom", "age":"18"}]}

前面我们知道对象john用JSON的表达,那么下面就来实现这个转换。这里用到了JsonFx,这是一个类对象和SON数据相互转换的动态链接库,网上有很多这样的库。在代码Person.cs中定义了Person类,在以下代码中创建了对象john,再将john转成JSON字符串,最后将JSON字符串转换回Person对象。

1
2
3
4
5
6
7
8
9
10
public class Person
{
public string name;
public int age;
public Person(string _name, int _age)
{
name = _name;
age = _age;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;
using JsonFx.Json;

public class JsonDemo : MonoBehaviour
{
void Start()
{
Person john = new Person("John", 19);
// 将对象序列化成JSON字符串
string Json_Text = JsonWriter.Serialize(john);
Debug.Log(Json_Text);
// 将JSON字符串反序列化成对象
john = JsonReader.Deserialize(Json_Text) as Person;
}
}

数据存储

Unity及其使用的Mono是跨平台的,符合NET框架,我们完全可以使用System.IO下的File.ReadAllText()和File.WriteAllTtext()这两个函数来实现数据的存取。
我们已经有了JSON字符串,下一步是得到存取的路径,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static string GetDataPath()
{
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
// iPhone路径
string path = Application.dataPath.Substring(0, Application.dataPath.Length - 5);
path = path.Substring(0, path.LastIndexOf('/'));
return path + "/Documents";
} else if (Application.platform == RuntimePlatform.Android)
{
// 安卓路径
return Application.persistentDataPath + "/";
} else
{
// 其他路径
return Application.dataPath;
}
}

最后一步,将JSON数据写到该路径:

1
File.WriteAllText(GetDataPath() + '/' + "FileName", Json_Text);

至此,对数据存储功能实现的每一步都进行了讲解,数据存储功能的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using UnityEngine;
using System.Collections;
using JsonFx.Json;
using System.IO;

public class DataDemo : MonoBehaviour
{
string path = "data.txt";
void OnGUI()
{
if(GUILayout.Button("保存"))
{
Write();
}

if(GUILayout.Button("读取"))
{
Read();
}
}

void Write()
{
Person john = new Person("John", 19);
string Json_Text = JsonWriter.Serialize(john);
File.WriteAllText(GetDataPath() + path, Json_Text);
}

void Read()
{
string Json_Text = File.ReadAllText(GetDataPath() + path);
Person john = JsonReader.Deserialize<Person>(Json_Text);
Debug.Log(john.name + "'s age is " + john.age);
}

public static string GetDataPath()
{
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
// iPhone路径
string path = Application.dataPath.Substring(0, Application.dataPath.Length - 5);
path = path.Substring(0, path.LastIndexOf('/'));
return path + "/Documents";
} else if (Application.platform == RuntimePlatform.Android)
{
// 安卓路径
return Application.persistentDataPath + "/";
} else
{
// 其他路径
return Application.dataPath;
}
}
}

运行一下,先点击保存按钮,然后刷新(Ctrl+R),发现在文件夹下多了刚才保存的data.txt文件,并且可以看到里面的内容。点击读取,在控制台里可以看到输出“John’s age is 19”。

数据加密

但是打开data.txt文件是可以直接看到文本内容的,十分不安全,需要对文本内容进行加密,加密算法有很多种,如RC2、RC4等。这里使用的是Rijndael算法。
Rijndael是.NET里包含的一个对称加密接口,在加密和解密时都使用相同的秘钥。位于System.Security.Crytography命名空间下。Rijndael算法符合AES堆成密码标准,秘钥长度为128、192、256位之一。
加密步骤如下:
(1)设置字符串秘钥并转化为byte数组。这里使用32的字符串转化为长度为32的byte数组,也就是256位的秘钥。

1
2
static string key = "12348578902223367877723456789012";
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(key);

(2)创建RijndaelManaged对象并设置参数。

1
2
3
4
5
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor();

(3)加密。

1
2
3
4
5
6
7
// 将原始字符串转化成byte数姐
byte[] toEnCryptArray = UTF8Encoding.UTF8.GetBytes("content");
// 加密
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);

// 转换回字符串并返回
return Convert.ToBase64String(resultArray, 0, resultArray.Length);

解密步骤如下:
(1)和加密共用同样的秘钥。
(2)创建RijndaelManaged对象并设置参数,和加密的第(2)步一致。
(3)解密。

1
2
3
4
5
6
// 将加密后的字符串转化成byte数组
byte[] toEncryptArray = Convert.FromBase64string(toD);
// 解密
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
// 转换回字符串并返回
return UTF8Encoding.UTF8.GetString(resultArray);

整个字符串加密与解密的完整代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using UnityEngine;
using System;
using System.Text;
using System.Collections;
using System.Security.Cryptography;

public class Encrypt_Decrypt{
static string key = "12348578902223367877723456789012";
/// <summary>
/// 字符串加密
/// </summary>
private static string Encrypt (string toE)
{
byte[] keyArray = UTF8Encoding.UTF8.GetBytes (key);

RijndaelManaged rDel = new RijndaelManaged ();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor ();

byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes (toE);
byte[] resultArray = cTransform.TransformFinalBlock (toEncryptArray, 0, toEncryptArray.Length);

return Convert.ToBase64String (resultArray, 0, resultArray.Length);
}
/// <summary>
/// 字符串解密
/// </summary>
private static string Decrypt (string toD)
{
byte[] keyArray = UTF8Encoding.UTF8.GetBytes (key);

RijndaelManaged rDel = new RijndaelManaged ();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor ();

byte[] toEncryptArray = Convert.FromBase64String (toD);
byte[] resultArray = cTransform.TransformFinalBlock (toEncryptArray, 0, toEncryptArray.Length);

return UTF8Encoding.UTF8.GetString (resultArray);
}
}

0%