1、目标
在场景间切换时,场景中的数据能够保存和恢复。
2、解决的问题
在Scene1中,我们拾取了房屋门前的道具。
然后我们进入Scene2后再返回Scene1,房屋门前的那些道具又出现了。
这个是因为在场景切换时我们没有保存信息。
3、原理概述
(1)为什么在场景间Items会消失
每次加载场景时,会创建许多的游戏对象。
这些对象包含场景中的Items。
当一个场景第一次加载时,所有的Items都被实例化。
然后玩家在场景中行走,收集道具并添加到他的库存中,然后移动到不同的场景做了同样的事情。移回第一个场景,他之前收集的道具又重新出现了。
这不是一个错误。每次场景加载实例化,场景对玩家的道具一无所知。
所以,我们需要一种追踪场景状态的方法,即在这种情况下记录玩家得到的道具,当玩家重新访问场景时,只会显示应该存在的道具。在场景加载过程中,我们可以通过使用相同的控制器管理器,在适当的时候存储和恢复数据来实现这一点。
(2)保存场景状态的方法
(3)创建Save&Load管理器
(4)保存Scene&Game状态
(5)高级的存储系统
(6)使用SceneItemsManager保存场景Item数据
SceneItemsManager类:
-- string ISaveableUniqueID // 唯一ID字段
-- GameObjectSave GameObjectSave // 保存游戏中所有场景的存储项目数据
唯一ID字段会把偶才能全局唯一标识符,由GenerateGUID方法生成GUID(Globally Unique Identifier)。
GameObjectSave的形式:Dictionary<string, SceneSave> sceneData,key为Scene Name。value值SceneSave的形式:Dictionary<string, List<SceneItem>> listSceneItemDictionary,key为Identifier Name For List,value为List<SceneItem>。
SceneItem的构成:
-- int itemCode
-- Vector3Serializable position
-- string itemName
4、创建基础脚本
按照第3部分的原理,我们创建了如下的脚本。
(1)创建Vector3Serializable脚本
在Assets -> Scripts -> Misc下创建Vector3Serializable脚本。
它是位置信息相关的类。
[System.Serializable]
public class Vector3Serializable
{public float x, y, z;public Vector3Serializable(float x, float y, float z){this.x = x;this.y = y;this.z = z;}public Vector3Serializable() { }
}
我们手工进行序列化的原因是:Unity中标准的Vector3类型不是可序列化的。
(2)创建SceneItem脚本
在Assets -> Scripts下创建SaveSystem的目录,然后在其下再创建SceneItem脚本。
[System.Serializable]
public class SceneItem
{public int itemCode;public Vector3Serializable position;public string itemName;public SceneItem(){position = new Vector3Serializable();}
}
(3)创建SceneSave脚本
在Assets -> Scripts -> SaveSystem目录下创建SceneSave脚本。
using System.Collections.Generic;[System.Serializable]public class SceneSave
{// string key is an identifier name we choose for this listpublic Dictionary<string, List<SceneItem>> listSceneItemDictionary;
}
(4)创建GameObjectSave脚本
在Assets -> Scripts -> SaveSystem目录下创建GameObjectSave脚本。
using System.Collections.Generic;[System.Serializable]public class GameObjectSave
{// string key = scene namepublic Dictionary<string, SceneSave> sceneData;public GameObjectSave(){sceneData = new Dictionary<string, SceneSave>();}public GameObjectSave(Dictionary<string, SceneSave> sceneData){this.sceneData = sceneData;}
}
(5)创建GenerateGUID脚本
在Assets -> Scripts -> SaveSystem目录下创建GenerateGUID脚本。
using UnityEngine;[ExecuteAlways]
public class GenerateGUID : MonoBehaviour
{[SerializeField]private string _gUID = "";public string GUID{get { return _gUID; } set { _gUID = value; }}private void Awake(){// Only populate in the editorif (!Application.IsPlaying(gameObject)){// Ensure the object has a guaranteed unique idif(_gUID == ""){// Assign GUID_gUID = System.Guid.NewGuid().ToString();}}}}
添加了[ExecuteAlways]标识后可以同时在播放模式和编辑器模式下运行,而这次我们希望它只在编辑器中运行。
GUID作为公共属性可以进行读取和写入。
5、场景保存和恢复
(1)整体思路
当切换场景前,将当前所有的Items保存到SceneItemsManager中。当切换到新场景之后,从SceneItemsManager中恢复Items。
SceneItemsManager的作用:存储和恢复在场景中的项目
SaveLoadManager的作用:存储场景数据,恢复场景数据。
(2)创建ISaveable接口
在Assets -> Scripts -> SaveSystem下创建ISaveable脚本。
public interface ISaveable
{string ISaveableUniqueID { get; set; }GameObjectSave GameObjectSave { get; set; }void ISaveableRegister();void ISaveableDeregister();void ISaveableStoreScene(string sceneName);void ISaveableRestoreScene(string sceneName);
}
该接口包含2个属性和4个方法。
在C#中,接口是一种约定,它规定了实现该接口的类必须要实现的成员(属性、方法等)。
在接口中,不能声明字段,只能声明属性。
(3)创建SaveLoadManager脚本
在Assets -> Scripts -> SaveSystem下创建SaveLoadManager脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;public class SaveLoadManager : SingletonMonobehaviour<SaveLoadManager>
{public List<ISaveable> iSaveableObjectList;protected override void Awake(){base.Awake();iSaveableObjectList = new List<ISaveable>();}public void StoreCurrentSceneData(){// loop through all ISaveable objects and trigger store scene data for eachforeach(ISaveable iSaveableObject in iSaveableObjectList){// 将所有的数据都存储在当前场景名下iSaveableObject.ISaveableStoreScene(SceneManager.GetActiveScene().name);}}public void RestoreCurrentSceneData(){// loop through all ISaveble objects and trigger restore scene data for eachforeach(ISaveable iSaveableObject in iSaveableObjectList){// 根据当前场景名恢复数据iSaveableObject.ISaveableRestoreScene(SceneManager.GetActiveScene().name);}}}
SaveLoadManager的功能:
- 初始化 保存数据 的列表
- 根据当前场景名 保存所有数据
- 根据当前场景名 恢复所有数据
(4)创建SceneItemsManager脚本
在Assets -> Scripts -> Scene 下创建SceneItemsManager脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[RequireComponent(typeof(GenerateGUID))]
public class SceneItemsManager : SingletonMonobehaviour<SceneItemsManager>, ISaveable
{private Transform parentItem;[SerializeField] private GameObject itemPrefab = null;private string _iSaveableUniqueID;private GameObjectSave _gameObjectSave;public string ISaveableUniqueID { get { return _iSaveableUniqueID; } set { _iSaveableUniqueID = value; } }public GameObjectSave GameObjectSave { get { return _gameObjectSave; } set { _gameObjectSave = value; } }private void AfterSceneLoad(){parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;}protected override void Awake(){base.Awake();ISaveableUniqueID = GetComponent<GenerateGUID>().GUID;GameObjectSave = new GameObjectSave();}private void OnEnable(){ISaveableRegister();EventHandler.AfterSceneLoadEvent += AfterSceneLoad;}private void OnDisable(){ISaveableDeregister();EventHandler.AfterSceneLoadEvent -= AfterSceneLoad;}public void ISaveableDeregister(){SaveLoadManager.Instance.iSaveableObjectList.Remove(this);}public void ISaveableRegister(){// 将当前对象添加到iSaveableObjectList中SaveLoadManager.Instance.iSaveableObjectList.Add(this);}// 恢复场景public void ISaveableRestoreScene(string sceneName){if(GameObjectSave.sceneData.TryGetValue(sceneName, out SceneSave sceneSave)){if(sceneSave.listSceneItemDictionary != null && sceneSave.listSceneItemDictionary.TryGetValue("sceneItemList", out List<SceneItem> sceneItemList)){// scene list items found - destroy existing items in sceneDestroySceneItems();// new instantiate the list of scene itemsInstantiateSceneItems(sceneItemList);}}}private void InstantiateSceneItems(List<SceneItem> sceneItemList){GameObject itemGameObject;foreach(SceneItem sceneItem in sceneItemList){itemGameObject = Instantiate(itemPrefab, new Vector3(sceneItem.position.x, sceneItem.position.y, sceneItem.position.z), Quaternion.identity, parentItem);Item item = itemGameObject.GetComponent<Item>();item.ItemCode = sceneItem.itemCode;item.name = sceneItem.itemName;}}// Destroy items currently in the sceneprivate void DestroySceneItems(){// Get all items in the sceneItem[] itemsInScene = GameObject.FindObjectsOfType<Item>();// Loop through all scene items and destroy themfor(int i = itemsInScene.Length - 1; i > -1; i--){Destroy(itemsInScene[i].gameObject);}}// 保存场景public void ISaveableStoreScene(string sceneName){// Remove old scene save for gameObject if existsGameObjectSave.sceneData.Remove(sceneName);// Get all items in the sceneList<SceneItem> sceneItemList = new List<SceneItem>();Item[] itemsInScene = FindObjectsOfType<Item>();// Loop through all scene itemsforeach(Item item in itemsInScene){SceneItem sceneItem = new SceneItem();sceneItem.itemCode = item.ItemCode;sceneItem.position = new Vector3Serializable(item.transform.position.x, item.transform.position.y,item.transform.position.z);sceneItem.itemName = item.name;// Add scene item to listsceneItemList.Add(sceneItem);}// Create list scene items dictionary in scene save and add to itSceneSave sceneSave = new SceneSave();sceneSave.listSceneItemDictionary = new Dictionary<string, List<SceneItem>>();sceneSave.listSceneItemDictionary.Add("sceneItemList", sceneItemList);// Add scene save to gameobjectGameObjectSave.sceneData.Add(sceneName, sceneSave);}
}
- GameObject.FindObjectsOfType():该方法允许你找到场景中所有指定类型的游戏对象(GameObject)。这对于需要遍历特定类型的所有对象并执行某些操作时非常有用。
- 在SceneItemsManager脚本中,通过ISaveableRegister方法将ISaveable类型对象添加到了 SaveLoadManager.Instance.iSaveableObjectList中。
(5)优化SceneControllerManager脚本
代码位于:Assets -> Scripts -> Scene下。
在Scene切换时通过StoreCurrentSceneData()实现当前场景数据的保存。在LoadSceneAndSetActive()之后进入到新场景,通过RestoreCurrentSceneData()恢复新场景的数据。
注意:场景转变之后,通过SceneManager.GetActiveScene().name获取到的都是最新的场景信息。
每次进入到新场景,都恢复一下场景下的数据信息。
完整代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;public class SceneControllerManager : SingletonMonobehaviour<SceneControllerManager>
{private bool isFading;[SerializeField] private float fadeDuration = 1f;[SerializeField] private CanvasGroup faderCanvasGroup = null;[SerializeField] private Image faderImage = null;public SceneName startingSceneName;// This is the main external point of contact and influence from the rest of the project.// This will be called when the player wants to switch scenes.// sceneName:目标场景名称// spawnPosition: 主角出现的位置public void FadeAndLoadScene(string sceneName, Vector3 spawnPosition){// If a fade isn't happening then start fading and switching scenes.if (!isFading){StartCoroutine(FadeAndSwitchScenes(sceneName, spawnPosition));}}// This is the coroutine where the 'building blocks' of the script are put together.private IEnumerator FadeAndSwitchScenes(string sceneName, Vector3 spawnPosition){// Call before scene unload fade out eventEventHandler.CallBeforeSceneUnloadFadeOutEvent();// Start fading to block and wait for it to finish before continuing.yield return StartCoroutine(Fade(1f)); // 变黑色// Set player positionPlayer.Instance.gameObject.transform.position = spawnPosition;// Store scene dataSaveLoadManager.Instance.StoreCurrentSceneData();// Call before scene unload event.EventHandler.CallBeforeSceneUnloadEvent();// Unload the current active scene.yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene().buildIndex);// Start loading the given scene and wait for it to finish.yield return StartCoroutine(LoadSceneAndSetActive(sceneName));// Call after scene load eventEventHandler.CallAfterSceneLoadEvent();// Restore new scene dataSaveLoadManager.Instance.RestoreCurrentSceneData();// Start fading back in and wait for it to finish before exiting the function.yield return StartCoroutine(Fade(0f)); // 变白色// Call after scene load fade in eventEventHandler.CallAfterSceneLoadFadeInEvent();}private IEnumerator Fade(float finalAlpha){// Set the fading flag to true so the FadeAndSwitchScenes coroutine won't be called again.isFading = true;// Make sure the CanvasGroup blocks raycasts into the scene so no more input can be accepted.faderCanvasGroup.blocksRaycasts = true;// Calculate how fast the CanvasGroup should fade based on it's current alpha,// it's final alpha and how long it has to change between the two.float fadeSpeed = Mathf.Abs(faderCanvasGroup.alpha - finalAlpha) / fadeDuration;// while the CanvasGroup hasn't reached the final alpha yet...while( !Mathf.Approximately(faderCanvasGroup.alpha, finalAlpha)){// ... move the alpha towards it's target alpha.faderCanvasGroup.alpha = Mathf.MoveTowards(faderCanvasGroup.alpha, finalAlpha,fadeSpeed * Time.deltaTime);// Wait for a frame then continue.yield return null;}// Set the flag to false since the fade has finished.isFading = false;// Stop the CanvasGroup from blocking raycasts so input is no longer ignored.faderCanvasGroup.blocksRaycasts = false;}private IEnumerator LoadSceneAndSetActive(string sceneName){// Allow the given scene to load over serval frames and add it to the already// loaded scenes (just the Persistent scene at this point).yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);// Find the scene that was most recently loaded (the one at the last index of the loaded scenes).Scene newlyLoadedScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);// Set the newly loaded scene as the active scene(this marks it as the one to be unloaded next).SceneManager.SetActiveScene(newlyLoadedScene);}private IEnumerator Start(){// Set the initial alpha to start off with a block screen.faderImage.color = new Color(0f, 0f, 0f, 1f);faderCanvasGroup.alpha = 1f;// Start the first scene loading and wait for it to finishyield return StartCoroutine(LoadSceneAndSetActive(startingSceneName.ToString()));// If this event has any subscribers, call itEventHandler.CallAfterSceneLoadEvent();SaveLoadManager.Instance.RestoreCurrentSceneData();// Once the scene is finished loading, start fading inStartCoroutine(Fade(0f));}}
(6)改变脚本执行顺序
Edit -> Project Settings -> Script Execution Order。
添加SaveLoadManager,并设置值为-80。
我们希望SceneControllerManager初始化之后,立马初始化SaveLoadManager。
这样做的好处:
SceneItemsManager的OnEnable()中会调用ISaveableRegister()方法,而在ISaveableRegister()的方法中会直接操作SaveLoadManager.Instance.iSaveableObjectList的属性。如果该属性在使用前没有被初始化,则会报错。
(7)创建SceneItemsManager对象
在Hierarchy -> PersistentScene下创建空物体命令为SceneItemsManager。
给该对象添加SceneItemsManager的脚本。
同时将Assets -> Prefabs -> Item下的Item预设体移到SceneItemsManager的Item Prefab参数下。
(8)创建SaveLoadManager对象
在Hierarchy -> PersistentScene下创建空物体命令为SaveLoadManager。
给该对象添加SaveLoadManager的脚本。
(9)优化SceneTeleport脚本
将OnTriggerEnter2D修改为OnTriggerStay2D。