前文 : ​​​​​​​[Unity] 状态机事件流程框架 (一)

本期来设计一个游戏状态的怎么在游戏中表示和存储。保存游戏状态的目的一是方便根据玩家当前的游戏进度实行各种各样的逻辑分支,二是在存档时能记录实时的游戏数据,方便读档回到存档位置。

效果展示

实现的效果图如下(图为作者参与过项目展示,图一的例子为游戏流程-序章剧情中某一处需要触发摄像机引导的Trigger配置,图二为在框架中自定义游戏状态表示,并可以使用一个Trigger去访问它。其中编辑器窗体由Odin制作,不在本期讨论范围)

最后所有的状态都能被写入磁盘(EasySave实现)

实现方式

如何在游戏中存储状态:

一般来说,我们会使用unity中ScriptableObject来表示游戏中一些数据,方便我们在游戏编辑器下的编辑,但使用ScriptableObject时需要搞清楚几个概念。首先分享一个在M_Studio中背包系统视频下的一条评论:

总结就是:游戏中的数据分为持久化数据非持久化数据。比如一个物品可能由A、B、C三种状态,这里的物品状态列表就可以使用一个ScriptableObject进行存储(即非持久化数据,使用List可进行存储)。当游戏开始运行时,某一刻该物品的状态是B,此时我们要读取状态做判断或者存档操作时,我们不需要知道该物品是否有其他状态(A、C),只需要知道【物品状态->B】的关系就可以了。这个就是需要持久化数据,这种一一对应的关系比较适合用字典方式来实现它。

因此,我们将游戏状态需要的数据分离成可持久化和非持久化,并需要将它们表示在不同的脚本位置。

非持久化数据:状态名(String),拥有的状态列表(List<String>),应放在ScriptableObject中

持久化数据:状态名,当前状态(Dictionary<string,string>),应放在MonoBehaviour脚本上。该脚本一般是拥有单例模式的管理类。

我们先表示以下怎么使用状态的ScriptableObject表示。这里的ValueDropID和ValueDropValue方法主要提供给是在Trigger使用下拉菜单。

     public interface IStatusCheck<TKey,TValue>{List<TKey> SelectID();List<TValue> SelectValue(TKey ID);}public abstract class StatusData : ScriptableObject{public void OnEnable(){key = this.GetType().ToString() + "-" + name;}[Header("请保证key值唯一")]public string key;public abstract List<string> ValueDropID();public abstract List<string> ValueDropValue(string id);}//实现范式版本public class StatusData<TKey, TValue> : StatusData, IStatusCheck<TKey, TValue>{[Header("备注"),TextArea]public string content;[Space]public List<Data> datas;[Serializable]public class Data{public TKey ID;public List<TValue> Values;}public override List<string> ValueDropID(){List<string> retList = new List<string>();foreach (var id in datas){retList.Add(id.ID.ToString());}return retList;}public override List<string> ValueDropValue(string id){List<string> retList = new List<string>();var selectData = datas.Find(x => x.ID.ToString() == id.ToString());if (selectData != null){foreach(var str in selectData.Values)retList.Add(str.ToString());}return retList;}}

这里具体实现游戏状态,使用 StatusData<string, string>进行派生就好啦。

    [CreateAssetMenu(menuName = "新建状态/游戏状态")]public class GameStatusData : StatusData<string, string>{}

