【Unity小功能开发实战教程】图片资源在Xml文件中序列化与反序列化的一种解决方案

👉一、背景

项目中有引用到外部图片资源的功能(由用户选择),而且当用户下次打开这个项目的时候也要显示用户上次使用过的图片,这就涉及到了一个图片数据的可持久化存储问题。由于项目中的其他数据是保存到XML文件中的,所以本博客仅此记录Unity实现在Xml文件中图片资源的序列化与反序列化问题的过程,并给出Demo验证。

👉二、解决思路及过程

  1. 我一开始的做法是用户选择一张图,通过代码将图片解析成二进制流,然后通过Texture类解析成纹理图,最后创建精灵图赋值到Image组件上。
    (缺点:没有对图片进行缓存,每次选择图片都要重新读取并且实例化,可能会影响性能)
  2. 创建图片管理类SpriteManager,定义字典spriteDic,key为图片名字,value为精灵图。用户第一次选择的时候,解析成图片保存到字典中,公开一个GetSprite方法供外部调用字典里的精灵图,选取图片时先判断字典是否存在同名的key,存在则直接返回sprite,否则就解析。
    (优点:避免重复读取及实例化;缺点:保存项目后再打开图片不会显示(没有完成持久化存储))
  3. 又考虑到可以在图片管理类中增加一个字典,在xml文件中保存图片名和对应路径,这样虽然能实现下次打开项目时初始化将这些图片再解析出来并显示(前提是外部图片路径不会被修改),为了避免这种情况,所以放弃这个想法。
    后来又想到一种可行方法:在项目文件夹下创建一个专门用来保存用户选择的图片,下次打开项目时先遍历这个图片文件夹,解析图片数据并保存。再次打开项目时就不会出现图片丢失的情况了。看上去功能像是实现了。(缺点:这样做的结果是将项目数据跟图片数据从物理上“分离”了,如果项目xml文件迁移了,而图片文件夹没有一起迁移,那么下次打开这个项目还是读取不到上次选择过的相关图片资源。而且图片越多,图片文件夹越大,虽然不占用包体,但不好管理。)
  4. 于是又得重新思考别的方法。那么有什么方法既能实现需求,又能保证项目xml文件与图片资源“融为一体”呢?答案之一就是将图片保存到xml文件中。对,你没有看错,图片保存到xml中!具体思路就是:把用户选择的图片解析成字节流,除了需转为sprite供界面显示之外,还需将其字节流转为字符串的形式保存到xml文件中(序列化过程);下次打开项目xml时,读取保存的图片节点信息,将图片字符串数据转为字节流,再将字节流转为精灵图sprite显示到图片组件上(反序列化过程)。
    刚开始是用户没选择一张图片就保存一张图片的字符串,考虑到可能会出现保存图片数据冗余的情况,将保存代码优化:如果多个图片组件使用用一张图片,那么xml中仅保存一份该图片的字符串数据。
  5. 后面又考虑到图片名字不能代表图片内容是一致的(同一个图片可能存在改名的情况),所以需要获取到图片的唯一标识符MD5作未图片数据的保存对象。

所以最终定下的解决方案是:封装图片数据到SpriteData类中,成员为Sprite(精灵图)、ImgStr(图片字符串)。在图片管理类中定义spriteDataDics字典来保存图片的md5值和SpriteData对象,序列化时遍历该字典保存到xml中,反序列时
解析xml数据保存到字典中,这样就保证了下次打开该项目文件还能显示相关使用过的图片。

👉三、原理

在这里插入图片描述

xml序列化:图片byte[]→string字符串→(写入)xml。
xml反序列化:xml→(读取)string字符串→byte[]→Texture/Sprite。

👉四、图片反序列与反序列化Demo验证

1、搭建验证demo

在这里插入图片描述

  • demo场景由一个面板Panel、两个按钮Button(序列化和反序列化图片按钮)、滚动视图ScrollView和三个图片组件Image组成。
  • demo中使用到了本地资源浏览器插件Crosstales.FileBrowser。该插件使用和获取教程:FileBrowser教程。使用该插件还需引用dll:System.Windows.Forms。
  • 创建的脚本SpriteManager,挂载到Panel上,注意拖拽赋值,脚本代码如下文。
2、demo完整代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Xml;//解析xml引入的命名空间
using Crosstales.FB;//文件资源浏览器需引用的命名空间,需要插件
using System.Security.Cryptography;//获取图片文件md5值需引用的命名空间

//精灵图数据
public class SpriteData
{
    public string ImgStr { get; set; }
    public Sprite Sprite { get; set; }

    public SpriteData(string imgStr,Sprite sprite)
    {
        this.ImgStr = imgStr;
        this.Sprite = sprite;
    }
}

//图片管理类
public class SpriteManager : MonoBehaviour
{

    public Button serializeBtn;//序列化图片按钮
    public Button deserializeBtn;//反序列化图片按钮
    public Transform imgContent;//场景中图片组件的父物体

