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,可以说,卡顿并不会很明显。

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

(0)

相关推荐

  • Unity3D游戏开发数据持久化PlayerPrefs的用法详解

    小编今天研究了在Unity3D中的数据持久化问题.数据持久化在任何一个开发领域都是一个值得关注的问题,小到一个应用中配置文件的读写,大到数据库的管理维护,都可以看到数据持久化的身影.小编在<C#基于Linq和反射实现数据持久化框架Xml4DB>这篇文章中曾介绍了博主在寒假期间开发的Xml4DB框架,这是一个基于Xml的轻量级数据持久化框架,可以采用面向对象的方式来处理数据.数据持久化从某种意义上来说,就是序列化和反序列化化的过程.在.NET中我们可以将对象序列化为Xml.Json.二进制.然后

  • Unity实现游戏存档框架

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

  • 通过Java修改游戏存档的实现思路

    目录 前言 一.实现思路 二.项目准备 1. 创建maven工程 2. 导入依赖 三.核心代码 1. 使用的对象 2. 修改关卡信息 3. 修改金币信息 四.代码测试 1. 读取数据文件 2. 修改关卡位置 3. 修改金币数量 4. 退出修改器 5. 输入参数错误情况 五.源码 1. 项目结构 2. 项目代码 总结 前言 植物大战僵尸的数据文件是存储在本地的dat文件当中,修改在本地的dat文件就可以修改到游戏中的数据.之前使用二进制编码工具Hex Editor Neo实现了修改植物大战僵尸的本

  • Unity查找游戏物体的六种方式详解

    一篇小白也能看懂的查找游戏物体的方式解析 – Unity 之 查找物体的几种方式.本文通过实际测试得出使用结论,大家进行简单记录,在使用时想不起来可以再来看看,多用几次基本就没有问题了. 一,Object.Find() Object.Find():根据名称找到游戏对象并返回它. void ObjectFind() { // 找父级 GameObject parent = GameObject.Find("GameObject"); Debug.Log("找父级物体,是否找到:

  • C1任务01之修改游戏存档的方法

    目录 挑战 一.任务实现工具: 游戏:植物大战僵尸中文版 十六进制编辑器:Hex Editor Neo 二.任务实现过程: 1.修改关卡 2.修改金币 3.修改用户名 4.修改局内文字信息 总结 挑战 有的玩家在玩游戏时,可能并不想⼀关⼀关地慢慢玩,⽽是希望可以直接跳到最后⼀关,或者从⾃⼰指定的关卡开始,⽐如 经典的<植物⼤战僵⼫>:再就是怎么能够「开挂」,得到更多的⾦钱.更⾼的属性.更强的道具,⽐如<三国志>系列.任何游戏都会保存玩家的进度和数据,不管是单机游戏还是⽹络游戏都是如

  • Unity实现游戏伤害数字显示HUD的方法

    目录 伤害数字显示HUD Demo展示 HUDPopup类 伤害数字显示HUD 游戏中收到伤害掉血,会有飘动的伤害数值: 可以使用OnGUI中GUI.Label来实现: 可自定义字体,颜色,大小等: 如果需要更好看的数字特效,可以手动添加: 普通字体不够好看可以使用插件FontEditor自定义: Demo展示 HUDPopup类 飘血数字类,创建一个空物体,将这个脚本挂上去,再将这个物体拖成预制体: public class HUDPopup : MonoBehaviour { //目标位置

  • Unity制作游戏自定义按键详解

    目录 一.效果图 二.布局 1.场景布局 2.设置面板布局 三.脚本思路 1.KeyItem脚本 2.SetKeyPanle脚本 3.player移动脚本 一.效果图 二.布局 1.场景布局 创建一个Panel 创建三个cube,Panel地板 两个cube设置一个绿色材质,调整Scale大小让其成为柱子形状,一个cube改名为player设置一个红色材质,当作玩家(用来演示操作的),修改相机位置就可以了. 2.设置面板布局 2.1新建一个空节点名字改为SetKeyPanle,修改属性 2.2在

  • Unity存储游戏数据的多种方法小结

    目录 1 PlayerPrefs: Unity自带的一种简单的键值存储系统 2 ScriptableObject: Unity中最灵活的数据管理工具 2.1 如何手动创建和修改数据文件 2.2 ScriptableObject优缺点总结 3 JSON: 轻量级的数据交换格式 3.1 序列化与反序列化 3.2 用JsonUtility对对象进行序列化和反序列化 4 XML:一种可扩展标记语言 5 三者特点总结 6 数据库:存储大量数据时使用的一种方法 1.安装SQLite插件 2.创建数据库和表

  • Unity实现游戏卡牌滚动效果

    最近项目中的活动面板要做来回滚动卡牌预览效果,感觉自己来写的话,也能写,但是可能会比较耗时,看到Github上有开源的项目,于是就借用了,Github的资源地址,感谢作者的分享. 本篇博客旨在告诉大家如何利用这个插件. 插件的核心在于工程中的6个脚本,以下是六个脚本的源码: DragEnhanceView.cs using UnityEngine; using System.Collections; using UnityEngine.UI; using UnityEngine.EventSys

  • Unity实战之FlyPin(见缝插针)小游戏的实现

    目录 一.简单介绍 二.FlyPin (见缝插针)游戏内容与操作 三.游戏代码框架 四.知识点 五.游戏效果预览 六.实现步骤 七.工程源码地址 八.延伸扩展 一.简单介绍 Unity 游戏实例开发集合,使用简单易懂的方式,讲解常见游戏的开发实现过程,方便后期类似游戏开发的借鉴和复用. 本节介绍,FlyPin (见缝插针) 休闲小游戏快速实现的方法,希望能帮到你,若有不对,请留言. 二.FlyPin (见缝插针)游戏内容与操作 1.游戏开始,针 Pin 自动准备好, 2.鼠标点击左键发射 Pin

  • Unity实现3D射箭小游戏

    Unity 小游戏:3D射箭,供大家参考,具体内容如下 前两周因为实训太忙,再加上自己对老师所讲的设计模式并不是很理解,所以就没有写博客.这次博客是记录3D射箭游戏的实现过程. 1. 准备资源 我是在网上找的弓与箭的资源,至于靶子,创建五个不同大小的同心圆柱体,如图所示: 需要注意的是,五个圆柱体并不在同一个平面上,这样才能够看清每一环的颜色,并且在检测碰撞时不会出现各种问题. 另外,如果靶子放得离相机太近,就没有射箭的感觉了:离相机太远,好像又看不清靶子了,然后我试着把靶子Material的S

随机推荐