学习目标:
大家好啊我是说的道理,今天来点大家想看的东西,就学习如何实现记录物品在不同场景的存在状态,这句话的意思我想表达的是一个物品如果消失在第一个场景,如果此时你进入第二个场景并回到第一个场景的时候,你会发现消失的物品又会回到原处,这是因为每次加载一个场景的时候运行游戏时的场景又会再实例化一次,所以我今天要做的就是用数据结构的方式来给每一个物品Item独一无二的GUID,在销毁的时候直接将数据彻底删除,话不多说就开始吧。
学习内容:
// 在本节课重点之前,我们先来制作一个场景加载控制器,新建一个空对象
在Enums脚本下新建场景名:
public enum SceneName
{
Scene1_Farm,
Scene2_Field,
Scene3_Cabin,
}
SceneControllerManager并给它同名脚本,内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class SceneControllerManager : Singleton<SceneControllerManager>
{
private bool isFading;
[SerializeField] private float fadeDuration = 1f;
[SerializeField] private CanvasGroup fadeCanvasGroup = null;
[SerializeField] private Image fadeImage = null;
public SceneName startingSceneName;
private IEnumerator Start()
{
fadeImage.color = new Color(0f, 0f, 0f, 1f);
fadeCanvasGroup.alpha = 1f;
yield return StartCoroutine(LoadSceneAndSetActiveCoroutine(startingSceneName.ToString()));
EventHandler.CallAfterSceneLoadEvent();
SaveStoreManager.Instance.RestoreCurrentSceneData();
StartCoroutine(FadeCoroutine(0f));
}
public void FadeAndLoadScene(string sceneName, Vector3 spawnPosition)
{
if (!isFading)
{
StartCoroutine(FadeAndSwitchScenesCoroutine(sceneName, spawnPosition));
}
}
private IEnumerator FadeAndSwitchScenesCoroutine(string sceneName,Vector3 spawnPosition)
{
EventHandler.CallBeforeSceneUnloadFadeOutEvent();
yield return StartCoroutine(FadeCoroutine(1f));
SaveStoreManager.Instance.StoreCurrentSceneData();
PlayerController.Instance.gameObject.transform.position = spawnPosition;
EventHandler.CallBeforeSceneUnloadEvent();
yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene().buildIndex);
yield return StartCoroutine(LoadSceneAndSetActiveCoroutine(sceneName));
EventHandler.CallAfterSceneLoadEvent();
SaveStoreManager.Instance.RestoreCurrentSceneData();
yield return StartCoroutine(FadeCoroutine(0f));
EventHandler.CallAfterSceneLoadFadeInEvent();
}
private IEnumerator FadeCoroutine(float finalAlpha)
{
isFading = true;
fadeCanvasGroup.blocksRaycasts = true;
float fadeSpeed = Mathf.Abs(fadeCanvasGroup.alpha - finalAlpha) / fadeDuration;
while (!Mathf.Approximately(fadeCanvasGroup.alpha, finalAlpha))
{
fadeCanvasGroup.alpha = Mathf.MoveTowards(fadeCanvasGroup.alpha, finalAlpha, fadeSpeed * Time.deltaTime);
yield return null;
}
isFading = false;
fadeCanvasGroup.blocksRaycasts = false;
}
private IEnumerator LoadSceneAndSetActiveCoroutine(string sceneName)
{
yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
Scene newlyLoadedScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
SceneManager.SetActiveScene(newlyLoadedScene);
}
}
对于EvnetHandler,这是一个事件处理器,适用于我们开发模式:观察者模式,具体可以看上一期文章,那么先把本期要用的事件创建好,分别对应的场景加载完成前淡出的事件,场景加载完成前的事件,场景刚开始加载后淡入的事件,场景刚开始加载后的事件。
public static class EventHandler
{
//上一期的事件
//场景加载事件
public static event Action BeforeSceneUnloadFadeOutEvent;
public static void CallBeforeSceneUnloadFadeOutEvent()
{
if(BeforeSceneUnloadFadeOutEvent!= null)
{
BeforeSceneUnloadFadeOutEvent();
}
}
public static event Action BeforeSceneUnloadEvent;
public static void CallBeforeSceneUnloadEvent()
{
if (BeforeSceneUnloadEvent != null)
{
BeforeSceneUnloadEvent();
}
}
public static event Action AfterSceneLoadEvent;
public static void CallAfterSceneLoadEvent()
{
if (AfterSceneLoadEvent != null)
{
AfterSceneLoadEvent();
}
}
public static event Action AfterSceneLoadFadeInEvent;
public static void CallAfterSceneLoadFadeInEvent()
{
if (AfterSceneLoadFadeInEvent != null)
{
AfterSceneLoadFadeInEvent();
}
}
我们把做好的场景加载持久化场景下面作为Additive,然后去
BuildSettings添加好三个场景
刚开始运行游戏的时候只会加载第一个场景,现在淡入淡出实现,那怎么去第二个场景呢?
这里就用Trigger来写个传送门,创建一个空对象叫SceneTeleport然后同名脚本
脚本内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(BoxCollider2D))]
public class SceneTeleport : MonoBehaviour
{
[SerializeField] private SceneName sceneNameToGo = SceneName.Scene1_Farm;
[SerializeField] private Vector3 scenePositionToGo = new Vector3();
private void OnTriggerStay2D(Collider2D collision)
{
if(collision.TryGetComponent<PlayerController>(out PlayerController player))
{
Debug.Log("成功与玩家触发条件");
float xPosition = Mathf.Approximately(scenePositionToGo.x, 0) ? player.transform.position.x : scenePositionToGo.x;
float yPosition = Mathf.Approximately(scenePositionToGo.y, 0) ? player.transform.position.y : scenePositionToGo.y;
float zPosition = Mathf.Approximately(scenePositionToGo.z, 0) ? player.transform.position.z : scenePositionToGo.z;
SceneControllerManager.Instance.FadeAndLoadScene(sceneNameToGo.ToString(), new Vector3(xPosition,yPosition,zPosition));
}
}
}
别忘了把这个对象做成Prefab,就这样我们找到第二个场景要传送的位置,然后添加好,每个场景的传送点都要设置好。
搞好了别忘了每次运行游戏的时候要把这三个场景Unload一下只让PersistenceScene加载
如果你已经成功了,那么还是得好好恭喜一下,接下来就到了我们的重头戏了
实现记录物品在不同场景的存在状态:
进入正题
首先面对不同场景,他们的物品位置和状态都是各有不同的,因此我们需要先记录下它们的位置和状态,
先从位置开始写一个脚本
[System.Serializable]
public class Vector3Serializable
{
public float x;
public float y;
public float z;
public Vector3Serializable(float x,float y,float z)
{
this.x = x;
this.y = y;
this.z = z;
}
public Vector3Serializable()
{
}
}
然后去Enums脚本新建枚举
public enum InventoryLocation
{
player,
chest,
count,
}
public enum ToolEffect
{
none,
watering,
}
public enum Direction
{
up,
down,
right,
left,
}
public enum ItemType
{
Seed,
Commodity,
Watering_tool,
Hoeing_tool,
Chopping_tool,
Breaking_tool,
Reping_tool,
Collecting_tool,
Reapable_scenery,
Furinture,
None,
Count,
}
再创建一个SceneItem
[System.Serializable]
public class SceneItem
{
public int itemCode;
public Vector3Serializable position;
public string itemName;
public SceneItem()
{
position = new Vector3Serializable();
}
}
接下来是能添加独特修饰符的GenerateGUID
using UnityEngine;
[ExecuteAlways]
public class GenerateGUID : MonoBehaviour
{
[SerializeField] private string _gUID = "";
public string GUID
{
get => _gUID;
set => _gUID = value;
}
private void Awake()
{
if (!Application.IsPlaying(gameObject))
{
if(_gUID == "")
{
//登记GUID
_gUID = System.Guid.NewGuid().ToString();
}
}
}
}
这里新学的一个特性
[ExecuteAlways]
意思是即使在Unity编辑器的状态下添加脚本,也会按游戏运行之后的内容来执行,
!Application.IsPlaying(gameObject); 感叹号反义,表示在没有运行游戏时,
对于不同场景的物品,我们用一个类的链表来存储属于某个场景的SceneItem
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class SceneSave
{
public List<SceneItem> listSceneItem;
}
然后我们把这三个场景的数据即SceneSave,存储在一个字典当中 ,写了一个重载
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class GameObjectSave
{
public Dictionary<string, SceneSave> sceneData;
public GameObjectSave()
{
sceneData = new Dictionary<string, SceneSave>();
}
public GameObjectSave(Dictionary<string,SceneSave> sceneData)
{
this.sceneData = sceneData;
}
}
我们还需要一个接口,实现物品的存储,重新存储,物品的GUID,登记场景中的物品,去除登记场景中的物品
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISaveable
{
string ISaveableUniqueID { get; set; }
GameObjectSave GameObjectSave { get; set; }
void ISaveableRegister();
void ISaveableDeregister();
void ISaveableStoreScene(string sceneName);
void ISaveableRestoreScene(string sceneName);
}
准备工作完成,那么我们怎么来实现这些接口来让场景的数据以及场景中的物品Item信息能够存储和取出你?
就先创建一个空对象叫SceneItemManager并创建同名脚本给它
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(GenerateGUID))] //需要组件GenerateGUID
public class SceneItemsManager : Singleton<SceneItemsManager>, ISaveable //继承ISaveable并实现方法
{
private Transform parentItem;
[SerializeField] private GameObject itemPrefab = null;
private string _iSaveableUniqueId;
public string ISaveableUniqueID { get => _iSaveableUniqueId; set => _iSaveableUniqueId = value; } //GUID属性,每帧更新
private GameObjectSave _gameObjectSave;
public GameObjectSave GameObjectSave { get => _gameObjectSave; set => _gameObjectSave = value; } //scneneData属性,更新
protected override void Awake()
{
base.Awake();
ISaveableUniqueID = GetComponent<GenerateGUID>().GUID;
GameObjectSave = new GameObjectSave();
}
private void OnEnable()
{
ISaveableRegister();
EventHandler.AfterSceneLoadEvent += AfterSceneLoaded;
}
private void OnDisable()
{
ISaveableDeregister();
EventHandler.AfterSceneLoadEvent -= AfterSceneLoaded;
}
/// <summary>
/// 在AfterSceneLoadEvent触发后,才能执行parentItem的实例化
/// </summary>
public void AfterSceneLoaded()
{
parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;
}
/// <summary>
/// 销毁所有场景中的物品
/// </summary>
private void DestorySceneItems()
{
Item[] itemInScenes = GameObject.FindObjectsOfType<Item>();
for (int i = itemInScenes.Length -1; i > -1; i--)
{
Destroy(itemInScenes[i].gameObject);
}
}
/// <summary>
/// 重新加载场景中的所有物品,先销毁所有物品游戏对象再初始化
/// </summary>
/// <param name="sceneName"></param>
public void ISaveableRestoreScene(string sceneName)
{
if(GameObjectSave.sceneData.TryGetValue(sceneName,out SceneSave sceneSave))//找到特定的sceneName字符串,如果找到了就销毁该场景的
{
if(sceneSave.listSceneItem!= null )
{
DestorySceneItems();
InstantiateSceneItems(sceneSave.listSceneItem);
}
}
}
/// <summary>
/// 初始化SceneItem链表,并给所有Item重新赋值
/// </summary>
/// <param name="sceneItemList"></param>
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;
}
}
public void InstantiateSceneItem(int itemCode,Vector3 itemPosition)
{
GameObject itemGameObject = Instantiate(itemPrefab, itemPosition, Quaternion.identity, parentItem);
Item item = itemGameObject.GetComponent<Item>();
item.Init(itemCode);
}
/// <summary>
/// 将场景中的所有Item存储起来
/// </summary>
/// <param name="sceneName"></param>
public void ISaveableStoreScene(string sceneName)
{
GameObjectSave.sceneData.Remove(sceneName); //先清空该场景
List<SceneItem> sceneItemLists = new List<SceneItem>();
Item[] itemsInScene = FindObjectsOfType<Item>();
foreach (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;
sceneItemLists.Add(sceneItem);
}
SceneSave sceneSave = new SceneSave();
sceneSave.listSceneItem = sceneItemLists;
GameObjectSave.sceneData.Add(sceneName, sceneSave);
}
/// <summary>
/// 移除SaveStoreManager.Instance.iSaveableObjectLists链表的该数据
/// </summary>
public void ISaveableDeregister()
{
SaveStoreManager.Instance.iSaveableObjectLists.Remove(this);
}
/// <summary>
/// 添加SaveStoreManager.Instance.iSaveableObjectLists链表的该数据
/// </summary>
public void ISaveableRegister()
{
SaveStoreManager.Instance.iSaveableObjectLists.Add(this);
}
}
还需要一个SceneLodeManager 游戏对象以及同名脚本,用于场景加载的时候调用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SaveStoreManager : Singleton<SaveStoreManager>
{
public List<ISaveable> iSaveableObjectLists;
protected override void Awake()
{
base.Awake();
iSaveableObjectLists = new List<ISaveable>();
}
public void StoreCurrentSceneData()
{
foreach (ISaveable iSaveableObjectList in iSaveableObjectLists)
{
iSaveableObjectList.ISaveableStoreScene(SceneManager.GetActiveScene().name);
}
}
public void RestoreCurrentSceneData()
{
foreach (ISaveable iSaveableObjectList in iSaveableObjectLists)
{
iSaveableObjectList.ISaveableRestoreScene(SceneManager.GetActiveScene().name);
}
}
}
回到场景加载控制器脚本,我们可以把报错的内容全部消除了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class SceneControllerManager : Singleton<SceneControllerManager>
{
private bool isFading;
[SerializeField] private float fadeDuration = 1f;
[SerializeField] private CanvasGroup fadeCanvasGroup = null;
[SerializeField] private Image fadeImage = null;
public SceneName startingSceneName;
private IEnumerator Start()
{
fadeImage.color = new Color(0f, 0f, 0f, 1f);
fadeCanvasGroup.alpha = 1f;
yield return StartCoroutine(LoadSceneAndSetActiveCoroutine(startingSceneName.ToString()));
EventHandler.CallAfterSceneLoadEvent();
SaveStoreManager.Instance.RestoreCurrentSceneData();
StartCoroutine(FadeCoroutine(0f));
}
public void FadeAndLoadScene(string sceneName, Vector3 spawnPosition)
{
if (!isFading)
{
StartCoroutine(FadeAndSwitchScenesCoroutine(sceneName, spawnPosition));
}
}
private IEnumerator FadeAndSwitchScenesCoroutine(string sceneName,Vector3 spawnPosition)
{
EventHandler.CallBeforeSceneUnloadFadeOutEvent();
yield return StartCoroutine(FadeCoroutine(1f));
SaveStoreManager.Instance.StoreCurrentSceneData();
PlayerController.Instance.gameObject.transform.position = spawnPosition;
EventHandler.CallBeforeSceneUnloadEvent();
yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene().buildIndex);
yield return StartCoroutine(LoadSceneAndSetActiveCoroutine(sceneName));
EventHandler.CallAfterSceneLoadEvent();
SaveStoreManager.Instance.RestoreCurrentSceneData();
yield return StartCoroutine(FadeCoroutine(0f));
EventHandler.CallAfterSceneLoadFadeInEvent();
}
private IEnumerator FadeCoroutine(float finalAlpha)
{
isFading = true;
fadeCanvasGroup.blocksRaycasts = true;
float fadeSpeed = Mathf.Abs(fadeCanvasGroup.alpha - finalAlpha) / fadeDuration;
while (!Mathf.Approximately(fadeCanvasGroup.alpha, finalAlpha))
{
fadeCanvasGroup.alpha = Mathf.MoveTowards(fadeCanvasGroup.alpha, finalAlpha, fadeSpeed * Time.deltaTime);
yield return null;
}
isFading = false;
fadeCanvasGroup.blocksRaycasts = false;
}
private IEnumerator LoadSceneAndSetActiveCoroutine(string sceneName)
{
yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
Scene newlyLoadedScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
SceneManager.SetActiveScene(newlyLoadedScene);
}
}
检查一下你有没有挂载好脚本呢
更改脚本的执行顺序
回到UIInventorySlot脚本,我们需要在事件触发之后才能实例化。更改如下:
private void OnEnable()
{
EventHandler.AfterSceneLoadEvent += SceneLoaded;
}
private void OnDisable()
{
EventHandler.AfterSceneLoadEvent -= SceneLoaded;
}
public void SceneLoaded()
{
itemParent = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;
}
学习产出:
淡入淡出
不同场景切换的时候数据结构发挥作用,让被拾取的Item被销毁并且不再实例化