Unity实现游戏存档框架

 更新时间:2020年01月19日 10:14:42   作者:hackerzhuli  
这篇文章主要为大家详细介绍了Unity实现游戏存档框架,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

最近重构了一下我的存档框架。我在这里对实现方法进行简单的解析。注意这里主要演示算法,所以,效率上并不是最佳。一个游戏中,可能有成百上千个物体需要存储,而且有几十种类型,接下来就用一个简单的例子来解释。一个很简单的例子,有一个Unit(单位)类型,有一个Inventory(背包)类型,有一个Item(道具)类型。

接下来先介绍框架中最重要的接口,ISavable,表示这个类型可以存档

public interface ISavable{
 uint Id {get; set;}
 Type DataType {get;} // 存档数据类型
 Type DataContainerType {get;} // 存档数据容器类型
 void Read(object data);
 void Write(object data);
}

ISavableContainer,用来返回一组ISavable的容器:

public interface ISavableContainer{
  IEnumerable<ISavable> Savables;
}

IId, 具有Id的接口:

public interface IId
{
  uint Id {get; set;}
}

SaveEntity, 这是一个MonoBehaviour,将这个组件放到需要存档的GameObject上就可以实现该GameObject的存档了,这是最核心的类之一:

public class SaveEntity : MonoBehaviour{
  public void Save(SaveDataContainer container){
    foreach(ISavable savable in GetSavables()){
      if(savable.DataContainerType = container.GetType()){
        IId newData = Activator.CreateInstance(savable.DataType) as IId;
        newData.Id = savable.Id;
        savable.Write(newData);
        container.SetData(newData);
      }
    }
  }
 
  public void Load(SaveDataContainer container){
    foreach(ISavable savable in GetSavables()){
      if(savable.DataContainerType = container.GetType()){
        IId data = container.GetData(savable.Id);
        savable.Read(data);
      }
    }    
  }
 
  public IEnumerable<ISavable> GetSavables(){
    foreach(ISavable savable in GetComponents<ISavable>()){
      yield return savable;
    }
    foreach(ISavable savableContainer in GetComponents<ISavableContainer>()){
      foreach(ISavable savable in savableContainer.Savables){
        yield return savable;
      }
    }
  }
}

SaveFile代表一个文件

[Serializable]
public class SaveFileData{
  public uint CurId;
  public string DataContainer;
}
 
// 代表一个存档文件
public class SaveFile: MonoBehaviour{
  // 包含实际数据的数据类
  private SaveDataContainer _saveDataContainer;
  private uint _curId;
 
  public string Path{get;set;}
  public SaveDataContainer SaveDataContainer{get{return _saveDataContainer;}}
 
  private uint NextId{get{return ++_curId;}}
 
  // 得到场景里所有的SaveEntity
  private IEnumerable<SaveEntity> GetEntities(){
    // 实现略过
  }
  
  // 将场景物体中的数据存入到_saveDataContainer中
  public void Save<T>() where T:SaveDataContainer, new()
  {
    // 一轮Id赋值,保证Id为0的所有ISavable都赋值一个新Id
    foreach(SaveEntity entity in Entities){
      foreach (Savable savable in entity.GetSavables()){
        if(savable.DataContainerType == typeof(T)){
          if(savable.Id == 0){
            savable.Id = NextId;
          }
        }
      }
    }
 
    T dataContainer = new T();
 
    foreach(SaveEntity entity in Entities){
      entity.Save(this, dataContainer);
    }
 
    _saveDataContainer = dataContainer;
  }
 
  // 将_saveDataContainer中的数据载入到场景物体中
  public void Load(){
    foreach(SaveEntity entity in Entities){
      entity.Load(this, _saveDataContainer);
    }
  }
 
  public void LoadFromFile<T>() where T:SaveDataContainer
  {
    string json = File.ReadAllText(Path);
    SaveFileData data = JsonUtility.FromJson<SaveFileData>(json);
    _saveDataContainer = JsonUtility.FromJson<T>(data.DataContainer);
    _curId = data.CurId;
  }
 
  public void SaveToFile(){
    SaveFileData data = new SaveFileData();
    data.CurId = _curId;
    data.DataContainer = JsonUtility.ToJson(_saveDataContainer);
    string json = JsonUtility.ToJson(data);
    File.WriteAllText(Path, json);
  }
}

SaveDataContainer:

