[Unity背包系统]2.存档背包与载入存档功能

存档背包与载入存档功能

提要

  • 上一节中,一个基本的背包系统已经实现了,尽快还很多有待完善的地方(比如鼠标拖拽,双击使用等)。这一节来尝试存档背包,并且载入已存档的背包功能
  • 在此之前先来了解一些关于Json的知识

Json

JSON是比XML更简单的一种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。
其语法规则如下:
(1)使用键值对( key:value )表示对象属性和值。
(2)使用逗号(,)分隔多条数据。
(3)使用花括号{}包含对象。
(4)使用方括号[ ]表示数组。
在JavaScript语言中,一切皆是对象,所以任何支持的类型都可以通过JSON来表示,如字符串、数字、对象、数组等。其中,对象和数组是比较特殊且常用的两种类型。

  • 比如,有这么一个类:
public class student
{
    string name;
    int age;
}
  • 假设它有一个实例,name为XiaoMing,age为18,我们可以利用一些api将这个类转为Json格式:
{"name":"XiaoMing","age":18}

-这就是Json,转为Json格式后可以更方便地进行数据交互(键值对),而在Unity中,为了实现游戏存档,记录各种各样不同的玩家信息,便会用到Json格式,也就是说会将各种各样的对象转化为Json格式存储,也有相应的api:JsonUtility.ToJson(),接下来便会用到它,来存储我们之前的ScriptableObject

存储ScriptableObject的一些问题

  • 之前的InventoryObject中,其中的BackPack我们设定的是存储InventorySlot类型,没什么问题,JsonUtility也能够将其转成Json格式,但是在InventorySlot中这个item也是一个ScriptableObject,也就是说当ScriptableObject里又套了一个ScriptableObject时,存储就会出现一些问题,看看这种情况下InventoryObject的Json格式:

  • 显然,虽然InventoryObject被成功转为了Json格式,BackPack这个数组也被成功拆开了,但是item(ItemObject)却是以instanceID的形式存储的,这个instanceID是Unity中为实例赋予的ID,但是并不具有持久化——关闭编辑器再打开编辑器时很可能就无法按照存档找到之前的数据了,为了解决这个问题,这里提供一种思路——给InventorySlot添加一个ID属性,并自行给每一个物品资源创建一个ID对应关系,存档时就记录这个ID,读取存档时也通过这个ID来读取对应物品。存储这个对应关系我们也使用ScriptableObject,创建一个新脚本:ItemDatabaseObject,并继承ScriptableObject

创建对照数据库

  • 首先来思考到底要做什么:写完这个脚本后我们会通过ItemDatabaseObject来创建一个Database的asset,把所有的物品资源拖入其中,并且为它们生成独一无二的ID,所以我们需要一个ItemObject类型的数组,使得我们可以放入所有的物品资源,同时需要两个Dictionary——一个是物品资源和其ID的对照,用于存储背包时用ID来代替每个物品,另一个是ID和物品资源的对照,用于读档时载入物品时通过记录的ID来找到对应物品:
[CreateAssetMenu(fileName = "new ItemDatabase",menuName = "Inventory System/Items/Database")]
public class ItemDatabaseObject : ScriptableObject,ISerializationCallbackReceiver
{
    
    public ItemObject[] Items;
    public Dictionary<ItemObject, int> GetId = new Dictionary<ItemObject, int>();
    public Dictionary<int, ItemObject> GetItem = new Dictionary<int, ItemObject>();
	}
  • 为什么需要这个ItemObject数组,而不是直接拖到两个Dictionary?这是因为上一节也提到过的原因:Unity不会为某些类,结构自动序列化,所以为了让Dictionary得到数据,我们需要学习一个特殊的接口:ISerializationCallbackReceiver
  • 先看官方文档中的描述:

