IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> [Unity] 状态机事件流程框架 (二) 设计游戏状态的保存框架,存档功能 ScriptableObject、EasySave -> 正文阅读

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

前文 : ???????[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;
                }
                else
                    Debug.LogError("找不到关于 " + _id + " 的数据类型");
            else
                Debug.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的编辑器拓展,动态管理窗口配置啥的。(下次一定)

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 19:36:23  更:2022-08-19 19:37:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 6:04:26-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码