Unity数据持久化_XML

  • 省流:本文包含Unity数据持久化Xml相关的用法,主要内容分为三部分:XML基础语法与在C#中的操作、XML序列化与反序列化(含自定义类及字典的序列化),最终将封装一个单例模式的XML数据管理器

Unity数据持久化_XML

一、XML基础语法与在C#中的操作

1.1 XML的基础语法

(1)XML基础

  • XML是一种可扩展语言,他是一种树形结构

  • XML是纯文本的,机器和人都可读,可以用来存储数据
    (2)XML的基本语法

  • 固定内容:<?xml version="1.0" encoding="UTF-8"?>
    固定内容为XML的版本 和 编码格式,其必须写在XML文件中的第一行

  • XML注释:<!--注释内容-->

  • XML节点:<节点名>节点内容</节点名>
    在XML文本中必须包含一个根节点

  • XML属性:<节点名 属性名="属性内容">节点内容<节点名 属性名="属性内容">
    XML属性的具体内容必须用单引号或双引号包裹,无论其是什么类型
    若节点内容无意义,可简写为:<节点名 属性名="属性内容"/>

  • 注意:在XML文件的编写时,XML对字母大小写和空格的检测 十分严格,因此在完成一个XML文本时,你可以通过一下流程检查你的XML文本的语法
    菜鸟教程 > XML教程 > XML验证器

1.2 XML在C#中的使用

(1)xml文件的存放位置

  • 对于只读不写的XML文件存放在Resoursces 或 StreamingAssets文件夹
  • 动态存储的XML文件存放在Application.presistentPath
    (2)C#读取XML的方法

(a)读取XML中的信息

  • 第一步:创建一个用来存储XML的数据结构
    XmlDocument x = new XmlDocument()
  • 第二步:读取XML中的信息
    法一:根据XML字符串内容读取XML中的信息(需要先加载对应的xml文j件)
    法二:通过xml文件路径读取 xml变量实例.Load(Application.StrmingAssetsPath + "target.xml")

(b)通过XML信息 读取 元素 和 属性 信息

  • 获取根节点:XmlNode root = xml变量实例.SelectSingleNode(string 根节点的名字)
  • 获取对应节点的子节点:XmlNod resoultXmlNode = targetXmlNode.SelectSingleNode(string 目标节点的名字)
  • 获取节点的具体内容:Xml节点实例.InnerText
  • 获取节点的属性信息:
    法一:Xml节点实例.Attributes[属性的键].Value
    法二:Xml节点实例.Attributes.GetNamedItem(属性的键).Value
  • 获取节点下的多个同名子节点
    XmlNodeList list = Xml节点实例.SelectNodes(目标节点名)
    (后续可以通过遍历来找到具体的节点)
    (3)C#存储XML的方法

(a)xml文件存储时用到的类

  • XmlDocument 用于创建节点
  • XmlDeclaration 存储xml文件的版本信息
  • XmlElement 是xml的节点类

(b)xml文件存储的流程

  • 第一步:创建文本对象XmlDocument,用来存储XML
    XmlDocument playerInfo = new XmlDocument()
  • 第二步:为xml添加 固定信息
    创建固定信息 并保存到XmlDeclaration类型变量中
    XmlDeclaration versionInfo = playerInfo.CreateXmlDeclaration("1.0", "UTF-8", "")
    将固定信息添加到xml文件中
    XmlDocument实例.AppendChild(versionInfo)
  • 第三步:为文本对象添加节点
    必须有一个根节点
  • 创建根节点并添加到xml文件中(即用来存储xml的XmlDocument 实例)
    XmlElement 根节点实例 = XmlDocument实例.CreateElement("根节点名")
    XmlDocument实例.AppendChild(根节点实例)
  • 添加子节点(playerInfo为XmlDocument实例,root为根节点实例)

    name.InnerTest是在设置 子节点的信息内容
  • 添加属性
  • 第四步保存:
    XmlDocument实例.Save("保存路径")

========== 【演示:完整的保存流程】 ==========

