当前位置: 首页 > news >正文

方法合集——第七章

GameSave脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using Google.Protobuf.Gs;
using Google.Protobuf.Collections;
using Debug = UnityEngine.Debug;public class GameSave
{private static GameSave instance = new GameSave();public static GameSave Instance => instance;public string root;public const int SaveID = 1000;public const string bake = "bake";public const string Chest_Root = "Chest_Root";public void Init(){root = Path.Combine(Application.persistentDataPath, "Save");}public void Save(long versionId, string scene){long version = 0;//找到已存档的目录 然后覆盖原有的文件if (versionId > 0){version = versionId;}else{//新建一个档案version = SaveID;// TimeHelper.Now();}var version_root = Path.Combine(root, version.ToString());if (Directory.Exists(version_root) == false){Directory.CreateDirectory(version_root);}//全局文件var v_main = Path.Combine(version_root, "main.txt");//关卡文件var v_scene = Path.Combine(version_root, $"{scene}.txt");//主角数据SaveInfoMain saveInfoMain = new SaveInfoMain();saveInfoMain.Scene = scene;saveInfoMain.Player = SavePlayer(UnitManager.Instance.player);//背包数据SetSaveInfo_BagGrild(saveInfoMain.Grilds);//写入全局数据进行保存var m_data = ProtoHelper.ToBytes(saveInfoMain);File.WriteAllBytes(v_main, m_data);//获取关卡的数据 进行保存SaveLevelInfo saveLevelInfo = new SaveLevelInfo();var npc_root = GameObject.Find("NPC");//场景NPCif (npc_root != null){var fsm = npc_root.transform.GetComponentsInChildren<FSM>(true);if (fsm != null && fsm.Length > 0){foreach (var item in fsm){saveLevelInfo.Ai.Add(item.gameObject.name, SavePlayer(item));}}}//宝箱var chest_root = GameObject.Find(Chest_Root);if (chest_root != null){ChestInfo[] chestInfos = chest_root.GetComponentsInChildren<ChestInfo>(true);if (chestInfos != null && chestInfos.Length > 0){foreach (var item in chestInfos){saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo(){State = item.state});}}}//关卡剩余的挑战时间var t = GameObject.Find("Timer/T001");if (t != null){var t_com = t.GetComponent<Timer>();saveLevelInfo.Time = t_com.GetSaveInfo();}//计时器 todo..var sceneBytes = ProtoHelper.ToBytes(saveLevelInfo);File.WriteAllBytes(v_scene, sceneBytes);}public SavePlayerInfo SavePlayer(FSM fsm){SavePlayerInfo savePlayerInfo = new SavePlayerInfo();if (fsm != null && fsm.yet){savePlayerInfo.State = fsm.currentState.id;savePlayerInfo.Hp = (int)fsm.att_crn.hp;savePlayerInfo.Pos.Add(fsm._transform.position.x);savePlayerInfo.Pos.Add(fsm._transform.position.y);savePlayerInfo.Pos.Add(fsm._transform.position.z);savePlayerInfo.Rot.Add(fsm._transform.eulerAngles.x);savePlayerInfo.Rot.Add(fsm._transform.eulerAngles.y);savePlayerInfo.Rot.Add(fsm._transform.eulerAngles.z);savePlayerInfo.Active = fsm._gameObject.activeSelf;}return savePlayerInfo;}public void SetSaveInfo_BagGrild(MapField<int, SaveGrildInfo> dct){dct.Clear();foreach (var item in BagData.Instance.dct){SaveGrildInfo info = new SaveGrildInfo();info.Id = item.Key;info.PropId = item.Value.id;info.Count = item.Value.count;dct[item.Key] = info;}}public void Load()//long versionId{//var version_root = Path.Combine(root, versionId.ToString());var version_root = Path.Combine(root, SaveID.ToString());//全局文件var v_main = Path.Combine(version_root, "main.txt");var main_bytes = File.ReadAllBytes(v_main);SaveInfoMain saveInfoMain = ProtoHelper.ToObject<SaveInfoMain>(main_bytes);//关卡文件var v_scene = Path.Combine(version_root, $"{saveInfoMain.Scene}.txt");var scene_bytes = File.ReadAllBytes(v_scene);SaveLevelInfo saveLevelInfo = ProtoHelper.ToObject<SaveLevelInfo>(scene_bytes);GameSystem.Instance.SceneController.Load(saveInfoMain.Scene, false, () => {//恢复主角UnitManager.Instance.player.Recover(saveInfoMain.Player);//恢复背包BagData.Instance.Recover(saveInfoMain);//恢复场景的NPC var npc_root = GameObject.Find("NPC");//场景NPCif (npc_root != null){var fsm = npc_root.transform.GetComponentsInChildren<FSM>(true);if (fsm != null && fsm.Length > 0){foreach (var item in fsm){if (saveLevelInfo.Ai.ContainsKey(item.gameObject.name)){var data = saveLevelInfo.Ai[item.gameObject.name];item.Recover(data);}}}}//宝箱var chest_root = GameObject.Find(Chest_Root);if (chest_root != null){ChestInfo[] chestInfos = chest_root.GetComponentsInChildren<ChestInfo>(true);if (chestInfos != null && chestInfos.Length > 0){foreach (var item in chestInfos){if (saveLevelInfo.Chest.ContainsKey(item.gameObject.name)){var data = saveLevelInfo.Chest[item.gameObject.name];Debug.Log(item.gameObject.name + "   " + data.State);item.Recover(data);}//saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo()//{//    State = item.state//});}}}//剩余的关卡时间//关卡剩余的挑战时间var t = GameObject.Find("Timer/T001");if (t != null){var t_com = t.GetComponent<Timer>();if (t_com != null){t_com.Recover(saveLevelInfo.Time);}//saveLevelInfo.Time = t_com.GetSaveInfo();}});}public bool HasSaveVersion(){var version_root = Path.Combine(root, SaveID.ToString());if (Directory.Exists(version_root)){return true;}else{return false;}}internal void NewGame(){var version_root = Path.Combine(root, SaveID.ToString());//bakevar bake_root = Path.Combine(root, bake);//Debug.LogError(bake_root);if (Directory.Exists(bake_root) == false){Directory.CreateDirectory(bake_root);}else{var sub = Directory.GetFiles(bake_root);foreach (var file in sub){File.Delete(file);}}if (Directory.Exists(version_root)){//FileSystem.CopyDirectory(version_root, bake_root, true);var sub = Directory.GetFiles(version_root);foreach (var file in sub){//Debug.LogError(file);//using (var read=new StreamReader(Path.Combine(version_root, file),encoding:System.Text.Encoding.UTF8))//{//    var s = read.ReadToEnd();//    using (var writer = new StreamWriter(Path.Combine(bake_root, file),false, System.Text.Encoding.UTF8)) {//        writer.Write(s);//    }//}File.Copy(file, Path.Combine(bake_root, Path.GetFileName(file)), true);}Directory.Delete(version_root, true);}}}

Init方法

        该方法的作用是 初始化游戏存档的根目录路径,将 root 字段设置为一个平台无关的、持久化存储路径下的 "Save" 子文件夹。

public void Init(){root = Path.Combine(Application.persistentDataPath, "Save");}

1. Application.persistentDataPath 是什么?

        这是 Unity 提供的一个只读属性,返回一个操作系统平台相关的路径,用于存储需要在游戏关闭后仍然保留的数据(如存档、配置、玩家进度等)。

不同平台的典型路径示例:

  • WindowsC:\Users\<用户名>\AppData\LocalLow\<公司名>\<产品名>

这个路径中,Application.persistentDataPath 的值就是:

C:\Users\xiao\AppData\LocalLow\DefaultCompany\My project (1)

2. Path.Combine(..., "Save") 的作用

  • 将 persistentDataPath 与子文件夹名 "Save" 安全地拼接成完整路径。
  • 使用 Path.Combine 而不是字符串拼接(如 + "/" +),是为了自动适配不同操作系统的路径分隔符

3. 为什么需要这个 root

后续的存档操作(如 Save()Load())都会基于这个 root 路径创建或读取文件,例如:

var version_root = Path.Combine(root, "1000"); // → .../Save/1000/
var v_main = Path.Combine(version_root, "main.txt");

所以必须先通过 Init() 确定 root 的值,否则 rootnull

Save方法

这个 Save(long versionId, string scene) 方法是 游戏存档的核心逻辑,它的作用是 将当前游戏进度(包括全局状态和当前关卡状态)序列化并保存到磁盘文件中

存档分为两部分:

  1. 全局数据main.txt):玩家基本信息、背包、当前场景名等。
  2. 关卡数据{scene}.txt):当前场景中的 NPC、宝箱、计时器等动态对象状态