    //缓存图片md5和精灵图数据的字典
    private Dictionary<string,SpriteData> spriteDataDics = new Dictionary<string,SpriteData>();
    //图片组件和赋值的图片md5做一个映射关系的字典(用来知道哪个物体赋值了哪张图)
    private Dictionary<string, string> imgObjMaps = new Dictionary<string, string>();

    private void Awake()
    {
        for (int i = 0; i < imgContent.childCount; i++)
        {
            Image img = imgContent.GetChild(i).GetComponent<Image>();
            Button imgBtn = img.GetComponent<Button>();
            imgBtn.onClick.AddListener(()=>
            {
                SelectImagePath(img);
            });
        }
        
        serializeBtn.onClick.AddListener(SerializeImg);
        deserializeBtn.onClick.AddListener(DerializeImg);

    }

    //通过资源浏览器选择一张本地图片
    private void SelectImagePath(Image img)
    {
        ExtensionFilter[] extensions = new ExtensionFilter[] {new ExtensionFilter("Image Files", "jpg", "png")  };
        string imgPath = FileBrowser.OpenSingleFile("请选择一张图片", "", extensions);
        if (File.Exists(imgPath))
        {
            string md5 = "";
            LoadImage(imgPath, ref md5);
            img.sprite = spriteDataDics[md5].Sprite;
            if (imgObjMaps.ContainsKey(img.name))
            {
                imgObjMaps[img.name] = md5;
            }
            else
            {
                imgObjMaps.Add(img.name, md5);
            }
        }
    }

    //解析图片
    private void LoadImage(string path, ref string md5)
    {
        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
        var crypto = MD5.Create();
        var md5Hash = crypto.ComputeHash(fs);//计算md5
        //D4-1D-8C-D9-8F-00-B2-04-E9-80-09-98-EC-F8-42-7E,这是通过BitConverter转的md5字符串,所以需要将其去掉“-”再转为小写形式
        md5 = System.BitConverter.ToString(md5Hash).Replace("-", "").ToLower();
        if (!spriteDataDics.ContainsKey(md5))
        {
            fs.Seek(0, SeekOrigin.Begin);//如果计算了md5的值,此时文件流的指针指向最后一位,如果还需要读取流,则需将指针返回文件头
            byte[] imgBytes = new byte[fs.Length];
            fs.Read(imgBytes, 0, imgBytes.Length);
            string imgStr = ByteToStr(imgBytes);
            Sprite sprite = ByteToSprite(imgBytes);
            AddTospriteDataDics(md5, imgStr,sprite);
        }
        fs.Close();
    }
   

    /// <summary>
    /// 添加精灵图数据到缓存字典
    /// </summary>
    /// <param name="md5"></param>
    /// <param name="imgBytes"></param>
    private void AddTospriteDataDics(string md5,string imgStr,Sprite sprite)
    {
        SpriteData spriteData = new SpriteData(imgStr, sprite);
        spriteDataDics.Add(md5, spriteData);
    }

    //字节数组转为一种Base64编码形式的字符串
    private string ByteToStr(byte[] bytes)
    {
        return System.Convert.ToBase64String(bytes);
    }

    //将Base64编码字符串转为字节数组
    private byte[] StrToByte(string str)
    {
        return System.Convert.FromBase64String(str);
    }

    //字节数组转精灵图
    private Sprite ByteToSprite(byte[] bytes)
    {
        Texture2D tex = new Texture2D(10,10);
        tex.LoadImage(bytes);
        Sprite sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
        return sprite;
    }

    //字符串转精灵图
    private Sprite StrToSprite(string str)
    {
        byte[] imgBytes = StrToByte(str);
        return ByteToSprite(imgBytes);
    }