csharp 复制代码
 //(3)xml文件存储的流程
 //  第一步:创建文本对象
 XmlDocument playerInfo = new XmlDocument();
 //  第二步:为xml文本对象添加 固定信息
 // 创建固定信息 并保存到XmlDeclaration类型变量中
 XmlDeclaration versionInfo = playerInfo.CreateXmlDeclaration("1.0", "UTF-8", "");
 // 将固定信息添加到xml文本中
 playerInfo.AppendChild(versionInfo);
 //  第三步:为文本对象添加根节点
 // 创建根节点
 XmlElement root = playerInfo.CreateElement("root");
 // 将根节点添加到xml文本文件中
 playerInfo.AppendChild(root);
 //  第四步:添加子节点
 XmlElement name = playerInfo.CreateElement("name");
 name.InnerText = "包子";//子节点的信息内容
 root.AppendChild(name);//将子节点添加到根节点
 //  第四步:添加属性
 XmlElement item = playerInfo.CreateElement("item");
 item.SetAttribute("道具名", "包子");
 item.SetAttribute("数量", "10");
 item.SetAttribute("效果", "恢复十点生命");
 name.AppendChild(item);
 //  第五步:保存
 playerInfo.Save(path);

 // 检测是否保存成功
 if(File.Exists(path))
 {
     print("保存成功 路径为:" + path);
     //打开该路径下的文件
     Application.OpenURL(Path.GetDirectoryName(path));
     // 删除xml文件的某一属性
     // 第一步,找到要删除的子节点
     // playerInfo.SelectSingleNode(target);
     // 第二步,从该子节点的根节点处将其删除
     // root.RemoveChild(要移除的目标节点)

 }

(c)删除子节点和建成是否保存成功

  • 删除xml文件的某一属性
    第一步,找到要删除的子节点
    XmlDocument实例.SelectSingleNode(target)
    第二步,从该子节点的根节点处将其删除
    目标节点的根节点.RemoveChild(要移除的目标节点)
  • 检测是否保存成功
    File.Exists(保存路径)

========== 【演示:删除子节点和建成是否保存成功】 ==========

csharp 复制代码
// 检测是否保存成功
if(File.Exists(path))
{
    print("保存成功 路径为:" + path);
    //打开该路径下的文件
    Application.OpenURL(Path.GetDirectoryName(path));
    // 删除xml文件的某一属性
    // 第一步,找到要删除的子节点
    // playerInfo.SelectSingleNode(target);
    // 第二步,从该子节点的根节点处将其删除
    // root.RemoveChild(要移除的目标节点)

}

二、序列化与反序列化

2.1 XML序列化

(1)什么是序列化和反序列化

  • 序列化:将内存中的对象转化为可传输的字节序列(即XML文件)的过程,存储对象时使用

  • 反序列化:将字节序列XML还原为对象实例的过程
    (2)xml序列化使用前提

  • 类的访问权限为 public,且存在无参构造

  • 类中的属性/字段 的访问权限为 public
    只读属性无法被序列化
    对于引用类型变量,当器为null时将不会对其序列化
    (3)XML文件序列化的流程

  • 第一步:确定存储路径
    string path = Application.persistentDataPath + "/playerInfo.xml"

  • 第二步:创建一个流写入器StreamWriter,并传入写入文件的保存的路径
    StreamWriter stream = new StreamWriter(path)
    注意:这里需要用using 包裹,作用是当path中存在对应文件时,则在对应文件中写入数据,不存在就先创建后写入
    此外:using括号当中包裹的声明的对象 会在 大括号语句块结束后 自动释放掉
    using (StreamWriter stream = new StreamWriter(path)){ using语句块 }

  • 第三步创建序列化器(第三四步位于 using语句块中)
    XmlSerializer s = new XmlSerializer(typeof(序列化对象的类型))

  • 第四步:通过序列化器将具体实例对象序列化
    s.Serialize(stream,序列化的实例对象)
    你可以通过 Application.OpenURL(Path.GetDirectoryName(path))快速找到保存的XML文件

========== 【演示:将TestClass的实例对象序列化】 ==========

csharp 复制代码
//(3)序列化的流程
// 创建的TestClass 实列对象
TestClass t = new TestClass();
//第一步:确定存储路径
string path = Application.persistentDataPath + "/playerInfo.xml";
// 第二步:创建一个流写入器StreamWriter,并传入写入文件的保存的路径
// 注意:这里需要用using 包裹,作用是当path中存在对应文件时,则在对应文件中写入数据,不存在就先创建后写入
// 此外:using括号当中包裹的声明的对象 会在 大括号语句块结束后 自动释放掉
using (StreamWriter stream = new StreamWriter(path))
{
    //第三步:创建序列化器
    XmlSerializer s = new XmlSerializer(typeof(TestClass));
    //第四步:通过序列化器将具体实例对象序列化
    s.Serialize(stream, t);
    Application.OpenURL(Path.GetDirectoryName(path));
}

2.2 XML反序列化

(1)反序列化的流程

  • 第一步:判断XML文件是否存在
    File.Exists(path)
    返回值为true时则存在,可以进行反序列化
  • 第二步:创建一个读取器,并传入要读取的地址
    using (StreamReader reader = new StreamReader(path)){ using 语句块 }
  • 第三步:创建反序列化器
    XmlSerializer s = new XmlSerializer(typeof(TestClass))
  • 第四步:通过反序列化器将XML文件还原为实列对象
    TestClass t2 = s.Deserialize(reader) as TestClass

