Unity游戏开发之游戏存档方式

目录

1.Unity自带存储方式PlayerPrefs

2.XML存储方式

3.Json类型存储方式

1.Unity的序列化问题

2.Unity中支持序列化的类

3.Unity中Json的使用方法

 4.SQLite

1.SQLite的一些基础(简单介绍,不会深入讲解)

2.在Unity中使用SQLite

3.SQLite的优劣

结语


1.Unity自带存储方式PlayerPrefs

        属于unity自带的数据存储方法,其形式类似于字典的存储形式,里面的存储元素按照键值对(key:value)形式存在;key键的数据类型必须为string字符类型,而value值的数据类型可以为int,float,bool等简单的数据类型。

// <T>表示数据类型
//Save
PlayerPrefs.Set<T>(key,value);
//Load
<T> element=PlayerPrefs.Get<T>(key);
//判断是否存在key
playerPrefs.HasKey(key);

        因为使用该方法的存储数据的类型有限,而且当需要存储的数据类型较为复杂时,该方法就难以实现(或者实现的过程过于复杂),因此,不推荐将此存储方式作为项目的主要存储方式,但是可以用该方式去存储一些简单的值,例如:

        游戏的一些设置:音量大小(int/float),某些设置是否开启(bool)

        一些少量物品的状态设置:某个区域的传送门是否可用(bool),某个区域是否已经开启(bool)

        等等。   

2.XML存储方式

        说到XML,可能有很多学过或者接触过计算机的通知会想到前端的HTML,其实我是觉得这两种形式类似的,其文件形式相近,只不过XML中没有HTML里面那些绚丽的CSS/JavaScript那些样式或者逻辑,XML只是单纯用于存放一些类以及其属性、子类的文件,形式如下:

<?xml version="1.0"?>//固定开头,当你在代码中写入下方内容是,保存文件会自动添加

<Root>//根元素

    <Child1  属性1 = Value1 属性2 = value2  ......>
    //子元素,元素中可以包含属性,属性之间空格隔开

        <element1>inertext1</element1>//元素中可以放置内容,为string类型

        <element2>inertext2</element2>

        <element2>inertext3</element3>
        <element4/>//如果元素之中没有东西,可以直接在开始时跟 / 终结符

    </child1>

</Root>

        其中涉及的变量名仅仅只是方便理解,实际中可以按照自己的需求来进行命名。

        使用XML来存储数据的方法:

public class XMLSaveManager : MonoBehaviour
{
    public string filePath;
    public SkillBag SkillBag;

    public void Awake()
    {
        filePath = Application.dataPath + @"GameData.xml";
    }

    //只存储人物的基础信息
    public void SaveData()
    {
        if (File.Exists(filePath))
        {
//第一步,一定要先创建XmlDocument,才可以对XML文件进行写入保存的操作
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(filePath);
//获得根元素(singleNode,唯一节点则为根节点)下的所有子元素
            XmlNodeList nodeLise = xmlDoc.SelectSingleNode("PlayerInfo").ChildNodes;
//遍历第一个子元素下的所有元素
            foreach (XmlElement item in nodeLise[0].ChildNodes)
            {
//用元素的名称来区分,便于下面的操作
                switch (item.Name)
                {
                    case "Name":
                        item.InnerText=PlayerContorller.instance.playerNameText.text;
                        break;
                    case "Level":
                        item.InnerText = PlayerContorller.instance.levelText.text;
                        break;
                    case "CoinValue":
                        item.InnerText = PlayerContorller.instance.ShowCoinValue.ToString();
//这里是因为ShowCoinValue为int,不用过多考虑改例子的内容
                        break ;
                    default:
                        break;
                }
            }
//每次进行操作完成之后都要进行save操作,否则操作内容只会存储在临时,不会写入磁盘
            xmlDoc.Save(filePath);
            Debug.Log("Save succeeful!");
        }
        else
        {
//一些首次创建XML文件时执行的操作
            XmlDocument xmlDoc = new XmlDocument();
            XmlElement rootElement = xmlDoc.CreateElement("PlayerInfo");
            XmlElement xmlElement = xmlDoc.CreateElement("Player");
            xmlElement.SetAttribute("Id", "001");

            XmlElement nameEl = xmlDoc.CreateElement("Name");
            nameEl.InnerText = PlayerContorller.instance.playerNameText.text;
            XmlElement levelEl = xmlDoc.CreateElement("Level");
            levelEl.InnerText = PlayerContorller.instance.levelText.text;
            XmlElement coinEl = xmlDoc.CreateElement("CoinValue");
            coinEl.InnerText = PlayerContorller.instance.coinValueText.text;
//注意,一定要分清楚每个Element之间的关系
            xmlElement.AppendChild(nameEl);
            xmlElement.AppendChild(levelEl);
            xmlElement.AppendChild(coinEl);
            rootElement.AppendChild(xmlElement);
            xmlDoc.AppendChild(rootElement);

            xmlDoc.Save(filePath);
            Debug.Log("Save succeeful!");
        }
    }

