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,可以說,卡頓並不會很明顯。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。