========== 【演示:将XML文件反序列化TestClass实例对象】 ==========

csharp 复制代码
// 反序列化:将字节序列还原为对象的过程
//(1)反序列化的流程
//- 第一步:判断XML文件是否存在
//读取XML文件的路径
string path = Application.persistentDataPath + "/playerInfo.xml";
if(File.Exists(path))
{
    //- 第二步:创建一个读取器,并传入要读取的地址
    using (StreamReader reader = new StreamReader(path))
    {
        //- 第三步:创建反序列化器,
        XmlSerializer s = new XmlSerializer(typeof(TestClass));
        //- 第四步:通过反序列化器将XML文件还原为实列对象
        TestClass t2 = s.Deserialize(reader) as TestClass;
    }
}

2.3 IXmlSerializable接口 自定义序列化

(1)继承IXmlSerializable接口的类要实现三个方法

(a) public XmlSchema GetSchema() 方法

该方法会返回一个结构,实现时只需要令其返回null即可
(b) public void WriteXml(XmlWriter writer) 方法

该方法需要传入一个XmlWriter写入器,自定义类序列化时自动执行,用于自定义序列化规则

  • 属性的序列化
    writer.WriteAttributeString("属性名", 对应的值);
  • 节点的序列化
    writer.WriteElementString("节点名", 对应的值);
  • 子节点的序列化
    writer.WriteStartElement("根节点名")
    进行子节点序列化
    writer.WriteEndElement()

========== 【演示:实现一个自定义类的序列化方法】 ==========

(c) public void ReadXml(XmlReader reader) 方法

该方法需要传入一个XmlReader读取器,自定义类反序列化时自动执行,用于自定义反序列化规则

  • 属性的反序列化
    reader.GetAttribute("属性名")
  • 节点的反序列化
    reader.ReadElementString("节点名")
  • 子节点的反序列化
    reader.ReadStartElement("根节点名")
    进行子节点反序列化
    reader.ReadEndElement()

========== 【演示:实现一个自定义类的反序列化方法】 ==========

三、单例模式实现XML数据管理器

3.1 字典的序列化与反序列化

XML实现数据管理依赖于将XML进行序列化,但对于一些数据结构,如字典,不能通过Serializable进行序列化,因此需要通过IXmlSerializable接口 自定义序列化

(1) 首先,要让字典实现序列化,需要继承IXmlSerializable接口,并实现其中的三个方法

但我们无法直接修改字典,所以我们需要创建一个自定义的可序列化的字典类
public class SerizlizerDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable

(2) 接下来我们要实现其中的三个方法

  • public void WriteXml(XmlWriter writer) 方法
    只需要让他返回null即可
  • public void WriteXml(XmlWriter writer)
    该方法用来实现自定义的可序列化的字典类的 反序列化功能

首先,我们需要两个序列化器,用来分别序列化字典的 键 和 值
XmlSerializer keySer = new XmlSerializer(typeof(TKey))
XmlSerializer valueSer = new XmlSerializer(typeof(TValue))

其次,我们遍历字典得到他的键值对item
foreach (KeyValuePair<TKey, TValue> item in this){ 遍历语句块 }

接下来,我们通过写入器writer对键值进行写入,其中每一个键值对的键和值都会被item节点包裹(方便区分各键值对)该部分是遍历语句块中的内容
writer.WriteStartElement("item")
keySer.Serialize(writer, item.Key)
valueSer.Serialize(writer, item.Value)
writer.WriteEndElement()

  • public void ReadXml(XmlReader reader)方法
    该方法用来实现自定义的可序列化的字典类的 反序列化功能

首先,我们需要两个反序列化器,用来分别反序列化字典的 键 和 值
XmlSerializer keySer = new XmlSerializer(typeof(TKey))
XmlSerializer valueSer = new XmlSerializer(typeof(TValue))

其次,因为读取XML时,会先进入根节点,所以我们要读取根节点
reader.Read()

接下来,我们通过读取器reader来进行读取键值对
while (reader.IsStartElement("item")){ while语句块 } 这里只会读取节点为item的节点,存在错误,详情看 3.3
reader.ReadEndElement()

