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 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 GetSavables(){
        foreach(ISavable savable in GetComponents()){
            yield return savable;
        }
        foreach(ISavable savableContainer in GetComponents()){
            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 GetEntities(){
        // 实现略过
    }
    
    // 将场景物体中的数据存入到_saveDataContainer中
    public void Save() 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() where T:SaveDataContainer
    {
        string json = File.ReadAllText(Path);
        SaveFileData data = JsonUtility.FromJson(json);
        _saveDataContainer = JsonUtility.FromJson(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 _data;

    public Dictionary 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 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();
        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();
        file.SaveToFile();
    }

    public Unit CreateUnit(UnitSave save){
        Unit unit = Instantiate(GetPrefab(save.PrefabId)).GetComponent();
        unit.Id = save.Id;
        unit.Inventory.Id = save.InventoryId;
        return unit;
    }

    public Item CreateItem(ItemSave save){
        Item item = GetPrefab(save.PrefabId).GetComponent().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,可以说,卡顿并不会很明显。

 

你可能感兴趣的:(游戏开发)