    public void LoadData()
    {
        if (File.Exists(filePath))
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(filePath);
            XmlNodeList nodeLise = xmlDoc.SelectSingleNode("PlayerInfo").ChildNodes;
            foreach (XmlElement item in nodeLise[0].ChildNodes)
            {
                switch (item.Name)
                {
                    case "Name":
                        PlayerContorller.instance.playerNameText.text = item.InnerText;
                        break;
                    case "Level":
                        PlayerContorller.instance.levelText.text = item.InnerText;
                        break;
                    case "CoinValue":
                        PlayerContorller.instance.ShowCoinValue = int.Parse(item.InnerText);
                        break;
                    default:
                        break;
                }
            }

            Debug.Log("Load sueeccful!");
            return;
        }
        Debug.LogWarning("Data file do not exit!");
    }
}

       使用时需要引用命名空间:

using System.IO;
using System.XML;

        本例子只展示了XML存储的基本使用方式,更详细的使用方式可以参考Unity官方手册。

        对于XML文档存储形式,优点在于:

        可读性高:以开始/终结元素名包裹着被存储的数据,容易理解数据的含义;

        易于实现:使用XML文档存储数据并没有涵括太多的编程内容,容易实现

        但是,XML文档也有它的局限性:

        编程”麻烦“:为什么会麻烦呢?优点里不是有 易于实现 吗?这个麻烦其实是指”每一个元素的创建以及起inertext的赋值都需要独立完成“,如何理解呢?简单来说就是,你要保存一个物体的信息(包含长、宽、高),那么你就要依次创建每个属性对应的元素,然后再依次对元素的inertext进行赋值;那么问题来了,倘若一个物体的属性非常的复杂,有十几个属性甚至包含一些数组属性,那么你要怎么去实现XML存储呢?还是一个一个去进行创建跟赋值,那样做的话效率太低,而且重复性代码太多,因此才是”麻烦“。

        可存储的数据类型有限:XML文档时不支持存储数组类型或者一些复杂的数据类型的,但如果你执意要用XML形式去存储一些复杂的数据类型,那么你就需要就复杂的数据类型进行拆分,拆解成为基础(简单)的数据类型进行存储,但如果这样的话,你读取数据的时候又需要重新将这些数据进行组合。这样子做的代码量的庞大的。

3.Json类型存储方式

        接下来要介绍的就是当前环境下最广泛应用的存储方式:将可序列化的数据转置为Json格式进行存储。

        首先要讲的是”什么是可序列化呢?“简单来讲,就是可以将整个数据转换为string字符类型的数据,或者再简单来说,可以在Unity的Editor中的Inspector窗口中显示的数据类型。

        接下来我们简单讲解一下Unity中的可序列化问题。

1.Unity的序列化问题

Unity序列化问题
不支持Unity序列化的字段
静态字段(static fields)
只读字段(readonly fields)
常量(const fields)
未带有[field:SerializeField]特性的属性
支持Unity序列化的字段
公有的 非静态的非只读的非常量 字段
带有[SerializeField]特性的非静态的 非只读的 非常量 字段
带有[field:SerializeField]特性的属性

        很多人可能对支持中的2有疑问,其实就是在私有化变量(privaite)前添加[SerializeField]特性即可将其序列化。