最后,将对应的键值存储到字典中 (该部分是while语句块中的内容)
TKey key = (TKey)keySer.Deserialize(reader)
TValue value = (TValue)valueSer.Deserialize(reader)
this.Add(key, value)
3.1完整代码

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using UnityEngine;
/// <summary>
/// 该脚本实现字典的序列化,因为我们无法修改字典,所以只能通过继承的方式实现一个可以序列化的字典
/// </summary>
/// <typeparam name="TKey">键的泛型</typeparam>
/// <typeparam name="TValue">值的泛型</typeparam>
public class SerizlizerDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        //要跳过根节点
        reader.Read();
        //当前元素为键值对就进行读取
        while (reader.IsStartElement("item"))//这里会导致只读取item节点,详情看问题
        {
            //读取 Key
            TKey key = (TKey)keySer.Deserialize(reader);
            // 读取 Value
            TValue value = (TValue)valueSer.Deserialize(reader);
            // 添加到字典
            this.Add(key, value);
        }
        reader.ReadEndElement();
    }

    public void WriteXml(XmlWriter writer)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        foreach (KeyValuePair<TKey, TValue> item in this)
        {
            //键值对 的序列化
            //每一个键值对都的键值都是其子节点,用来区分
            writer.WriteStartElement("item");
            keySer.Serialize(writer, item.Key);
            valueSer.Serialize(writer, item.Value);
            //writer.WriteAttributeString(kv.Key.ToString(),kv.Value.ToString());
            writer.WriteEndElement();
        }
    }
}

3.2 读取与存储功能

  • 读取
    首先检查读取的XML文件是否存在,存在则通过反序列化进行读取
  • 存储
    首先,通过传入的文件名,设置保存路径
    其次,进行序列化,将数据实例对象转化为XML文件
    3.2完整代码
csharp 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;

public class XmlDataMgr
{
    #region 单例模式实现 保证场景上只有一个实例
    private static XmlDataMgr instance = new XmlDataMgr();

    public static XmlDataMgr Instance => instance;

    private XmlDataMgr() { }
    #endregion

    public void SaveData(object data, string fileName)
    {
        //通过传入的文件名,设置保存路径
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        //进行序列化,将数据实例对象转化为XML文件
        using (StreamWriter writer = new StreamWriter(path))
        {
            XmlSerializer s = new XmlSerializer(data.GetType());
            s.Serialize(writer, data);
        }
    }


    public object LoadData(Type type, string fileName)
    {
        //反序列化,首先检查是否存在对应的XML文件
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        if (File.Exists(path))
        {
            using (StreamReader reader = new StreamReader(path))
            {
                //反序列化 取出数据
                XmlSerializer s = new XmlSerializer(type);
                return s.Deserialize(reader);
            }
        }   
        return null;
    }
}

3.3 问题

在测试时,我发现可以正常将实例化对象序列化为XML文件

但是在读取时,出现错误

原因是,在我的原代码中只会读取 节点名为 item的节点,不会继续读他的子节点

修改后的代码为

csharp 复制代码
  public void ReadXml(XmlReader reader)
  {
      XmlSerializer keySer = new XmlSerializer(typeof(TKey));
      XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

      //要跳过根节点
      reader.Read();
      //当前元素为键值对就进行读取
      while (reader.NodeType != XmlNodeType.EndElement)
      {
          reader.ReadStartElement("item");
          //读取 Key
          TKey key = (TKey)keySer.Deserialize(reader);
          // 读取 Value
          TValue value = (TValue)valueSer.Deserialize(reader);
          // 添加到字典
          this.Add(key, value);
          reader.ReadEndElement();
      }
      
  }

测试结果为

相关推荐
風清掦2 小时前
【江科大STM32学习笔记-11】SPI通信协议 - 11.2 硬件SPI读写W25Q64
笔记·stm32·单片机·嵌入式硬件·学习
RReality2 小时前
【Unity Shader URP】模板遮罩 / 传送门 实战教程
ui·unity·游戏引擎·图形渲染·材质
sulikey2 小时前
个人Linux操作系统学习笔记1 - Linux权限与工具
linux·笔记·学习
艾莉丝努力练剑3 小时前
【Linux网络】计算机网络入门:Socket编程预备,从字节序共识到 Socket 地址结构的“伪多态”设计
linux·服务器·网络·c++·学习·计算机网络
是烟花哈10 小时前
【前端】React框架学习
前端·学习·react.js
檀越剑指大厂10 小时前
32 万星的面试学习计划 + 内网穿透工具,程序员面试准备效率翻倍!
学习·面试·职场和发展
YangYang9YangYan12 小时前
2026年工作后学习数据分析的价值与路径
学习·数据挖掘·数据分析
qeen8712 小时前
【数据结构】树的基本概念及存储
c语言·数据结构·c++·学习·
老唐77717 小时前
常见经典十大大机器学习算法分类与总结
人工智能·深度学习·神经网络·学习·算法·机器学习·ai