随后我们设置对应的Trigger,使用它的下拉方法ValueDropID()和ValueDropValue()

    [System.Serializable]public class Status{public StatusData config;[ValueDropdown(nameof(ValueDropID))]public string id;[ValueDropdown(nameof(ValueDropValue))]public string value;public List<string> ValueDropID(){if (config)return config.ValueDropID();return null;}public List<string> ValueDropValue(){if (config)return config.ValueDropValue(id);return null;}[LabelText("条件为真/假")]public bool isTrue = true;}[AddComponentMenu("Sugarzo触发器/游戏状态触发器")]public class StatusTrigger : BaseTrigger{public List<Status> status = new List<Status>();//还有很多其他设置先省略}

现在看看我们写好的效果:

         嗯嗯,看起来程序运行的十分顺利(?),我们已经正确能在Trigger看到写好的状态并选择他们。很明显,ScriptableObject的数据只存在编辑器中。我们现在的状态数据还没有被装进游戏中。现在应该添加一个MonoBehaviour的管理类脚本,去管理游戏中实时数据了。

作为一个管理类,应该实现什么功能呢?首先应该是一个单例,随后要有设置数据/检查数据的方法,然后是保存数据/读取数据的方法,我们先把接口写出来:

public interface IStatusSave{void LoadData();void SaveData();}public interface IStatusCheck{bool IsStatus(string _data, string _id, string _value);void SetStatus(string _data, string _id, string _value);}

这里的方法IsStatus用了三个参数的版本。id/value自然是标记状态名和具体状态的。data主要是表示该状态位于哪一个ScriptableObject中的数据(这里用了前文中的StatusData.Key)

接着我们注册一个静态类存储一些静态方法,方便我们的Trigger调用:

public static class StatusManager{//存储IStatusCheck的实例public static Dictionary<Type,IStatusCheck> managerInstances = new Dictionary<Type, IStatusCheck>();public static bool IsStatus(StatusData data, string ID, string value){if (managerInstances.ContainsKey(data.GetType()))return managerInstances[data.GetType()].IsStatus(data.key, ID, value);Debug.LogError("找不到关于 " + data.name + " 的管理类实例");return false;}public static void SetStatus(StatusData data, string ID, string value){if (managerInstances.ContainsKey(data.GetType())){Debug.Log("切换游戏状态 " + ID + " -> " + value);managerInstances[data.GetType()].SetStatus(data.key, ID, value);EventManager.EmitEvent(EventEnum.GameStatusChange.ToString());}}}

可以看到设置状态时发送了EventManager.EmitEvent(EventEnum.GameStatusChange.ToString());该事件需要由所有状态Trigger监听,意思时修改完状态时,通过发送信号所有Trigger都会检查当前状态是否满足条件,如果满足就执行Action。

我们回到StatusTrigger实现完其余功能:

    [AddComponentMenu("Sugarzo触发器/游戏状态触发器")]public class StatusTrigger : BaseTrigger{public List<Status> status = new List<Status>();public override void Execute(){if (IsState())base.Execute();}//会在Enable中运行public override void RegisterSaveTypeEvent(){base.RegisterSaveTypeEvent();if(status.Count > 0)EventManager.StartListening(EventEnum.GameStatusChange.ToString(), Execute);}//会在DisEnable中运行public override void DeleteSaveTypeEvent(){base.DeleteSaveTypeEvent();EventManager.StopListening(EventEnum.GameStatusChange.ToString(), Execute);}private bool IsState(){foreach (var statu in status){if (StatusManager.IsStatus(statu.config,statu.id,statu.value) != statu.isTrue)return false;}return true;}}

对应的修改状态的Action:

public class StatusAction : BaseAction{[Header("设置游戏状态")]public List<Status> status = new List<Status>();public override void RunningLogic(){foreach(var sta in status){StatusManager.SetStatus(sta.config, sta.id, sta.value);}RunOver();}[System.Serializable]public class Status{public StatusData config;[ValueDropdown(nameof(ValueDropID))]public string id;[ValueDropdown(nameof(ValueDropValue))]public string value;public List<string> ValueDropID(){if (config)return config.ValueDropID();return null;}public List<string> ValueDropValue(){if (config)return config.ValueDropValue(id);return null;}}}

接着我们就可以写具体实现了接口IStatusSave和IStatusCheckStatusManager的管理类实例了。为了方便扩展这里使用了三个泛型参数。TData被StatusData约束,<TKey, TValue>对应的也是StatusData的数据类型。

在管理类中,我们需要维护两个东西,一个是需要配置在游戏中的数据List<TData> configs,另一个则是实时数据存储的字典了:Dictionary<string, Dictionary<string, string>> configData,我们实时存档的数据都存储在字典中,设置检查状态,读档和存档的操作也是在操作这个类型。

public class StatusManager<TData,TKey, TValue> : MonoBehaviour,IStatusSave, IStatusCheck where TData : StatusData<TKey, TValue>{public virtual bool IsStatus(string _data, string _id, string _value){if(configData.ContainsKey(_data))if (configData[_data].ContainsKey(_id))return configData[_data][_id].Equals(_value);Debug.LogError("找不到关于 " + _id + " 的数据类型");return false;}public virtual void SetStatus(string _data, string _id, string _value){if (configData.ContainsKey(_data))if (configData[_data].ContainsKey(_id)){configData[_data][_id] = _value;}elseDebug.LogError("找不到关于 " + _id + " 的数据类型");elseDebug.LogError("找不到关于 " + _id + " 的数据类型");return;}public List<TData> configs;protected Dictionary<string, Dictionary<string, string>> configData;protected virtual void Awake(){InitData();RegisterStatic(); RegisterSave();}//初始化字典数据protected virtual void InitData(){configData = new Dictionary<string, Dictionary<string, string>>();foreach(var config in configs){configData.Add(config.key, new Dictionary<string, string>());foreach(var cData in config.datas){configData[config.key].Add(cData.ID.ToString(), string.Empty);}}}//注册静态数据protected virtual void RegisterStatic(){StatusManager.managerInstances.Add(typeof(TData), this);}//注册存档事件监听数据protected virtual void RegisterSave(){EventManager.StartListening(EventEnum.GameSave.ToString(), SaveData);EventManager.StartListening(EventEnum.GameLoad.ToString(), LoadData);}public void LoadData(){GameSaveManager.LoadData(this.GetType().ToString(), out configData);}public void SaveData(){GameSaveManager.SaveData(this.GetType().ToString(), configData);}[Sirenix.OdinInspector.Button]public void DebugAllStatus(){foreach (var data in configData){foreach (var cData in data.Value){Debug.Log(cData.Key + " " + cData.Value);}}}}}

使用这个范式基类派生出我们真正需要的GameStatusManager实例:

public class GameStatusManager : StatusManager<GameStatusData,string,string>{}

接着是存档框架,这里使用了ES3.Save和ES3.Load,通过SaveData<T>(string saveKey,T data)的函数签名,可以很方便的存储游戏数据。

    public class GameSaveManager : SingletonMono<GameSaveManager>{public string slotKey = "Save0";protected override void Awake(){base.Awake();GameSaveInstance = new GameSave();}public static GameSave GetGameSave(){return Instance.GameSaveInstance;}[Button,ButtonGroup]public static void SaveGameToSlot(){Debug.Log("存储卡槽存档 " + Instance.slotKey);EventManager.EmitEvent(EventEnum.GameSave.ToString());GetGameSave().slotKey = Instance.slotKey;}[Button, ButtonGroup]public static void LoadGameFromSlot(){Debug.Log("读取卡槽存档 " + Instance.slotKey);EventManager.EmitEvent(EventEnum.GameLoad.ToString());}public static void SaveData<T>(string saveKey,T data){Debug.Log(saveKey + " 保存");ES3.Save(Instance.slotKey + "@" + saveKey,data);}public static void LoadData<T>(string saveKey,out T data){Debug.Log(saveKey + " 读取");data = (T)ES3.Load(Instance.slotKey + "@" + saveKey);}}

好了这里的框架就分析了。虽然感觉是有点乱(?)把这一部分的源码上传到了github,有兴趣的可以参考参考,框架内已内置Odin和EasySave3插件。有问题欢迎讨论GitHub - sugarzo/UnityFrame: 一些unity框架,目前只做到了Trigger/Action/状态表示系统

后面可能还会有几篇文档,可能会讲讲unity的编辑器拓展,动态管理窗口配置啥的。(下次一定)

[Unity] 状态机事件流程框架 (二) 设计游戏状态的保存框架,存档功能 ScriptableObject、EasySave相关推荐

  1. AJAX框架构成设计,基于Ajax的Web框架设计与实现

    摘要: 随着Web技术的发展和人们对Web使用的要求越来越高,传统的交互方式一方面使用户觉得难以使用,体验感太差,另一方面也不利于开发人员进行开发和维护.所以当Web2.0概念被提出,就立即得到了广泛 ...

  2. python开源项目框架二次开发_Python中三大框架各自的应用场景(DJango,flask,Tornado)...

    django:主要是用来搞快速开发的,他的亮点就是快速开发,节约成本,正常的并发量不过10000,如果要实现高并发的话,就要对django进行二次开发,比如把整个笨重的框架给拆掉,自己写socket实 ...

  3. 在java保存游戏状态_保存游戏状态的最佳方法是什么?

    但我听说这种方式有一些问题,不适合储蓄. 没错.在某些设备上,存在一些问题BinaryFormatter..当您更新或更改类时,情况会变得更糟.由于类不再匹配,您的旧设置可能会丢失.有时,由于这个原因 ...

  4. 基于C++的《元素战争》基于win32框架的电脑游戏设计

    资源下载地址:https://download.csdn.net/download/sheziqiong/85628397 一.游戏介绍 ​ <元素战争>是一款基于 win32 框架的电脑 ...

  5. 利用jspxcms框架二次开发遇到的问题

    发版之后里面的文件丢失问题 要在一个利用jspxcms框架二次开发的项目里添加新功能,拉下来代码加上新功能之后,把项目打成war包,放到服务器上重新启动,后来有别的开发人员告诉我,他以前上传的文件和一 ...

  6. RSD 教程 —— §4.2 保存框架

    §4.2 保存框架 上述创建的框架可以以磁盘文件的格式加以保存.保存后的文件称之为框架模板文件.这是一种扩展名为.tpl的二进制文件.按照图4.5的菜单命令可以将当前的框架保存为框架模板文件. 图 4 ...

  7. java游戏界面制作_软件设计之基于Java的连连看小游戏(二)——游戏基础界面的制作及事件的添加...

    上次完成到游戏首页的制作,今天完成了游戏基础界面的制作以及事件的简单添加.由于功能尚未完全实现,因此游戏界面的菜单列表只是简单地添加了一下,其余菜单列表以及倒计时等在后续的制作中逐一完善. 1.首先在 ...

  8. 《游戏开发基础》课程论文丨基于Unity与Bolt的抗美援朝主题3D游戏设计

    文章目录 摘要 关键词 一.导言 二.角色操控 2.1 隐藏鼠标 2.2 通过移动鼠标更新玩家视角 2.3 使角色跳跃 2.4 使角色移动 2.5 使角色冲刺 2.6 使角色卧倒 2.7 设置准星及切 ...

  9. 基于skynet设计游戏服务端框架

    skynet并不是一个开箱即用的服务端框架,游戏后端在开展业务时,需要根据自身业务特点,合理设计相应的服务端框架.在这里我根据自身的设计目标,写下各方面的选择与取舍.对于小型企业来说,一些商业化的软件 ...

最新文章

  1. 【总结】IE6,IE7,IE8,Firefox兼容的css hack 补充!
  2. 如何动态修改下拉列表的默认选中项
  3. Leetcode 398. 随机数索引 解题思路及C++实现
  4. [scala-spark]10. RDD转换操作
  5. Boost:can_require_concepr的使用测试程序
  6. Java源代码分析与生成
  7. logisim实验——通过2个半加器实现1-bit全加器,通过4个一位全加器构成4-bit加法器(详解)
  8. 浅析数据库设计三范式
  9. epson me 1+只有主机能打印不能共享网络打印问题的处理
  10. js图片转二进制流_为了安全问题后台返回前端得文件为二进制得流,我们该如何处理这些流文件...
  11. 专栏 | IBM Watson启示录:AI不应该仅仅是炫技
  12. jni java参数签名,什么是“方法签名”参数调用使用JNI的Java方法?
  13. fasterrcnn论文_论文笔记:Fast(er) RCNN
  14. wallys/IPQ4019/IPQ4029/Access Point Wireless Module Dual band 11AC Wave2 Module
  15. PHP-laravel框架一1
  16. 宾馆客房管理系统的设计与实现
  17. 树莓派Python实现相机控制,定时与画面变化捕捉拍照
  18. 人脸识别活体检测sdk 百度 python_Python百度人脸识别SDK的使用
  19. 《进击的虫师》爬取豆瓣电影海报Top250(2020年10月23日更新)
  20. vs picturebox 图片缩放

热门文章

  1. python亲和数_亲和数(示例代码)
  2. MDK编译过程及ARM编译工具链
  3. 安装图形化界面consol/centos-xfce-vnc
  4. 基于STM32的四足机器人
  5. Dynamics 365 配置IFD的向导界面下一步按钮禁用的解决办法
  6. 活着,要有温暖的感觉
  7. down mark 打钩_Markdown 基本语法
  8. MySQL-Workbench数据库备份
  9. U盘html文件恢复不了,u盘文件突然不见了怎么恢复?恢复小技巧来了
  10. 时空复杂度(时间复杂度/空间复杂度)O(1)、O(n)、O(n^2)、O(log n)、O(n log n)是什么意思