Unity实现游戏存档框架

时间:2021-05-20

最近重构了一下我的存档框架。我在这里对实现方法进行简单的解析。注意这里主要演示算法,所以,效率上并不是最佳。一个游戏中,可能有成百上千个物体需要存储,而且有几十种类型,接下来就用一个简单的例子来解释。一个很简单的例子,有一个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,可以说,卡顿并不会很明显。

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

声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

相关文章