2.Unity中支持序列化的类

一些继承自Object的类 MonBaviour以及继承自它的类

枚举类(Enums)

结构类(Structs)
原始数据类型 int,float,bool,etc.....
其所包含的元素的类型是支持序列化类型的一维数组
其所包含的元素的类型是支持序列化类型的列表
Unity引擎内置的一些类 Vector3,Quaternion,Color,etc.......

3.Unity中Json的使用方法

        首先,新版的Unity引擎中已经支持Json的一些语法编写,不需要再去下载第三方的库也可以正常使用了,只需要引用命名空间:

using Systeam.IO;

        先讲解一下Json存储以及读取数据的一些思路:

        首先,存储数据时,先将可序列化的对象类型转化为Json字符串类型,再将Json字符串写入文件之中;读取数据时,是先将文件中的Json字符串读取出来,在进行反序列化,反序列化过程中需要指定反序列化的数据类型。

JsonUtility.ToJson(object obj,[bool prettyPrint])

        该方法是将可序列化对象obj转换为Json字符串,返回的是string类型,第二个参数默认值是flase,表示是否将Json文件变为更方便阅读的文本格式(同志们自己尝试,这里就不做演示)。

public string filePath = "GameData.json";


    public void SaveData(object saveObj)
    {
        //无需判断文件是否存在,存在则覆盖,不存在则新建
        FileStream fileStream = new FileStream(filePath, FileMode.Create);

        var json = JsonUtility.ToJson(saveObj);

        StreamWriter streamWriter = new StreamWriter(fileStream);

        streamWriter.Write(json);
        Debug.Log("json Save");
        streamWriter.Close();
        fileStream.Close();
    }

关于FileStream的详细使用信息,建议参考  http://t.csdn.cn/z81Vbhttp://t.csdn.cn/z81Vb

         而Stream流,在此的应用中我们仅需要学会使用StreamWirter与StreamReader即可。

JsonUtility.FromJson(string json) / JsonUtility.FromJsonOverWirte(string json,object obj)

        这两种方法都是将Json文件读取并反序列化的操作,但不同的是前一种方法需要指定反序列化生成的数据类型,然后返回一个对应数据类型的实例;而后一种方法是直接对已存在的实例进行重写。

//两种方式指定反序列化的类型
/*该方法只支持普通类和结构;不支持派生自 UnityEngine.Object 的类(如 MonoBehaviour 或 ScriptableObject)*/
JsonUtility.FromJson(String Json,Type T)
JsonUtility.FromJson<T>(String Json)


//若要读取的Json中存储的是一些派生自UnityEngine.Object 的类,推荐使用方法二进行数据重写
JsonUtility.FromJsonOverWirte(String Json,Obejct obj)

例子参考:

public void LoadData(object loadObj)
    {
        if (File.Exists(filePath))
        {
            FileStream fileStream = new FileStream(filePath, FileMode.Open);

            StreamReader streamReader = new StreamReader(fileStream);
            string jsonString = streamReader.ReadToEnd();

            JsonUtility.FromJsonOverwrite(jsonString, loadObj);   
            Debug.Log("json Load");
            streamReader.Close();
            fileStream.Close();
        }
        else
        {
            Debug.Log("Do not found");
        }
    }

        为什么我要在这里详细的(对比前两种方法)介绍Json的存储方式呢,因为Json存储方式是目前我所接触到的存储方式中,最适合游戏存档的方式:

        方便人类阅读以及编写;

        应用范围广泛,跨平台跨语言;

        虽然存储后的文本格式与XML类似,但是Json文件更加轻便,因此在网络传输过程中,同等信息的传输上会更加迅速

        Json十分符合当前的形式,将逐渐成为游戏存档的主流形式。

        虽然Json很好用,但也并非完美:

        支持的数据类型有限(这里仅指 unity自带的JsonUtility所实现的系统所支持的数据类型有限,开发过程中可以通过导入第三方库来进行扩展)

        数据安全性较低(因为可读性高,因此容易被篡改存档的内容)

 4.SQLite

        先来介绍一下SQLite:

SQLite是一款轻型的针对本地化存储的数据库(native database),它是一种关系型数据库管理系统。
SQLite的底层由C语言函数库组成,以嵌入式作为设计目标,在占用资源非常少的情况下实现强大的数据库级的数据存储。
SQLite支持Windows/Linux/Unix/Android/IOS等等主流的操作系统,同时能够使用我们熟悉的C、C++、Ruby、Python、C#、PHP、Java等主流编程语言来编写其代码。
SQLite是一个以文件形式存在的关系型数据库,作为一个轻量级的嵌入式数据库,它不需要系统提供服务支持,通过SDK直接操作文件就可以了使用了。

        简单概括一下,SQLite是一个轻量级的、可跨平台跨语言使用的一种数据库。(常用于本地存储,服务端存储很少或者基本没有使用)

        使用SQLite来作为Unity游戏开发中的数据存储需要具备一定的Sql语言以及数据库操作基础,因为在存储中设计到在代码中使用sql语言来对数据库进行一些基础的增、删、改、查的操作,所以,如果还完全不了解数据库以及数据库操作的同学,建议先花点事件去了解并学习这方面的知识(但如果你说在以后的这门行业里自己绝对不会用到这方面的知识,那我也无言以对)。

1.SQLite的一些基础(简单介绍,不会深入讲解)

SQL的数据类型
(1)NULL,值是一个 NULL 值。
(2)INTEGER,值是一个带符号的整数,根据值的大小存储在1、2、3、4、6或8字节中。
(3)REAL,值是一个浮点值,存储为8字节的IEEE浮点数字。
(4)TEXT,值是一个文本字符串,使用UTF-8或UTF-16存储。
(5)BLOB,值是一个blob数据,完全根据它的输入存储。

特别需要注意的数据类型
Bool数据类型 SQLite没有单独的Bool类型。使用整数0(false)和 1(true)来代替bool值。
Date与Time数据类型 SQLite没有单独的日期或时间类型,可以使用TEXT、REAL或 INTEGER来代替。
TEXT表示时间,格式为"YYYY-MM-DD HH:MM:SS.SSS"的日期。
REAL表示时间,从公元前4714年11月24日格林尼治时间的正午开始算起的天数。
INTEGER表示时间,从1970-01-01 00:00:00UTC算起的秒数。

SQL语句

        关于使用SQL语句对数据库进行SELECT、INSERT、UPDATE、DELETE、ALTER、DROP等操作,在此不作详细说明,感兴趣的小伙伴可以上网搜索相关内容,本篇着重讲解SQLite在Unity中的使用。