    //序列化图片
    private void SerializeImg()
    {
        string savePath = FileBrowser.SaveFile("请选择保存路径", "", "imgXml", "xml");
        if (!string.IsNullOrEmpty(savePath))
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.CreateXmlDeclaration("1.0", "utf-8", "");
            xmlDoc.AppendChild(SerializeImgXmlNode(xmlDoc));
            xmlDoc.Save(savePath);
        }
    }
    private XmlNode SerializeImgXmlNode(XmlDocument doc)
    {
        XmlNode imgNode = doc.CreateElement("Image");
        //保存图片组件与图片的映射关系
        foreach (string objName in imgObjMaps.Keys)
        {
            XmlElement objNode = doc.CreateElement("Obj");
            objNode.SetAttribute("img", objName);
            objNode.InnerText = imgObjMaps[objName];
            imgNode.AppendChild(objNode);
        }
        //保存图片的md5值和字符串
        foreach (string item in spriteDataDics.Keys)
        {
            XmlElement itemElement = doc.CreateElement("ImageData");
            itemElement.SetAttribute("md5", item);
            itemElement.InnerText = spriteDataDics[item].ImgStr;
            imgNode.AppendChild(itemElement);
        }
        return imgNode;
    }

    //反序列化图片
    private void DerializeImg()
    {
        string openPath = FileBrowser.OpenSingleFile("请选择一个xml文件", "", "xml");
        if (File.Exists(openPath))
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(openPath);
            DerializeImgXmlNode(doc.FirstChild);
        }
    }

    private void DerializeImgXmlNode(XmlNode node)
    {
        foreach (XmlNode nodeItem in node.ChildNodes)
        {
            switch (nodeItem.Name)
            {
                case "Obj":
                    {
                        string objName = nodeItem.Attributes["img"].InnerText;
                        string md5 = nodeItem.InnerText;
                        imgObjMaps.Add(objName, md5);
                    }
                    break;
                case "ImageData":
                    {
                        string md5 = nodeItem.Attributes["md5"].Value;
                        string imgStrData = nodeItem.InnerText;
                        Sprite sprite = StrToSprite(imgStrData);
                        AddTospriteDataDics(md5, imgStrData, sprite);
                    }
                    break;
            }
        }
        //反序列完成之后显示上次选择的图片
        foreach (string objName in imgObjMaps.Keys)
        {
            if (spriteDataDics.ContainsKey(imgObjMaps[objName]))
            {
                GameObject.Find(objName).GetComponent<Image>().sprite = spriteDataDics[imgObjMaps[objName]].Sprite;
            }
        }
    }
}
3、demo演示

序列化图片:
请添加图片描述
我们打开序列化图片保存的xml文件可以看到:成功的将图片组件和图片的md5的映射关系保存下来了;图片数据md5值和对应的字符串也保存下来了。
在这里插入图片描述

反序列化图片:
请添加图片描述
通过反序列化图片打开之前保存的项目xml文件,成功的还原了我们上次选择过的图片。

👉五、未来想要优化的问题

  1. 计算md5值的时候,我是直接计算的整个图片文件的字节数组得出的md5,目前来说读取0到10兆内的图片没发现有卡顿或者说读取慢的情况。更大的图片文件暂时没测试过。后面可以看看,如果发现读取慢,可以考虑只计算图片字节数组的前面、或者后面的固定长度的字节数据,或者随机选择指定长度的字节数据进行计算md5值。
  2. 通过Xml方法的序列化与反序列化,确实可读性较强,可以在无其他环境时使用;但随着使用图片资源增多,会导致体积庞大。所以未来可以考虑用其他方法进行序列化与反序列保存(如二进制、Json等),比较后再择优选择方案。
  3. 当然也可以尝试将数据保存到网络服务器中,不过服务器这块暂时不是很熟悉,但这是未来想学习并尝试的方案。

未完待续…

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
Unity序列化(Serialization)是指将对象转换为字节流的过程,而反序列化(Deserialization)则是将字节流转换为对象的过程。Unity提供了一些机制来实现对象的序列化反序列化Unity序列化机制主要用于保存和加载游戏对象的状态,或者在网络传输传递对象。以下是一些常见的序列化反序列化方法: 1. Unity的内置序列化Unity提供了内置的序列化机制,使得你可以将脚本的变量标记为可序列化。通过在变量前面添加 `[SerializeField]` 属性,可以将该变量标记为可序列化。例如: ```csharp [SerializeField] private int score; ``` 2. XML 和 JSON 序列化Unity还支持使用XML或JSON格式进行序列化反序列化。你可以使用 `System.Xml.Serialization` 命名空间下的类来进行XML序列化反序列化,或者使用JsonUtility类来进行JSON的序列化反序列化。 ```csharp // XML序列化反序列化示例 using System.Xml.Serialization; // 序列化XML XmlSerializer serializer = new XmlSerializer(typeof(MyClass)); using (StreamWriter writer = new StreamWriter("data.xml")) { serializer.Serialize(writer, myObject); } // 从XML反序列化 using (StreamReader reader = new StreamReader("data.xml")) { MyClass myObject = (MyClass)serializer.Deserialize(reader); } // JSON序列化反序列化示例 using UnityEngine; using UnityEngine.Networking; // 序列化为JSON string json = JsonUtility.ToJson(myObject); // 从JSON反序列化 MyClass myObject = JsonUtility.FromJson<MyClass>(json); ``` 3. 二进制序列化:如果需要更高效的序列化反序列化操作,可以使用二进制格式。Unity提供了BinaryFormatter类来进行二进制的序列化反序列化。 ```csharp // 二进制序列化反序列化示例 using System.Runtime.Serialization.Formatters.Binary; // 序列化为二进制 BinaryFormatter formatter = new BinaryFormatter(); using (FileStream stream = new FileStream("data.bin", FileMode.Create)) { formatter.Serialize(stream, myObject); } // 从二进制反序列化 using (FileStream stream = new FileStream("data.bin", FileMode.Open)) { MyClass myObject = (MyClass)formatter.Deserialize(stream); } ``` 这些是Unity常用的序列化反序列化方法,你可以根据具体的需求选择适合的方法来实现对象的序列化反序列化

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周周的Unity小屋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值