// 这个类型存储了实际的数据,相当于是一个数据库
[Serializable]
public class SaveDataContainer{
  // 这个中存储这实际物体的数据,需要将这个字典转换成数组并序列化
  private Dictionary<uint, IId> _data;
 
  public Dictionary<unit, IId> Data{get{return _data}}
 
  public IId GetData(uint id){
    return _data[id];
  }
 
  public void SetData(IId data){
    _data[data.Id] = data;
  }
}

好了,框架就讲到这里,接下来实现示例代码:

Unit:

[Serializable]
public class UnitSave:IId{
  [SerializeField]
  private uint _id;
  public uint PrefabId;
  public uint InventoryId;
  public int Hp;
  public int Level;
  public uint Id {get{return _id;}set{_id = value;}}
}
 
public class Unit:MonoBehaviour, ISavable{
  public int Hp;
  public int Level;
  public int PrefabId;
  public Inventory Inventory;
  
  public uint Id{get;set;}
  ISavable.DataType{get{return typeof(UnitSave);}}
  ISavable.DataContainerType{get{return typeof(ExampleSaveDataContainer);}}
  ISavable.Read(object data){
    UnitSave save = data as UnitSave;
    Hp = save.Hp;
    Level = save.Level;
  }
 
  ISavable.Write(object data){
    UnitSave save = data as UnitSave;
    save.Hp = Hp;
    save.Level = Level;
    save.InventoryId = Inventory.Id;
  }
}

Inventory: 

[Serializable]
public class InventorySave:IId{
  [SerializeField]
  private uint _id;
  public uint UnitId;
  public uint[] Items;
  public uint Id{get{return _id;}set{_id = value;}}
}
 
public class Inventory:MonoBehaviour, ISavable, ISavableContainer{
  public Unit Unit;
  public List<Item> Items;
 
  public uint Id{get;set;}
  ISavable.DataType{get{return typeof(InventorySave);}}
  ISavable.DataContainerType{get{return typeof(ExampleSaveDataContainer));}}
  ISavable.Read(object data){
    // 空
  }
  ISavable.Write(object data){
    InventorySave save = data as InventorySave;
    save.UnitId = Unit.Id;
    save.Items = Items.Select(item => item.Id).ToArray();
  }
 
  ISavableContainer.Savables{
    return Items;
  }
}

Item:

[Serializable]
public ItemSave: IId{
  [SerializeField]
  private uint _id;
  public uint PrefabId;
  public int Count;
  public uint Id{get{return _id;}set{_id = value;}}
}
 
// 道具并不是继承自MonoBehaviour的,是一个普通的类
public class Item:ISavable{
  // 道具源数据所在Prefab,用于重新创建道具
  public uint PrefabId;
  public int Count;
  public uint Id {get;set;}
 
  public uint Id{get;set;}
  ISavable.DataType{get{return typeof(ItemSave);}}
  ISavable.DataContainerType{get{return typeof(ExampleSaveDataContainer));}}
  ISavable.Read(object data){
    ItemSave save = data as ItemSave;
    Count = save.Count;
  }
  ISavable.Write(object data){
    ItemSave save = data as ItemSave;
    save.PrefabId = PrefabId;
    save.Count = Count;
  }
}

ExampleSaveDataContainer:

[Serializable]
public class ExampleSaveDataContainer: SaveDataContainer, ISerializationCallbackReceiver {
  public UnitSave[] Units;
  public ItemSave[] Items;
  public InventorySave[] Inventories;
 
  public void OnBeforeSerialize(){
    // 将Data字典中的数据复制到数组中,实现略过
  }
 
  public void OnAfterDeserialize(){
    // 将数组中的数据赋值到Data字典中,实现略过
  }
}

ExampleGame:

public class ExampleGame:MonoBehaviour{
 
  public void LoadGame(SaveFile file){
    // 从文件中读入数据到SaveDataContainer
    file.LoadFromFile<ExampleSaveDataContainer>();
    SaveDataContainer dataContainer = file.SaveDataContainer;
 
    // 创建所有物体并赋值相应Id
    Unit[] units = dataContainer.Units.Select(u=>CreateUnit(u));
    Item[] items = dataContainer.Items.Select(item=>CreateItem(item));
 
    // 将道具放入相应的道具栏中
    foreach(Unit unit in units){
      uint inventoryId = unit.Inventory.Id;
      InventorySave inventorySave = dataContainer.GetData(inventoryId);
      foreach(Item item in items.Where(i=>inventorySave.Items.Contains(i.Id))){
        unit.Inventory.Put(item);
      }
    }
 
    // 调用Load进行实际的数据载入
    file.Load();
  }
 