2.在Unity中使用SQLite

        首先,想要在Unity中使用SQLite,我们需要几个依赖包:sqlite3.dll、System.Data.dll以及Mono.Data.Sqlite.dll。第一个依赖包可以在slqite的官网进行下载(https://www.sqlite.org/download.html),根据所需要的版本及内容进行下载。

         另外两个依赖是在你安装Unity的时候自动下载好了,只需要你找出来即可,在你安装Unity的目录下找到:EditorDataMonolibmono2.0,在2.0的文件夹下分别找到System.Data.dll以及Mono.Data.Sqlite.dll。三个文件都齐全之后,将其复制一份并一起放入正在开发的Unity项目中的Plugins文件夹中,然后就可以正常使用了。

使用SQLite

        想要正常使用SQLite,需要引入的命名空间:

using UnityEngine;
using System;
using Mono.Data.Sqlite;

        使用SQLite的三部曲:连接数据库,创建并使用SQL命令,获得SQL命令结果(有些操作可能会返回读取数据结果)

    //数据库连接定义
    private SqliteConnection dbConnection;
    //SQL命令定义
    private SqliteCommand dbCommand;
    //查询记录数据读取器定义
    private SqliteDataReader dataReader;

        创建数据库连接

//构造函数,负责构建数据库连接
    public SQLiteDataHelper(string dbName)
    {
        try
        {
            //拼接数据库连接字符串
            string connectString = "data source=" + Application.dataPath + "/" + dbName + ".db";
            //构造数据库连接对象
            dbConnection = new SqliteConnection(connectString);
            //打开这个数据库
            dbConnection.Open();
            Debug.Log("数据库打开成功!");
        }
        catch(Exception e)
        {
            Debug.Log("数据库打开失败:" + e.Message);
        }
    }

注意:其中的connectString为固定形式,一定是"data source="开头,跟着数据库的位置,".db"结尾。

        SQL操作

//执行SQL命令,使用SQL语句字符串
    public SqliteDataReader ExecuteQuery(string queryString)
    {
        //创建命令
        dbCommand = dbConnection.CreateCommand();
        //设置要执行的SQL语句
        dbCommand.CommandText = queryString;
        //执行命令并返回结果
        dataReader = dbCommand.ExecuteReader();
        return dataReader;
    }

        一般的执行方式就是传入一段SQL语言的String类型的变量,然后创建Command,执行Command后返回数据读取记录。

        但是,很多同学应该也想到这个实现方法的弊端,传入的变量是SQL语言的String,那么在实现功能的过程中,就要花费大量时间在c#脚本中编写SQL语言,这是我们不想看到的,因此,我们可以将SQL的常用语言抽象成方法,需要使用的时候就不需要传入完整的SQL语言,而是传入一些比较重要的变量部分。

        

//创建数据库表
    public SqliteDataReader CreateTable(string tableName, string[] colNames, string[] colTypes)
    {
        //判断字段个数与类型是否一致
        if(colNames.Length != colTypes.Length)
        {
            Debug.Log("创建表失败:字段个数与类型个数不一致!");
            return null;
        }
        //拼接创建表SQL语句
        string queryString = "CREATE TABLE " + tableName + "(" + colNames[0] + " " + colTypes[0];
        for(int i = 1; i < colNames.Length; i++)
        {
            queryString += "," + colNames[i] + " " + colTypes[i];
        }
        queryString += ")";
        Debug.Log("创建表:" + tableName + "成功!");
        //执行SQL语句的命令并返回结果
        return ExecuteQuery(queryString);
    }

    //读取整张数据表的所有数据
    public SqliteDataReader ReadFullTable(string tableName)
    {
        //获取所有记录
        string queryString = "SELECT * FROM " + tableName;
        //执行并返回
        return ExecuteQuery(queryString);
    }

    //向指定的数据库表中插入数据
    public SqliteDataReader InsertValues(string tableName, string[] values)
    {
        //获取数据库表中字段的个数
        int fieldCount = ReadFullTable(tableName).FieldCount;
        //判断插入的数据值的个数是否与字段个数一致
        if(values.Length != fieldCount)
        {
            Debug.Log("向表" + tableName + "中插入数据失败,值与字段个数不相符!");
            return null;
        }
        //拼接插入SQL语句字符串
        string queryString = "INSERT INTO " + tableName + " VALUES(" + values[0];
        for(int i = 1; i < values.Length; i++)
        {
            queryString += "," + values[i];
        }
        queryString += ")";
        Debug.Log("向表" + tableName + "中插入记录成功!");
        //执行命令并返回结果
        return ExecuteQuery(queryString);
    }

    //更新指定数据表中的记录
    public SqliteDataReader UpdateValues(string tableName, string[] colNames, string[] colValues,
                                                                        string key, string operation, string value)
    {
        //判断字段个数与值的个数是否一致
        if (colNames.Length != colValues.Length)
        {
            Debug.Log("更新表" + tableName + "失败:字段个数与值的个数不一致!");
            return null;
        }
        //拼接更新SQL语句字符串
        string queryString = "UPDATE " + tableName + " SET " + colNames[0] + "=" + colValues[0];
        for(int i = 1; i < colValues.Length; i++)
        {
            queryString += "," + colNames[i] + "=" + colValues[i];
        }
        queryString += " WHERE " + key + operation + value;
        Debug.Log("更新数据表" + tableName + "中的记录成功!");
        //执行命令并返回结果
        return ExecuteQuery(queryString);
    }

    //删除指定数据表中的记录
    private SqliteDataReader DeleteValues(string tableName, string[] colNames, string[] operations,
                                                                        string[] colValues, string delMode)
    {
        //判断字段个数与关系运算个数与值的个数是否一致
        if(colNames.Length != operations.Length || operations.Length != colValues.Length)
        {
            Debug.Log("删除数据表:" + tableName + "中的数据失败!字段,运算符,值的个数不一致!");
            return null;
        }
        //拼接SQL删除语句字符串
        string queryString = "DELETE FROM " + tableName + " WHERE " + colNames[0] + operations[0] + colValues[0];
        for(int i = 1; i < colValues.Length; i++)
        {
            queryString += " " + delMode + " " + colNames[i] + operations[i] + colValues[i];
        }
        Debug.Log("删除数据表" + tableName + "中的记录成功!");
        //执行SQL命令并返回结果
        return ExecuteQuery(queryString);
    }

    //删除指定数据表中的记录,条件之间使用AND逻辑与操作
    public SqliteDataReader DeleteValuesAnd(string tableName, string[] colNames, string[] operations,
                                                                        string[] colValues)
    {
        return DeleteValues(tableName, colNames, operations, colValues, "AND");
    }

    //删除指定数据表中的记录,条件之间使用OR逻辑与操作
    public SqliteDataReader DeleteValuesOr(string tableName, string[] colNames, string[] operations,
                                                                        string[] colValues)
    {
        return DeleteValues(tableName, colNames, operations, colValues, "OR");
    }

        以上实现的方法都是传入一些简单的变量之后进行SQL语句的缝合,这样做哪怕你不是很了解SQL语言的编写,只要你理清楚你这一步对数据库操作想达到什么效果,继而传入对应所需的变量以及操作符,就可以完成操作。(注意:以上实现的只是数据库的一些简单的操作,如果你想要使用更加复杂、更加自由的操作,那么则需要你自己去实现了。)

        最后,当你执行完全部对数据库的操作之后,不需要再使用了,那么请将数据库以及有关数据库的一些东西给关闭掉,以节省不必要的开支。

    //关闭数据库连接,释放系统资源
    public void CloseConnection()
    {
        //销毁Command
        if(dbCommand != null)
        {
            dbCommand.Cancel();
            dbCommand = null;
        }

        //销毁Reader
        if(dataReader != null)
        {
            dataReader.Close();
            dataReader = null;
        }

        //销毁Connection
        if(dbConnection != null)
        {
            dbConnection.Close();
            dbConnection = null;
        }
        Debug.Log("数据库已经被关闭!");
    }

3.SQLite的优劣

        前面也讲到了,SQLite是一种轻量级的数据库,因此它对比与其他例如Oracle、MySQL、SQLServer等大型数据库,对于一些使用的电脑性能不是很好的同学更加友好,除外还有:

ACID事务,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
零配置,即无需安装和管理配置
储存在单一磁盘文件中的一个完整的数据库
读写效率很高,比大型数据库要快很多
独立,没有额外依赖
项目源码完全的开源, 你可以将其用于任何商业用途, 包括出售它

        但是,毕竟SQLite对于之学习过Unity的同学来说,可能相当于另外一个全新的领域,因此需要花费额外的时间去学习并掌握使用方式;而且,由于SQLite的轻量级的性质,它所搭载的方法以及可实现的功能可能就没有一些大型数据库那么齐全,并且SQLite并不适合用于存储庞大的数据,它只是一个本地的数据库。

结语

        至此,关于Unity游戏开发的游戏存档方式的介绍到此结束。

        以上的方法各有优劣,并不存在什么这种方法就一定是高人一等的说法,根据游戏数据存储的需求来选择合适的方法,比盲目的使用一种方法的效率更高,而往往一个游戏项目中的存储形式不会单一,因此,学会如何去判断当前游戏数据的更优(更便利)的存储形式尤为重要。

        祝诸君,武运昌隆。