存档系统

存档系统简介

将游戏数据持久化的一种方式,方便玩家读取进度而不必从新开始游戏。常用的持久化方式有三种,一是透过Unity自带的存档方式PlayerPrefs,二是通过二进制格式器将对象写入二进制文件,三是通过Json,将对象序列化成Json格式并将字符串写入文件。本章仅介绍框架中所使用到的二进制方法。

系统结构设计

设计SaveManager作为实际的存档读写和存档获取的入口。类似的,不继承MonoBehavior使用static。在框架的设计中,存档有两类,一类是用户型存档,存储这某个游戏用户的具体信息,如血量,武器,游戏进度等。一类是设置型存档,与任何用户存档无关,是通用的存储信息,比如屏幕分辨率,音量设置等。故可分两个文件夹进行不同类型的存档数据存储,由于用户不可对游戏存档结构进行更改,所以两个文件夹命名是确定的,故使用const的string即可。存档存放路径也由此确定(存档路径的根目录有计算机系统的持久化路径Application.persistentDataPath决定)。设计readonly的string在初始化时获取默认存储路径并和文件夹名字进行字符串拼接。

public static class SaveManager
{
  	//存档所在的文件夹命名
  	//设置所在的文件夹命名,常规情况下由存档管理器自行维护
	private const string saveDirName = "saveData";
  	private const string settingDirName = "setting";
  	//存档文件夹路径,在初始化的时候获取
  	private static readonly string saveDirPath;
  	private static readonly string settingDirPath;
  
  	private static SaveManager()
	{
	  	//拼接字符串,存档即放置在该文件夹路径下
		saveDirPath = Application.persistentDataPath + "/" + saveDirName;
	  	settingDirPath = Application.persistentDataPath + "/" + settingDirName;
	  	
	  	//确保路径存在
	  	if(Directory.Exists(saveDirPath) == false)
		{
			Directory.CreateDirectory(saveDirPath);
		}
	  	if(Directory.Exists(settingDirPath) == false)
		{
			Directory.CreateDirectory(settingDirPath);
		}
	  
	  	//初始化SaveManagerData(后续补充)
	  	InitSaveManagerData();
	}
}

注:为了防止游戏过程中删除文件夹导致无法获得路径的情况。初始化,游戏进行中都需要确保路径的存在,才对文件进行读写。