  public void SaveGame(SaveFile file){
    // 相对来说,存档的实现比载入简单了许多
    file.Save<ExampleSaveDataContainer>();
    file.SaveToFile();
  }
 
  public Unit CreateUnit(UnitSave save){
    Unit unit = Instantiate(GetPrefab(save.PrefabId)).GetComponent<Unit>();
    unit.Id = save.Id;
    unit.Inventory.Id = save.InventoryId;
    return unit;
  }
 
  public Item CreateItem(ItemSave save){
    Item item = GetPrefab(save.PrefabId).GetComponent<ItemPrefab>().CreateItem();
    item.Id = save.Id;
    return item;
  }
}

使用方法:

给单位Prefab中的Unit组件和Inventory组件所在的GameObject上放SaveEntity组件即可。

思考问题:

1.扩展功能,让SaveFile包含一个SaveDataContainer数组,这样子可以实现包含多个数据容器(数据库)的情况
2.对SaveFile存储内容进行压缩,减少存储体积
3.SaveFile存储到文件时进行加密,避免玩家修改存档
4.如何避免存储时候卡顿 

存储过程:

1.从场景中搜集数据到SaveFile中(SaveFile.Save),得到一个SaveFileData的数据
2.将SaveFileData序列化成一个json字符串
3.对字符串进行压缩
4.对压缩后的数据进行加密
5.将加密后的数据存储于文件 

可以发现,只要完成第1步,得到一个SaveFileData,实际上就已经完成了存档了,接下来实际上就是一个数据转换的过程。所以,这也给出了避免游戏卡顿的一种方法:

完成第一步之后,将后面的步骤全部都放到另一个线程里面处理。实际上,第一步的速度是相当快的。往往不会超过50ms,可以说,卡顿并不会很明显。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • C#/VB.NET实现在Word文档中添加页眉和页脚

    C#/VB.NET实现在Word文档中添加页眉和页脚

    页眉位于文档中每个页面的顶部区域,常用于显示文档的附加信息;页脚位于文档中每个页面的底部的区域,常用于显示文档的附加信息。今天这篇文章就将为大家展示如何以编程的方式在在 Word 文档中添加页眉和页脚
    2023-03-03
  • C#中使用Override和New关键字进行版本控制

    C#中使用Override和New关键字进行版本控制

    在 C# 中,override 和 new 关键字用于控制类之间的成员方法的隐藏和重写,理解它们之间的差异和使用场景对于设计灵活且易于维护的代码至关重要,在这篇博客中,我们将详细探讨这两个关键字的用法,并通过示例来说明它们的实际应用,需要的朋友可以参考下
    2024-10-10
  • C#异步编程详解

    C#异步编程详解

    本文主要介绍异步编程中Task、Async和Await的基础知识。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • C# TreeView控件使用技巧汇总

    C# TreeView控件使用技巧汇总

    这篇文章主要介绍了C# TreeView控件使用技巧汇总,TreeView控件在窗体应用里面使用也是频率比较高的,我们在使用TreeView一般是对资源的分层展示,类似数据结构里面树的凹入表示法
    2022-08-08
  • C# 使用Log4net添加日志记录的方法

    C# 使用Log4net添加日志记录的方法

    本文主要介绍了C# 使用Log4net添加日志记录的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • C#批量插入数据到Sqlserver中的三种方式

    C#批量插入数据到Sqlserver中的三种方式

    这篇文章主要为大家详细介绍了C#批量插入数据到Sqlserver中的三种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • c# 配置文件App.config操作类库的方法

    c# 配置文件App.config操作类库的方法

    下面小编就为大家带来一篇c# 配置文件App.config操作类库的方法。小编觉的挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12
  • c# 引用Nlog插件的步骤

    c# 引用Nlog插件的步骤

    这篇文章主要介绍了c# 引用Nlog插件的步骤,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-04-04
  • C#常用日期时间方法汇总

    C#常用日期时间方法汇总

    这篇文章介绍了C#常用的日期时间方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • C#删除UL LI中指定标签里文字的方法

    C#删除UL LI中指定标签里文字的方法

    这篇文章主要介绍了C#删除UL LI中指定标签里文字的方法,涉及C#针对页面HTML元素进行正则匹配与替换的相关操作技巧,需要的朋友可以参考下
    2017-05-05

最新评论