Unity3D RPG Core | 33 保存数据

这节介绍使用 Unity 自带的 PlayerPrefs 机制保存游戏数据。

1. 实现保存功能

我们新建一个 C# 脚本,命名为 SaveManager,它也是一个单例类。

如代码清单 1 所示,我们编写了基础的 Save() 和 Load() 函数,它针对于所有类型的类。我们通过 JsonUtility.ToJson 来将类转化成 json 字符串;JsonUtility.FromJsonOverwrite 将 json 字符串中的内容覆盖到现有类上。PlayerPrefs.SetStringPlayerPrefs.GetString 可以写入字符串和获取字符串,即可以用于我们生成的 json 文本。PlayerPrefs.Save 用于保存数据,实现可持久存储。

视频中是把键的名字用对象名称指定。本地实验下来发现,预制体实例化出来的对象名称会额外加上 "Clone" 字样,导致键不匹配。

这边将键名称写死。

代码清单 1 实现保存功能
  1. public class SaveManager : Singleton<SaveManager>
  2. {
  3.     // Update is called once per frame
  4.     void Update()
  5.     {
  6.         if (Input.GetKeyDown(KeyCode.S))
  7.         {
  8.             SavePlayerData();
  9.             Debug.Log("SavePlayerData");
  10.         }
  11.         else if (Input.GetKeyDown(KeyCode.L))
  12.         {
  13.             LoadPlayerData();
  14.             Debug.Log("LoadPlayerData");
  15.         }
  16.     }
  17.  
  18.     protected override void Awake()
  19.     {
  20.         base.Awake();
  21.         DontDestroyOnLoad(this);
  22.     }
  23.  
  24.     public void SavePlayerData()
  25.     {
  26.         PlayerDataScriptableObject playerData = GameManager.GetInstance().m_player.m_characterData.m_playerData;
  27.         Save(playerData, "PlayerData");
  28.  
  29.         AttackDataScriptableObject attackData = GameManager.GetInstance().m_player.m_attackData.m_attackData;
  30.         Save(attackData, "PlayerAttackData");
  31.     }
  32.  
  33.     public void LoadPlayerData()
  34.     {
  35.         PlayerDataScriptableObject playerData = GameManager.GetInstance().m_player.m_characterData.m_playerData;
  36.         Load(playerData, "PlayerData");
  37.  
  38.         AttackDataScriptableObject attackData = GameManager.GetInstance().m_player.m_attackData.m_attackData;
  39.         Load(attackData, "PlayerAttackData");
  40.     }
  41.     void Save(object obj, string key)
  42.     {
  43.         string json = JsonUtility.ToJson(obj);
  44.         PlayerPrefs.SetString(key, json);
  45.         PlayerPrefs.Save();
  46.     }
  47.  
  48.     void Load(object obj, string key)
  49.     {
  50.         if (PlayerPrefs.HasKey(key))
  51.         {
  52.             string json = PlayerPrefs.GetString(key);
  53.             JsonUtility.FromJsonOverwrite(json, obj);
  54.         }
  55.     }
  56. }

代码清单 1 中的 SavePlayerData() 和 LoadPlayerData() 用于存储和加载角色信息数据,包含人物数值和攻击数值。需要注意的是,作用的类需要是 ScriptableObject

需要注意读写需要作用在 ScriptableObject 类上。

发现之前跟着教程的做法,针对 ScriptableObject 的封装有点冗余。

1.1 保存测试

在代码清单 1 中,我们在 Update() 函数中编写测试代码:按下 S 键保存,按下 L 键加载。我们可以在进入游戏后保存人物数据。然后打怪让人物信息发生变化后,再加载之前保存的数据,看界面上 UI 是否按预期显示。

在 Windows 系统上,PlayerPrefs 相关内容存储在注册表上。如图 1 所示,我们可以找到地方,具体查看一下键值。

图1 PlayerPrefs 注册表位置

2. 完善场景切换逻辑

切换不同场景时,人物是通过预制体实例化出来的,所以需要同步传送前的人物数据信息。

如代码清单 2 所示,我们在场景加载之前保存当前人物角色数据,到了新场景后再加载之前保存的数据,达到数据同步的效果。

特别需要注意时序的问题,需要是这样的时序:保存人物数据;人物对象 Disable,取消 GameManager 的注册;生成新人物对象,在 GameManager 上注册;加载人物数据。即加载的数据需要作用在最新的人物对象上。

代码清单 2 同步人物数据
  1. public class SceneController : Singleton<SceneController>
  2. {
  3.     IEnumerator TransferDifferentScene(string sceneName, string destPortalName)
  4.     {
  5.         SaveManager.GetInstance().SavePlayerData();
  6.  
  7.         AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
  8.  
  9.         // Wait until the asynchronous scene fully loads
  10.         while (!asyncLoad.isDone)
  11.         {
  12.             yield return null;
  13.         }
  14.  
  15.         PortalController destPortal = GetDestinationPortal(destPortalName);
  16.         Instantiate(m_playerPrefab,
  17.             destPortal.m_point.position,
  18.             destPortal.m_point.rotation);
  19.  
  20.         while (GameManager.GetInstance().m_player == null)
  21.         {
  22.             yield return null;
  23.         }
  24.  
  25.         SaveManager.GetInstance().LoadPlayerData();
  26.     }
  27. }

上一篇文章中在地下城场景中还没有人物信息 UI。添加的方式也很简单,将原先场景中的人物信息 UI 存为预制体,再拖拽到新场景中即可,相关逻辑是通用的。

新场景添加完人物信息 UI 之后,测试的话就很方便。只需要看传送到新场景后人物信息是否和传送前一致。