public void Save(long versionId, string scene){long version = 0;//找到已存档的目录 然后覆盖原有的文件if (versionId > 0){version = versionId;}else{//新建一个档案version = SaveID;// TimeHelper.Now();}var version_root = Path.Combine(root, version.ToString());if (Directory.Exists(version_root) == false){Directory.CreateDirectory(version_root);}//全局文件var v_main = Path.Combine(version_root, "main.txt");//关卡文件var v_scene = Path.Combine(version_root, $"{scene}.txt");//主角数据SaveInfoMain saveInfoMain = new SaveInfoMain();saveInfoMain.Scene = scene;saveInfoMain.Player = SavePlayer(UnitManager.Instance.player);//背包数据SetSaveInfo_BagGrild(saveInfoMain.Grilds);//写入全局数据进行保存var m_data = ProtoHelper.ToBytes(saveInfoMain);File.WriteAllBytes(v_main, m_data);//获取关卡的数据 进行保存SaveLevelInfo saveLevelInfo = new SaveLevelInfo();var npc_root = GameObject.Find("NPC");//场景NPCif (npc_root != null){var fsm = npc_root.transform.GetComponentsInChildren<FSM>(true);if (fsm != null && fsm.Length > 0){foreach (var item in fsm){saveLevelInfo.Ai.Add(item.gameObject.name, SavePlayer(item));}}}//宝箱var chest_root = GameObject.Find(Chest_Root);if (chest_root != null){ChestInfo[] chestInfos = chest_root.GetComponentsInChildren<ChestInfo>(true);if (chestInfos != null && chestInfos.Length > 0){foreach (var item in chestInfos){saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo(){State = item.state});}}}//关卡剩余的挑战时间var t = GameObject.Find("Timer/T001");if (t != null){var t_com = t.GetComponent<Timer>();saveLevelInfo.Time = t_com.GetSaveInfo();}//计时器 todo..var sceneBytes = ProtoHelper.ToBytes(saveLevelInfo);File.WriteAllBytes(v_scene, sceneBytes);}

1. 确定存档目录

// 如果 versionId 大于 0,就使用 versionId 作为“版本号”;否则使用 SaveID。
long version = versionId > 0 ? versionId : SaveID;// 把版本号转成字符串,并与根目录 root 拼接,得到形如 “root\123” 的完整路径。
var version_root = Path.Combine(root, version.ToString());// 如果该路径对应的文件夹尚不存在,则立即创建它。
if (!Directory.Exists(version_root))Directory.CreateDirectory(version_root);
  • 最终存档路径示例:
    C:\Users\xiao\AppData\LocalLow\...\Save\1000\
  • 这个目录下会存放:
    • main.txt → 全局数据
    • Level1.txt → 关卡数据(假设当前场景叫 Level1

2. 保存全局数据

把当前场景、玩家、背包等关键数据打包成一个对象,序列化成二进制后写进主存档文件

即:ProtoHelper.ToBytes 把对象“压”成二进制;File.WriteAllBytes 把二进制“落”到磁盘。

saveInfoMain保存内容:

数据项说明
Scene当前场景名,用于下次加载时知道该进哪个关卡
Player玩家状态(HP、位置、旋转、状态机状态、是否激活)
Grilds背包格子数据(物品ID、数量等)
// 1. 创建一个用于保存“主存档数据”的对象
SaveInfoMain saveInfoMain = new SaveInfoMain();// 2. 把当前场景信息写进去
saveInfoMain.Scene = scene;// 3. 把玩家对象转换成可序列化的 Player 数据并赋值
//    SavePlayer 大概率是手写的一个“把运行时 PlayerUnit 转成存档结构”的方法
saveInfoMain.Player = SavePlayer(UnitManager.Instance.player);// 4. 把背包/格子数据也填进去
SetSaveInfo_BagGrild(saveInfoMain.Grilds);// 5. 用 protobuf 把整棵对象树序列化成字节数组
var m_data = ProtoHelper.ToBytes(saveInfoMain);// 6. 一次性写入磁盘,文件路径由变量 v_main 指定(通常是 xxx.sav 或 xxx.dat)
File.WriteAllBytes(v_main, m_data);
File.WriteAllBytes(v_main, m_data);

这里的File是 mscorlib.dll(.NET 底层程序集)里提供的工具类,专门用来一次性完成“文件级”操作:读、写、复制、删除

这里就是将m_data(saveInfoMain 对象经过 Protocol Buffers 序列化后的整块二进制存档。)——把“主存档对象”序列化后的字节流一次性写进 main.txt 文件,完成真正的落盘保存。

3. 保存关卡数据({scene}.txt)

SaveLevelInfo saveLevelInfo = new SaveLevelInfo();

(1)保存 NPC 状态

var npc_root = GameObject.Find("NPC");
// 遍历所有 FSM 组件(代表可存档的 AI/NPC)
foreach (var item in fsm)
{saveLevelInfo.Ai.Add(item.gameObject.name, SavePlayer(item));
}
  • 通过名字(如 "1001")作为 key,保存每个 NPC 的状态。
  • 同样调用 SavePlayer(FSM),说明 NPC 和玩家共用一套状态保存逻辑。

(2)保存宝箱状态

var chest_root = GameObject.Find(Chest_Root); // "Chest_Root"
foreach (var item in chestInfos)
{saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo() { State = item.state });
}
  • 保存每个宝箱是否已被打开(state 字段)。

(3)保存计时器

var t = GameObject.Find("Timer/T001");
if (t != null)
{var t_com = t.GetComponent<Timer>();saveLevelInfo.Time = t_com.GetSaveInfo();
}
  • 保存挑战剩余时间等计时信息。

最后:

var sceneBytes = ProtoHelper.ToBytes(saveLevelInfo);
File.WriteAllBytes(v_scene, sceneBytes);
  • 将关卡数据序列化,写入如 Level1.txt 的文件。

4.保存关卡数据的原理

比如:

  • 某个宝箱已经被打开了 → 下次进来不能还是关着的
  • 某个敌人已经被打败了 → 下次进来不能复活
  • 倒计时还剩 30 秒 → 下次进来不能重新从 60 秒开始

这些不是全局数据(不属于玩家背包或角色属性),而是属于当前关卡(场景)的数据,所以要单独保存

关卡数据GameSaveProto如下:

// 关卡数据:可能包含多个AI、多个宝箱、关卡剩余的时间、多个计时器
message SaveLevelInfo {map<string, SavePlayerInfo> ai = 1; // AImap<string, SaveChestInfo> chest = 2; // 宝箱float time = 3; // 关卡剩余的时间map<int32, SaveTimerInfo> timer = 4; // 计时器的状态/剩余时间
}message SavePlayerInfo {int32 state = 1; // 状态int32 hp = 2; // 当前的血量repeated float pos = 3; // 当前的位置repeated float rot = 4; // 当前的角度bool active =5;//是否激活玩家
}message SaveGrildInfo {int32 id = 1; // 格子IDint32 prop_id = 2; // 道具IDint32 count = 3; // 剩余的数量
}message SaveChestInfo {int32 state = 1; // 状态
}message SaveTimerInfo {int32 state = 1; // 状态int32 time = 2; // 已经开始了多长时间
}

关键点:用 GameObject 的名字作为“唯一标识”

saveLevelInfo.Ai.Add(item.gameObject.name, SavePlayer(item));
saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo { State = item.state });
  • 例如:一个宝箱 GameObject 叫 "Chest_01",它的状态是“已打开”(state = true)
  • 存档时就记下:"Chest_01" → { State: true }
  • 加载时,程序会:
    1. 找到场景中叫 "Chest_01" 的 GameObject
    2. 把 State: true 应用给它 → 宝箱变成打开状态