为了解决上述问题,设计GetSavePath以便复用:检查是否存在该存档路径,若无则创建。该框架选择使用二进制的方式保存数据,存档相关操作都离不开数据的保存和加载,故设计LoadFile和SaveFile作为工具函数供框架内部使用。(实际用户存档SaveItem和设置存档SaveSetting存数据对于工具函数底层写入文件是不做区分其原因在于存的时候只需要传一个object转成二进制流放到指定位置,取得时候传泛型T和其他参数即可锁定位置且知道要将object强转成什么类型的存档数据

//BinaryFormatter可以复用,在外面定义即可
private static BinaryFormatter binaryFormatter = new BinaryFormatter();

private static void SaveFlie(object saveObject, string path)
{
  	//打开文件模式OpenOrCreate:若有则打开若无则创建
	FileStream f = new FileStream(path, FileMode.OpenOrCreate);
  	//二进制的方式把对象写入文件,文件流用完需要关闭
  	binaryFormatter.Serialize(f, saveObject);
  	f.Dispose();
}

private static T LoadFile<T>(string path) where T : class
{
  	//为了防止文件流报错,需要文件路径进行判空
	if(!File.Exits(path))
	{
		return null;
	}
  	FileStream file = new FileStream(path, FileMode.Open);
  	//将内容解码成对象
  	T obj = (T)binaryFormatter.Deserialize(file);
  	file.Dispose();
  	return obj;
}

private static string GetSavePath(int saveID, bool createDir = true)
{
  	//验证是否有某个存档
  	if (GetSaveItem(saveID) == null) throw new Exception("JK:saveID 存档不存在!");
	string saveDir = saveDirPath + "/" + saveID;
  	//确定文件夹是否存在
  	if(!Directory.Exists(saveDir))
	{
	  	//若不存在根据参数createDir创建文件夹
		if(createDir)
		{
			Directory.CreateDirectory(saveDir);
		}
	  	else
		{
			return null;
		}
	}
  	return saveDir;
}

支持多存档功能,由于存档内容保存在本地文件,List中的存档仅作为存档信息载体,并存在外部访问的可能性(类似GameMAnager或者UI)故在外部设计SaveItem存档类,包含ID和最后更新时间(参考游戏中存档列表)。需要一个容器对存档进行收纳管理,考虑到玩家一般存档数量为0-50之间(相对而言数量并不大),故使用List作为容器而不是Dictionary(性能消耗可忽略不计)。SaveManager还需要一个全局数据:用于递增当前存档ID,并持有所有的存档数据List。设计一个内部类SaveManagerData,并在构造函数中对SaveManagerData进行初始化。

//存档管理器的设置数据
[Serializable]
private class SaveManagerData
{
  	//当前存档ID
	public int currID = 0;
  	//收纳所有存档的列表
  	public List<SaveItem> saveItemList = new List<SaveItem>();
}


//一个存档的数据
public class SaveItem()
{
  	//saveID只允许由存档管理器来决定,更新时间亦是如此
	public int saveID { get; private set; }
  	public DataTime lastSaveTime { get; private set; }
  
  	public SaveItem(int saveID, DataTime lastSaveTime)
	{
		this.saveID = saveID;
	  	this.lastSaveTime = lastSaveTime;
	}
  
  	//有时候只需要更新存档的更新时间
  	public void UpdateTime(DataTime lastSaveTime)
	{
		this.lastSaveTime = lastSaveTime;
	}
}

同时持有缓存功能,避免频繁的从本地加载一个对象,消除性能隐患。由于存储结构为Application.persistentDataPath/存档ID/具体存储文件,设计Dictionary嵌套对存档进行存储。

// 存档中对象的缓存字典 
// <存档ID,<文件名称,实际的对象>>
private static Dictionary<int, Dictionary<string, object>> cacheDic = new Dictionary<int, Dictionary<string, object>>();

对象

将某个具体对象写入某个存档、从某个存档读取具体对象。设计SaveObject和LoadObject方法实现该功能,并提供多种重载提高灵活度。以具体对象写入某个存档为例,一个保存操作需要参数有object对象,通过GetSavePath根据存档ID获取存档路径string和自定义string文件名进行字符串拼接即可得到文件路径。故参数为(object saveObject, string saveFileName, int saveID),随后调用函数更新各种属性。LoadObject首先对缓存字典进行查找(避免频繁从本地加载导致的性能隐患),若无则从本地加载,随后调用函数更新各种属性。

public static void SaveObject(object saveObject, string saveFileName, int saveID)
{
	//存档路径
  	string dirPath = GetSavePath(saveID, true);
  	//具体对象文件路径
  	string savePath = dirPath + "/" + saveFileName;
  	SaveFile(saveObject, savePath);
  	//修改存档更新时间
  	//更新缓存列表
  	//更新SaveManagerData并写入磁盘
  	GetSaveItem(saveID).UpdateTime(DataTime.Now);
  	SetCache(saveID, saveFileName, saveObject);
  	UpdateSaveManagerData();
}

public static T LoadObject<T>(string saveFileName, int saveID) where T : class
{
  	//查询缓存字典
	T obj = GetCache<T>(saveID, saveFileName);
  	if(obj == null)
	{
		//从本地加载
	  	string dirPath = GetSavePath(saveID);
	  	if(dirPath == null) return null;
	  	string savePath = dirPath + "/" + saveFileName;
	  	obj = LoadFile<T>(string savePath);
	  	//更新缓存
	  	SetCache(saveID, saveFileName, obj);
	}
  	return obj;
}

提供多种重载,其中存在逻辑复用和函数调用。

public static void SaveOjbect(object saveObject, int saveID = 0)
{
	SaveOjbect(saveObject, saveObject.GetType().Name, saveID);
}
public static void SaveOjbect(object saveObject, SaveItem saveItem)
{
	SaveOjbect(saveObject, saveObject.GetType().Name, saveItem);
}
public static T LoadObject<T>(int saveID = 0) where T : class
{
	return LoadObject<T>(typeof(T).Name, saveID);
}
public static T LoadObject<T>(string saveFileName, SaveItem saveItem) where T : class
{
	return LoadObject<T>(saveFileName, saveItem.saveID);
}
public static T LoadObject<T>(SaveItem saveItem) where T : class
{
	return LoadObject<T>(typeof(T).Name, saveItem.saveID);
}

缓存

缓存字典持有增、删、查三种功能。设计对应的SetCache,RemoveCache,GetCache方法,依据字典结构设计参数。其本质是将存档object存入字典置于内存中,删除缓存则通过id把整个存档字典删除即可。

//设置缓存
public static void SetCache(int saveID, string fileName, object saveObject)
{
	//检查缓存中有无saveID这个键
  	if(cacheDic.ContainsKey(saveID))
	{
		//检查存档中是否有fileName为命名的对象
	  	if(cacheDir[saveID].ContainsKey(fileName))
		{
			cacheDir[saveID][fileName] = saveObject;
		}
	  	else
		{
			cacheDir[saveID].Add(fileName, saveObject);
		}
	}
  	else
	{
		cacheDir.Add(saveID, new Dictionary<string, object>(){ {fileName,saveObject} });
	}
}

//获取缓存
public static T GetCache<T>(int saveID, string fileName) where T : class
{
	if(cacheDic.ContainsKey(saveID))
	{
		if(cacheDir[saveID].ContainsKey(fileName))
		{
			return (T)cacheDir[saveID][fileName];
		}
	  	else
		{
			return null;
		}
	}
  	else
	{
		return null;
	}
}

//移除缓存
public static void RemoveCache(int saveID)
{
	cacheDir.Remove(saveID);
}

存档

在存档部分实质是对List<SaveItem>进行操作。主要有添加、删除、获取三个功能,通过saveID或传对象对存档进行查找,设计CreateSaveItem,DeleteSaveItem和GetSaveItem三种方法,并对其设计方法重载(saveID或传对象)。注:添加存档要依据SaveManagerData数据,删除实质是将本地文件删除,其还涉及缓存和SaveManagerData的更新!

//添加一个存档
public static SaveItem CreateSaveItem()
{
  	//根据全局数据.currID创建存档对象并放入List容器收纳
	SaveItem saveItem = new SaveItem(saveManagerData.currID, DataTime.Now);
  	saveManagerData.saveItemList.Add(saveItem);
  	saveManagerData.currID += 1;
  	//更新SaveManagerData并写入磁盘
  	UpdateSaveManagerData();
  	
  	return saveItem;
}

//重载
public static void DeleteSaveItem(SaveItem saveItem)
//删除一个存档
public static DeleteSaveItem(int saveID)
{
	string dirPath = GetSavePath(saveID, false);
  	if(dirPath != null)
	{
	  	//把这个存档下的文件递归删除
		Directory.Delete(itemDir, true);
	}
  	//移除缓存
  	RemoveCache(saveID);
  	//更新全局数据
  	saveManagerData.saveItemList.Remove(GetSaveItem(saveID));
  	UpdateSaveManagerData();
}

//获取一个存档
public static SaveItem GetSaveItem(int id)
{
	for (int i = 0; i < saveManagerData.saveItemList.Count; i++)
	{
		if (saveManagerData.saveItemList[i].saveID == id)
		{
			return saveManagerData.saveItemList[i];
		}
	}
	return null;
}

存档设置

提供初始化方法InitSaveManagerData对存档管理器数据进行初始化和更新方法UpdateSaveManagerData对数据进行更新和写入磁盘。参考游戏中存档列表(可按创建时间顺序,最后更新时间顺序对存档进行排列)提供GetAllSaveItemByCreateTime和GetAllSaveItemByUpdateTime(依据实现IComparer类并重写Compare方法的OrderByUpdateTimeComparer类)。

public static void InitSaveManagerData()
{
  	//先读取,若无在创建,防止残留
	saveManagerData = LoadFile<SaveManagerData>(saveDirPath + "/SaveManagerData");
  	if(saveManagerData == null)
	{
		saveManagerData = new SaveManagerData();
	  	UpdateSaveManagerData();
	}
}

public static void UpdateSaveManagerData()
{
	SaveFile(saveManagerData, saveDirPath + "/SaveManagerData");
}

//创建时间最新的在前
public static List<SaveItem> GetAllSaveItemByCreateTime()
{
  	//保持原List不变,创建新List对原List进行拷贝排序
  	//确定List长度,无需在途中拓展
	List<SaveItem> saveItems = new List<SaveItem>(saveManagerData.saveItemList.Count);
	for (int i = 0; i < saveManagerData.saveItemList.Count; i++)
	{
		saveItems.Add(saveManagerData.saveItemList[saveManagerData.saveItemList.Count - (i+1)]);
	}
	return saveItems;
}

//最新更新的在前
public static List<SaveItem> GetAllSaveItemByUpdateTime()
{
	List<SaveItem> saveItems = new List<SaveItem>(saveManagerData.saveItemList.Count);
	for (int i = 0; i < saveManagerData.saveItemList.Count; i++)
	{
		saveItems.Add(saveManagerData.saveItemList[i]);
	}
	OrderByUpdateTimeComparer orderBy = new OrderByUpdateTimeComparer();
	saveItems.Sort(orderBy);
	return saveItems;
}
private class OrderByUpdateTimeComparer : IComparer<SaveItem>
{
	//实现并重写Compare
	public int Compare(SaveItem x, SaveItem y)
	{
		if (x.lastSaveTime>y.lastSaveTime)
		{
			return -1;
		}
		else
		{
			return 1;
		}
	}
}

附带获取所有存档的万能解决方案,继承如上所有功能对外提供自定义比较依据Function的万能GetAllSaveItem泛型方法。

/// 获取所有存档
/// 最新的在最后面
public static List<SaveItem> GetAllSaveItem()
{
	return saveManagerData.saveItemList;
}

/// 获取所有存档
/// 万能解决方案
public static List<SaveItem> GetAllSaveItem<T>(Func<SaveItem, T> orderFunc, bool isDescending = false)
{
	if (isDescending)
	{
		return saveManagerData.saveItemList.OrderByDescending(orderFunc).ToList();
	}
	else
	{
		return saveManagerData.saveItemList.OrderBy(orderFunc).ToList();
	}
}

全局设置

全局数据部分持有加载和保存设置两种操作,设计LoadSetting泛型方法和SaveSetting即可。

/// <summary>
/// 加载设置,全局生效,不关乎任何一个存档
/// </summary>
public static T LoadSetting<T>(string fileName) where T : class
{
	return LoadFile<T>(settingDirPath + "/" + fileName);
}

/// <summary>
/// 加载重载
/// </summary>
public static T LoadSetting<T>() where T : class
{
	return LoadSetting<T>(typeof(T).Name);
}

/// <summary>
/// 保存设置,全局生效,不关乎任何一个存档
/// </summary>
public static void SaveSetting(object saveObject, string fileName)
{
	SaveFile(saveObject, settingDirPath + "/" + fileName);
}

/// <summary>
/// 保存重载
/// </summary>
public static void SaveSetting(object saveObject)
{
	SaveSetting(saveObject, saveObject.GetType().Name);
}

JKFrame v1.0 文章被收录于专栏

个人学习用

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务