在序列化和反序列化时接收回调的接口。
Unity 的序列化器能够序列化大多数(并非全部)数据类型。对于不支持的数据类型,Unity 提供了两个供您手动处理它们的回调函数,以便 Unity 能够序列化和反序列化这些数据类型。
在任何类型的操作期间,都可能需要进行序列化。例如,使用 Instantiate() 克隆对象时,Unity 会对原始对象进行序列化和反序列化,以便查找对原始对象的内部引用,并将它们替换为对克隆对象的引用。在这种情况下,您也可以使用上述回调来更新任何使用了 Unity 无法序列化的类型的内部引用。
该回调接口仅用于类。不用于结构。

  • 也就是说,实现这个接口的一些方法后,我们就可以自定义序列化一些数据类型,像Dictionary,我们无法在Inspector窗口中编辑或者赋值,所以我们暴露了一个ItemObject数组,通过对它进行编辑赋值,进而给Dictionary数据。我们先实现接口:
public class ItemDatabaseObject : ScriptableObject,ISerializationCallbackReceiver
  • 实现该接口的两个方法:
    public void OnAfterDeserialize()
    {
    }

    public void OnBeforeSerialize()
    {
    }
  • 第一个是Unity在反序列化该对象之后调用,第二个方法是在序列化该对象之前调用,我们需要的是第一个:
    public void OnAfterDeserialize()
    {
        GetId = new Dictionary<ItemObject, int>();
        GetItem = new Dictionary<int, ItemObject>();
        for (int i = 0; i < Items.Length; i++)
        {
            GetId.Add(Items[i], i);
            GetItem.Add(i, Items[i]);
        }
    }
  • 写好了ItemDatabaseObject后,像之前一样,创建资源:ItemDatabase,并且将所有的物品资源都拖入,根据刚刚的代码,它们的编号就是数组的索引

使用Database

  • 写好了这个查找ID的Database后,我们改写InventorySlot,为其增加一个成员变量ID,并改写构造方法:
[System.Serializable]
public class InventorySlot
{
    public ItemObject item;
    public int amount;
    public int ID;

    public InventorySlot(int id,ItemObject item,int amount)
    {
        ID = id;
        this.item = item;
        this.amount = amount;
    }

    public void AddAmount(int value)
    {
        amount += value;
    }

}
  • 我们将会在InventoryObject中编写保存和载入存档背包的功能,因此需要引入刚刚的Database:public ItemDatabaseObject Database;,在Inspector窗口中拖入刚刚创建的Database资源
  • 改写InventoryObject的AddItem()方法:
    public void AddItem(ItemObject item,int amount)
    {
        for(int i = 0 ; i < BackPack.Count; i ++ )
        {
            if(BackPack[i].item == item)
            {
                BackPack[i].AddAmount(amount);
                return;
            }
        }
        BackPack.Add(new InventorySlot(Database.GetId[item],item,amount));//ID通过Database来获取
    }
  • 这样初步就设定好了,在开始做存档和载入存档前,

Save()与Load()

PlayPrefs

  • PlayPrefs是Unity提供的可以用于存储和读取玩家数据的公共类,它是利用键值对来进行存储的,下面我们直接上手,在InventoryObject中编写Save()和Load()方法:
      public void Save()
    {
        string saveData = JsonUtility.ToJson(this, true);//将当前(this)的InventoryObject转化为json格式
        PlayerPrefs.SetString("Inventory",saveData);//存储,键为Inventory,值为刚刚的json
        PlayerPrefs.Save();//存储的命令
    }

    public void Load()
    {
        if (PlayerPrefs.HasKey("Inventory"))
        {
            JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString("Inventory"), this);//读取PlayerPrefs文件,覆写当前的InventoryObject
        }
    }
  • 现在大体功能就做的差不多了,最后再加上键盘控制的代码:
    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            inventory.Save();
        }

        if(Input.GetKeyDown(KeyCode.O))
        {
            inventory.Load();
        }

    }
  • 可以进行游戏测试了,已经大体完成了,最后再插一个小细节,我们可以再用一次自定义序列化的接口,做到这样的功能:

  • 在BackPack中,限制物体与ID的对应,不能随意更改物品,而是输入对应的ID才可以,你可以自己试试看,让InventoryObject像之前一样实现接口ISerializationCallbackReceiver:

    public void OnAfterDeserialize() 
    {
        for(int i = 0; i < BackPack.Count; i ++ )
            BackPack[i].item = Database.GetItem[BackPack[i].ID];
    }

    public void OnBeforeSerialize()
    {

    }

本文作者:皓月当空的时候

本文链接:https://www.cnblogs.com/Haron/articles/17425160.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   月牙同学  阅读(363)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起