以宝箱为例,解读关卡数据存档流程

步骤 1:找到所有宝箱

var chest_root = GameObject.Find("Chest_Root");
ChestInfo[] chestInfos = chest_root.GetComponentsInChildren<ChestInfo>(true);
  • 假设场景里有个空物体叫 Chest_Root,下面挂着所有宝箱。
  • GetComponentsInChildren<ChestInfo>(true) 会找到所有(包括未激活的)宝箱组件。

Chest_Root及其内容如图所示

步骤 2:遍历每个宝箱,提取状态

foreach (var item in chestInfos)
{saveLevelInfo.Chest.Add(item.gameObject.name,           // 键:宝箱名字,如 "Chest_01"new SaveChestInfo() {State = item.state          // 值:当前状态(true=已开)});
}

步骤 3:序列化并写入文件

var sceneBytes = ProtoHelper.ToBytes(saveLevelInfo);
File.WriteAllBytes(".../Save/1000/Level1.txt", sceneBytes);
  • 整个 SaveLevelInfo 对象被转成二进制,存进 Level1.txt

SavePlayer方法

将一个由 FSM(有限状态机)控制的游戏对象(如玩家或 NPC)的当前运行状态,转换为可序列化的数据结构 SavePlayerInfo,用于存档

public SavePlayerInfo SavePlayer(FSM fsm)
{// 创建一个空的存档数据对象(用于存储玩家FSM数据)SavePlayerInfo savePlayerInfo = new SavePlayerInfo();// 只有 FSM 存在且已经初始化过(yet=true)才抽数据if (fsm != null && fsm.yet){// 1. 当前状态 IDsavePlayerInfo.State = fsm.currentState.id;// 2. 当前血量(向下取整)savePlayerInfo.Hp = (int)fsm.att_crn.hp;// 3. 世界坐标——依次写入 x y zsavePlayerInfo.Pos.Add(fsm._transform.position.x);savePlayerInfo.Pos.Add(fsm._transform.position.y);savePlayerInfo.Pos.Add(fsm._transform.position.z);// 4. 欧拉角旋转——依次写入 x y zsavePlayerInfo.Rot.Add(fsm._transform.eulerAngles.x);savePlayerInfo.Rot.Add(fsm._transform.eulerAngles.y);savePlayerInfo.Rot.Add(fsm._transform.eulerAngles.z);// 5. GameObject 的激活状态(true/false)savePlayerInfo.Active = fsm._gameObject.activeSelf;}// 即使没数据也会把空结构返回,调用方不用判 nullreturn savePlayerInfo;
}

SetSaveInfo_BagGrild方法

这段代码的作用是:将当前背包中的所有物品数据,转换为可序列化的存档格式(SaveGrildInfo),并填充到传入的字典 dct 中,用于后续整体存档

  • 输入:一个空的或待填充的 MapField<int, SaveGrildInfo>(这是 protobuf 生成的可序列化字典类型)。
  • 输出:该字典dct被填入所有背包格子的数据。
  • 用途:作为 SaveInfoMain.Grilds 的内容,最终写入 main.txt
    public void SetSaveInfo_BagGrild(MapField<int, SaveGrildInfo> dct)
    {dct.Clear();                // 清空旧存档,防止脏数据残留// 遍历运行时背包 BagData.Instance.dct// 假设其结构为 Dictionary<int, BagGrid>,Key 是格子索引,Value 是格子数据foreach (var item in BagData.Instance.dct){SaveGrildInfo info = new SaveGrildInfo();   // 新建一个可序列化的格子信息info.Id     = item.Key;                     // 格子序号info.PropId = item.Value.id;                // 道具配置 idinfo.Count  = item.Value.count;             // 堆叠数量// 写入 protobuf 字典;若键已存在会覆盖,不存在则新增dct[item.Key] = info;}
    }

     Load方法

    从本地磁盘读取之前保存的游戏进度(包括全局数据和当前关卡数据),加载指定场景,并将所有角色、背包、NPC、宝箱、计时器等对象恢复到存档时的状态。

    public void Load()//long versionId{//var version_root = Path.Combine(root, versionId.ToString());var version_root = Path.Combine(root, SaveID.ToString());//全局文件var v_main = Path.Combine(version_root, "main.txt");var main_bytes = File.ReadAllBytes(v_main);SaveInfoMain saveInfoMain = ProtoHelper.ToObject<SaveInfoMain>(main_bytes);//关卡文件var v_scene = Path.Combine(version_root, $"{saveInfoMain.Scene}.txt");var scene_bytes = File.ReadAllBytes(v_scene);SaveLevelInfo saveLevelInfo = ProtoHelper.ToObject<SaveLevelInfo>(scene_bytes);GameSystem.Instance.SceneController.Load(saveInfoMain.Scene, false, () => {//恢复主角UnitManager.Instance.player.Recover(saveInfoMain.Player);//恢复背包BagData.Instance.Recover(saveInfoMain);//恢复场景的NPC var npc_root = GameObject.Find("NPC");//场景NPCif (npc_root != null){var fsm = npc_root.transform.GetComponentsInChildren<FSM>(true);if (fsm != null && fsm.Length > 0){foreach (var item in fsm){if (saveLevelInfo.Ai.ContainsKey(item.gameObject.name)){var data = saveLevelInfo.Ai[item.gameObject.name];item.Recover(data);}}}}//宝箱var chest_root = GameObject.Find(Chest_Root);if (chest_root != null){ChestInfo[] chestInfos = chest_root.GetComponentsInChildren<ChestInfo>(true);if (chestInfos != null && chestInfos.Length > 0){foreach (var item in chestInfos){if (saveLevelInfo.Chest.ContainsKey(item.gameObject.name)){var data = saveLevelInfo.Chest[item.gameObject.name];Debug.Log(item.gameObject.name + "   " + data.State);item.Recover(data);}//saveLevelInfo.Chest.Add(item.gameObject.name, new SaveChestInfo()//{//    State = item.state//});}}}//剩余的关卡时间//关卡剩余的挑战时间var t = GameObject.Find("Timer/T001");if (t != null){var t_com = t.GetComponent<Timer>();if (t_com != null){t_com.Recover(saveLevelInfo.Time);}//saveLevelInfo.Time = t_com.GetSaveInfo();}});

      1.确定存档路径并加载全局数据

      // 1. 拼接存档目录路径:root + 存档版本号(1000)
      var version_root = Path.Combine(root, SaveID.ToString());   // 例如 ".../Save/1000"// 2. 拼接主存档文件完整路径:目录 + main.txt
      var v_main = Path.Combine(version_root, "main.txt");        // 例如 ".../Save/1000/main.txt"// 3. 一次性把磁盘上的 main.txt 全部读进内存,返回 byte[]
      var main_bytes = File.ReadAllBytes(v_main);// 4. 用 protobuf 反序列化,把二进制数据重新变成 SaveInfoMain 对象
      SaveInfoMain saveInfoMain = ProtoHelper.ToObject<SaveInfoMain>(main_bytes);
      • 和 Save() 方法对应,读取的是 Save/1000/ 目录(单存档槽设计)。
      • 反序列化出 SaveInfoMain,包含:
        • Player:玩家存档状态(位置、HP、状态机等)
        • Grilds:背包格子数据
        • Scene上次退出时所在的场景名(如 "Level1"
      • 根据 saveInfoMain.Scene 动态拼出关卡存档文件名(如 Level1.txt)。
      • 反序列化出该关卡的动态对象状态:
        • Ai:NPC 状态字典(key = GameObject 名字)
        • Chest:宝箱状态字典
        • Time:计时器数据

      2.异步加载场景 + 恢复状态

      GameSystem.Instance.SceneController.Load(saveInfoMain.Scene, false, () => {…………// 所有恢复逻辑都在这里执行(如NPC、玩家、宝箱数据)
      });
      • 使用场景管理器异步加载目标场景(避免卡顿)。
      • 回调函数 () => { ... } 在场景完全加载后执行,此时所有 GameObject 已存在,可以安全操作

      2.1恢复状态——恢复玩家状态

      UnitManager.Instance.player.Recover(saveInfoMain.Player);
      • 调用玩家 FSM 的 Recover(SavePlayerInfo) 方法,还原:
        • 位置、旋转
        • HP
        • 状态机当前状态(如 Idle/Run)
        • 激活状态(activeSelf)

      2.2恢复状态——恢复背包物品

      BagData.Instance.Recover(saveInfoMain);
      • 背包系统根据 saveInfoMain.Grilds 重建所有格子的物品和数量

      2.3恢复状态——恢复场景中的 NPC

      // 1. 在场景中查找名为 "NPC" 的根节点(若无则返回 null)
      var npc_root = GameObject.Find("NPC");// 2. 安全获取根节点下所有子对象(包含隐藏)的 FSM 组件数组
      var fsm = npc_root?.GetComponentsInChildren<FSM>(true) ?? new FSM[0];// 3. 遍历每个 FSM(即每个 NPC)
      foreach (var item in fsm)
      {// 4. 如果存档字典里存过该 NPC 的数据,就调用 Recover 恢复其状态if (saveLevelInfo.Ai.ContainsKey(item.gameObject.name)){item.Recover(saveLevelInfo.Ai[item.gameObject.name]);}
      }
      • 关键机制:通过 GameObject 名字匹配存档数据
        • 如果存档中有 "Goblin_01" 的状态,就找到场景中叫 "Goblin_01" 的 FSM 并恢复它。
      • GetComponentsInChildren<FSM>(true) 包含未激活对象,确保死亡的敌人也能恢复为“死亡状态”。

      2.4恢复状态——恢复宝箱状态

      if (saveLevelInfo.Chest.ContainsKey(item.gameObject.name))
      {item.Recover(data); // 比如设置 state = true(已打开)
      }
      • 同样通过名字匹配,将宝箱设为“已打开”或“关闭”状态。

      HasSaveVersion方法

      检查本地是否存在指定 ID(SaveID)的游戏存档。

      public bool HasSaveVersion()
      {// 拼接存档目录路径:root + 版本号(1000)var version_root = Path.Combine(root, SaveID.ToString());// 用 System.IO.Directory 判断目录是否存在if (Directory.Exists(version_root)){return true;   // 存在}else{return false;  // 不存在}
      }

      NewGame方法

      该方法是控制“点击新游戏时”的存档过程

        /// 1. 若不存在 bake 目录则创建;存在则清空其内所有文件;
        /// 2. 若旧存档目录存在,则把旧存档文件全部拷进 bake 做备份,随后整目录删除。
        /// </summary>internal void NewGame()
        {/* 旧存档目录:root\1000 */var version_root = Path.Combine(root, SaveID.ToString());/* 备份目录:root\bake */var bake_root = Path.Combine(root, bake);// 备份目录不存在就新建if (Directory.Exists(bake_root) == false){Directory.CreateDirectory(bake_root);}else{// 已存在则清空里面的文件,避免旧数据残留var sub = Directory.GetFiles(bake_root);foreach (var file in sub){File.Delete(file);}}// 如果旧存档目录存在,则先做备份再删除if (Directory.Exists(version_root)){// 把旧存档目录下的所有文件拷到 bake 目录(同名覆盖)var sub = Directory.GetFiles(version_root);foreach (var file in sub){File.Copy(file, Path.Combine(bake_root, Path.GetFileName(file)), true);}// 整个旧存档目录连文件带子目录一并删除Directory.Delete(version_root, true);}
        }

        1.定义路径

        var version_root = Path.Combine(root, SaveID.ToString()); // 如 Save/1000
        var bake_root = Path.Combine(root, bake);                // 如 Save/bak
        • version_root:当前使用的存档目录(比如 1000 号存档)。
        • bake_root:备份目录

        2.确保备份目录存在,并清空它

        if (Directory.Exists(bake_root) == false)
        {Directory.CreateDirectory(bake_root);
        }
        else
        {var sub = Directory.GetFiles(bake_root);foreach (var file in sub){File.Delete(file); // 清空旧的备份内容}
        }
        • 如果 bak 目录不存在,就创建它。
        • 如果已存在,先删除里面所有文件,确保备份是“干净的”,避免混入上次的备份。

        3.如果有旧存档,先备份再删除

        if (Directory.Exists(version_root))
        {// 遍历 version_root 中的所有文件var sub = Directory.GetFiles(version_root);foreach (var file in sub){// 将每个文件复制到 bak 目录(覆盖同名文件)File.Copy(file, Path.Combine(bake_root, Path.GetFileName(file)), true);}// 复制完成后,彻底删除原存档目录Directory.Delete(version_root, true);
        }
        • 备份:把 Save/1000/main.txtSave/1000/Level1.txt 等全部复制到 Save/bak/
        • 删除:用 Directory.Delete(..., true) 递归删除整个 1000 目录(包括子目录和文件)。

        4.NewGame中的数据流向

        整个流程是:

        1. 先清空 bake 文件夹

        2. 然后,把当前存档(version_root,比如 Save/1000)里的所有文件,复制进这个“刚刚清空”的 bake 文件夹。

          • 源:Save/1000/main.txt → 目标:Save/bak/main.txt
          • 源:Save/1000/Level1.txt → 目标:Save/bak/Level1.txt
        3. 复制完成后,把原来的 Save/1000 整个删掉

        所以,数据流向是:

        Save/1000/ (旧存档)↓ 复制(备份)
        Save/bak/  (临时备份区,内容 = 旧存档的完整副本)↓
        Save/1000/ 被彻底删除

        ProtoHelper方法

        public class ProtoHelper
        {public static byte[] ToBytes(object message){return ((Google.Protobuf.IMessage)message).ToByteArray();}public static T ToObject<T>(byte[] bytes) where T : Google.Protobuf.IMessage,new(){var message = new T();// Activator.CreateInstance<T>();message.MergeFrom(bytes);return message;}public static T Clone<T>(object message) where T : Google.Protobuf.IMessage, new(){return ToObject<T>(ToBytes(message));}}

        ToBytes方法

        万能 protobuf 序列化辅助函数——给任何 IMessage 对象,就吐出对应的二进制存档。

          public static byte[] ToBytes(object message){return ((Google.Protobuf.IMessage)message).ToByteArray();}

        return ((Google.Protobuf.IMessage)message).ToByteArray();

        • 先强制类型转换:把 objectGoogle.Protobuf.IMessage

        • 再调用接口方法 .ToByteArray(),得到 byte[]

        • 立即 return 出去

        用法:

        var sceneBytes = ProtoHelper.ToBytes(saveLevelInfo);

        就是把 saveLevelInfo 这个 protobuf 对象Protocol Buffers 二进制格式 序列化,最终得到一块 byte[](二进制数据),后面就可以写磁盘或发网络。

        Object<T>(byte[] bytes)方法

        • 功能:将字节数组反序列化为指定类型的 protobuf 对象。
        • 约束
          • T 必须实现 IMessage 接口(即是一个 protobuf 消息类型)。
          • T 必须有无参构造函数(new() 约束),以便能通过 new T() 创建实例。
        • 说明
          • 创建一个新实例后,调用 MergeFrom(bytes) 将字节数据解析并填充到该实例中。
          • 这是 protobuf 反序列化的标准做法。
        public static T ToObject<T>(byte[] bytes) where T : Google.Protobuf.IMessage, new(){// 创建空对象var message = new T();// 通过 protobuf 自带的 MergeFrom 把二进制流合并到对象中message.MergeFrom(bytes);return message;}
        

        把二进制数据 bytes 还原成 protobuf 对象。

        1. message.MergeFrom(bytes);

          • 按 Protocol Buffers 格式解析字节数组

          • 把字段值填进 message 的对应属性/成员

          • 若对象里原有数据会被覆盖或合并(protobuf 的合并规则)

        2. return message;

          • 返回已经填充好的对象,供外部直接使用

        合起来就是“反序列化”步骤:字节 → 对象。

        用法:

        SaveInfoMain saveInfoMain = ProtoHelper.ToObject<SaveInfoMain>(main_bytes);
        • main_bytes 是前面 File.ReadAllBytes(v_main) 得到的整块二进制数据

        • ProtoHelper.ToObject<SaveInfoMain> 按 protobuf 格式解析,把字段值填进新创建的 SaveInfoMain 实例

        结果:
        saveInfoMain 里就装着之前存进去的场景名、玩家数据、背包格子等全局信息,后面代码就能直接用了。

         Clone<T>(object message)方法

        public static T Clone<T>(object message) where T : Google.Protobuf.IMessage, new(){return ToObject<T>(ToBytes(message));}

        这是一行代码实现的 “protobuf 深拷贝” 工具:

        1. 先把任意对象序列化成二进制(ToBytes(message)

        2. 立刻把同一段二进制反序列化回一个新的 T 实例(ToObject<T>(...)

        SceneController(更新后的Load方法与LoadSceneAsync

        第三章——SceneController中加载场景相关方法

         在第三章的基础上,修改了导入的参数(新增了切换之后的场景名,是否需要重置,以及场景切换后将触发的事件),另外还用了state参数用于判断当前场景是否位于加载状态

          public void Load(string next, bool reset = false, Action act = null){LoadingViewController.Instance.Open();LoginViewController.Instance.Close();state = 1;StartCoroutine(LoadSceneAsync(next, false, act));}

        LoadSceneAsync方法

        这里自己加了一段:根据切换后场景名称不为Init,才决定是否要加载角色——因为退出并存档时会进入Init场景,照样会调用创建角色的方法,但此场景中没有GatePoint所以会导致报错

        这里需要注意,创建角色需要在当前帧渲染之前否则下一场景的元素还没等加载结束就会出现在加载场景中

        这个 WaitForEndOfFrame 确保:

        • 所有在 op.allowSceneActivation = true; yield return op; 之后的逻辑(包括角色创建、摄像机设置等)已经在当前帧渲染前完成
        • 然后才关闭加载界面,这样 下一帧直接就是完整游戏画面
          IEnumerator LoadSceneAsync(string next, bool reset, Action act){var op = SceneManager.LoadSceneAsync(next);op.allowSceneActivation = false;while (op.progress < 0.9f){yield return new WaitForEndOfFrame();LoadingViewController.Instance.UpdateLoadProgress(op.progress);}float progress = op.progress;while (progress <= 1){progress += GameTime.deltaTime;LoadingViewController.Instance.UpdateLoadProgress(progress);yield return new WaitForEndOfFrame();}op.allowSceneActivation = true;yield return op;// ✅ 统一判断:是否为游戏场景(非 Init)if (next != "Init"){var player = UnitManager.Instance.CreatePlayer();GameSystem.Instance.CameraController.SetTarget(player.transform);MainViewController.Instance.Open();var gate_point = GameObject.Find("GatePoint");if (gate_point != null){var t = gate_point.transform.Find("0");if (t != null){player.GetComponent<FSM>().SetPosition(t);}}}      yield return new WaitForEndOfFrame();LoadingViewController.Instance.Close();GameEvent.OnSceneLoadComplete?.Invoke();if (reset){ResetToLast();}state = 0;act?.Invoke();}

        http://www.dtcms.com/a/482016.html

        相关文章:

      • 定制衣柜厂柔性生产:客户需求拆解、板材切割与组装工序协同路径
      • 厦门外贸网站建设 之家wordpress菜单与顶部互换
      • openrewrite 的rewrite.yml 编写注意事项
      • 系统架构的平衡之道
      • 考研10.2笔记
      • Linux:传输层协议
      • 北京做网站建设的公司有哪些优化网站哪个好
      • 搭建网站工具抚州公司做网站
      • RK3588 + 银河麒麟部署 swarm 集群指南-续(自己应用程序部署)
      • 为什么我选择用 Rust 构建全栈后台管理系统?
      • 一篇文章讲清 UPD协议 与 TCP协议
      • 武邑网站建设价格wordpress 8小时
      • SSM高校职称申报系统337gs(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
      • 深度解析:Linux sudo权限配置中的 %wheel ALL=(ALL:ALL) ALL 到底是什么意思?
      • d3.js:学习积累
      • ESLint
      • 大米CMS支付漏洞复现报告
      • SAP MM采购申请审批接口分享
      • 自定义类型:结构体、联合和枚举
      • iOS 是开源的吗?苹果系统的封闭与开放边界全解析(含开发与开心上架(Appuploader)实战)
      • 网站建设费 项目经费通用网址通用网站查询
      • 知道网站域名怎么联系wordpress插件的安装目录下
      • 网站建设价格与方案wordpress抓取别人网站
      • 服务网格 Service Mesh:微服务通信的终极进化
      • 计算机理论学习Day14
      • Spring Cloud OpenFeign + Nacos 实战教程:像调用本地方法一样调用远程微服务
      • Java求职面试: 互联网医疗场景中的缓存技术与监控运维应用
      • 【论文精读】InstanceCap:通过实例感知提升文本到视频生成效果
      • 如何将 iPhone 同步到新电脑而不会丢失数据
      • yolov8 检测