场景管理
在 Untiy 当中也是需要管理好自身场景,
很多人以为场景切换就是单纯场景资源编号之后调用 Unity 自身的 SceneManager 就行;
实际上场景并非这么简单直接切换, 因为场景还有不少问题需要处理:
是否为独占场景: 需要关闭其他UI独占场面是否同个分组场景: 有的场景是多个子场景拼接是否为界面首页: 默认进入游戏的主界面首页是切换Loading还是Home: 确定启动游戏之后先运行 Loading 还是 Home- 其他还有做
ab包动态下载场景资源的情况
另外需要说明下 Unity 的场景概念很多人混合在一起, 他和 Godot 节点式常见不一样,
需要在菜单栏 File(文件) - BuildSetting(构建设置) 当中去手动或者代码引入对应 Scene 对象场景;
但是有人也喜欢在单一 Scene 之中引入外部预制件并设置 Active 状态让其可用或者不可用唤起,
这种方法好处就是直接单次加载所有预制件直接 Active 切换场景即可, 但是坏处就是某个场景资源十分庞大直接卡死所有其他简单场景.
所以在 Unity 场景分为以下几种意思:
Scene: 也就是BuildSetting设置的场景, 包括且不限制登录Scene | 首页Scene | 游戏Scene等等之类单一场景Page: 子场景也就是场景内显示的预制体层, 比如触发点击战场情况时候看起来转场到战场地图的新场景, 同时还有打开背包进入背包菜单界面的情况
注意: 这里后续会大量依赖
QFramework|UniTask|FSM这些组件, 所以最好对其有基本认识.
这里首先需要创建新的游戏项目, 这里假设需要做 在线卡牌 项目命名为 QuickCard, 初始场景命名为 MainScene.
首先按照之前习惯需要构建应用入口:
using QFramework;
/// <summary>
/// 游戏应用对象
/// </summary>
public class QuickCardApp : Architecture<QuickCardApp>
{
/// <summary>
/// 初始化方法
/// </summary>
protected override void Init()
{
// todo: 注册 Model|System|Command|Utility
// 后续补充
}
}
那么现在后续全部 QFramework 的控制器都是依赖 QuickCardApp.Interface,
也就是 GetArchitecture 只需要返回 QuickCardApp.Interface, 这里是项目首先初始化的注册对象入口.
之后就是另外关键管理器脚本:
using State;
using QFramework;
using UnityEngine;
namespace Manager
{
/// <summary>
/// 启动状态机类型
/// </summary>
public enum LaunchType
{
InitNetwork, // 初始化网络, 首先确认网络连通性
InitUI, // 初始化UI, 初始化最基本 Alert, Dialog 方便异常出错直接闪断
UpgradeAssets, // 热更新资源, 游戏资源进行网络版本热更新
InitConfig, // 加载全局配置, 主要是加载相关所需的配置信息
InitGameConfig, // 初始化游戏配置, 实际上弹出控制命令或者GM指令面板用于调试和追加资源
EnterGame, // 进入游戏, 跳转游戏载入页面: Login|Index
ExitGame // 退出游戏, 弹出异常或者直接退出应用
}
/// <summary>
/// 启动管理器
/// </summary>
public class LaunchManager : MonoBehaviour, IController
{
/// <summary>
/// 绑定应用接口
/// </summary>
/// <returns>IArchitecture</returns>
public IArchitecture GetArchitecture() => QuickCardApp.Interface;
/// <summary>
/// 最高初始化, 启动管理器随着项目启动而启动
/// </summary>
private void Awake() => DontDestroyOnLoad(gameObject);
/// <summary>
/// 状态机句柄
/// </summary>
public FSMFactory<LaunchType> Factory { get; } = new();
/// <summary>
/// 启动初始化
/// </summary>
private void Start()
{
// 注册状态机模块
Factory.AddState(LaunchType.InitNetwork, new InitNetworkState(Factory, this));
Factory.AddState(LaunchType.InitUI, new InitUIState(Factory, this));
Factory.AddState(LaunchType.UpgradeAssets, new UpgradeAssetsState(Factory, this));
Factory.AddState(LaunchType.InitConfig, new InitConfigState(Factory, this));
Factory.AddState(LaunchType.InitGameConfig, new InitGameConfigState(Factory, this));
Factory.AddState(LaunchType.EnterGame, new EnterGameState(Factory, this));
Factory.AddState(LaunchType.ExitGame, new ExitGameState(Factory, this));
// 加载流程
// 1. InitNetwork, 初始化网络更新资源
// - 依赖 AssetSystem , 用于初始化 CDN|Web|Game 请求地址, 还有绑定 Reconnect 回调处理
// - 依赖 InitNetworkCommand(初始化网络指令), InitNetworkEvent(初始化网络事件), AssetSystem(资源系统)
// 2. InitUI, 基础组件必须初始才能让后续窗口正常弹出
// - 实际上应该是初始化视图控制器(UIManager), 让 Main|UIPage|Popup|Alert|Cmd 等UI界面
// - 这里需要依赖实例化持久组件 UIManager 管理事件|动画|粒子效果, 后续错误直接就能依靠其 Alert 弹出
// 3. UpgradeAssets, 热更新资源, 游戏必不可少的动态资源加载
// - 热更新主流方案目前有 Lua 和 HybridCLR, 游戏语音|动画|i18n语言包都依赖下载的资源包处理
// - 这里有两种状态切换: InitConfig 和 ExitGame, 对应成功进入下一步加载配置还是资源包错误直接退出游戏
// - 需要注意这里依赖 UIManager 唤起 UIPage 切换到 Loading 做资源读条
// 4. InitConfig, 初始化全局配置
// - 一般可以用来拉取 Web 服务的配置和常见资源, 如游戏公告|游戏图标等
// - 验证账号|拉取公告|基础配置, 带状态切换: InitGameConfig 和 ExitGame
// - 这里开始就是需要做第三方登录授权获取到登录 Token 推送游戏服务端管理并拉取到玩家数据保存本地内存
// - 注意这里 Web 请求拿到 Token 基本上宣告第三方授权完成, 剩下就是游戏服务器内部( TCP|UDP|WS )的长链接请求
// 5. InitGameConfig, 加载游戏相关配置
// - 需要对游戏本身资源初始化, 简单点的如 主音量|语音音量等
// - 因为获得玩家数据, 需要判断是否为GM, 如果为GM还需要在 UIManager 做好超级管理员界面菜单渲染
// - 这里步骤基本上完成游戏热更加载到授权登录全部流程, 把启动状态机最后切换到 EnterGame 代表游戏正式开始
Factory.StartState(LaunchType.InitNetwork);
}
/// <summary>
/// 组件销毁回调
/// </summary>
private void OnDestroy() => Factory.Clear();
}
}
Start 内部的注释是需要细细思考的部分, 每个步骤都是及其关键:
// 加载流程
// 1. InitNetwork, 初始化网络更新资源
// - 依赖 AssetSystem , 用于初始化 CDN|Web|Game 请求地址, 还有绑定 Reconnect 回调处理
// - 依赖 InitNetworkCommand(初始化网络指令), InitNetworkEvent(初始化网络事件), AssetSystem(资源系统)
// 2. InitUI, 基础组件必须初始才能让后续窗口正常弹出
// - 实际上应该是初始化视图控制器(UIManager), 让 Main|UIPage|Popup|Alert|Cmd 等UI界面
// - 这里需要依赖实例化持久组件 UIManager 管理事件|动画|粒子效果, 后续错误直接就能依靠其 Alert 弹出
// 3. UpgradeAssets, 热更新资源, 游戏必不可少的动态资源加载
// - 热更新主流方案目前有 Lua 和 HybridCLR, 游戏语音|动画|i18n语言包都依赖下载的资源包处理
// - 这里有两种状态切换: InitConfig 和 ExitGame, 对应成功进入下一步加载配置还是资源包错误直接退出游戏
// - 需要注意这里依赖 UIManager 唤起 UIPage 切换到 Loading 做资源读条
// 4. InitConfig, 初始化全局配置
// - 一般可以用来拉取 Web 服务的配置和常见资源, 如游戏公告|游戏图标等
// - 验证账号|拉取公告|基础配置, 带状态切换: InitGameConfig 和 ExitGame
// - 这里开始就是需要做第三方登录授权获取到登录 Token 推送游戏服务端管理并拉取到玩家数据保存本地内存
// - 注意这里 Web 请求拿到 Token 基本上宣告第三方授权完成, 剩下就是游戏服务器内部( TCP|UDP|WS )的长链接请求
// 5. InitGameConfig, 加载游戏相关配置
// - 需要对游戏本身资源初始化, 简单点的如 主音量|语音音量等
// - 因为获得玩家数据, 需要判断是否为GM, 如果为GM还需要在 UIManager 做好超级管理员界面菜单渲染
// - 这里步骤基本上完成游戏热更加载到授权登录全部流程, 把启动状态机最后切换到 EnterGame 代表游戏正式开始
这里就是很标准的 Launch(启动) 步骤解析, 只有跑完这个流程才能允许被切换到主界面当中,
这里 UpgradeAssets 热更资源不是必要, 也可以直接跳过该状态直接往下处理, 但是最好做预留扩展以后处理.
热更是我感觉比如引入的功能之一, 否则每次打包都需要平台提交包体全量更新.
上面结合状态机和启动步骤可以明显直观看到状态机在游戏当中的关键作用, 如果想在启动时候补充启动状态就可以继续扩展下去.
这里篇幅很大所以只按照启动的路线一步步走通启动流程, 所以聚焦 Factory.StartState(LaunchType.InitNetwork) 来开始启动.
LaunchType.InitNetwork
项目刚立项时候可能没有太多网络请求需求, 所以可以先定义简单初始化网络状态:
using System;
using Command;
using Manager;
using QFramework;
using UnityEngine;
namespace State
{
/// <summary>
/// 初始化网络状态机
/// </summary>
public class InitNetworkState : AbstractFSMState<LaunchType, LaunchManager>, IController
{
public InitNetworkState(FSMFactory<LaunchType> factory, LaunchManager target) : base(factory, target)
{
}
/// <summary>
/// 进入状态
/// </summary>
public override void OnEnter()
{
// 需要推送指令, 要求初始化网络配置
Debug.Log("InitNetworkState:OnEnter");
this.SendCommand(new InitNetworkCommand()); // 推送指令让其他系统知道
this.GetSystem<IResourceSystem>().SetUpdateResource(ChangeToInitUI);
}
/// <summary>
/// 切换到初始化UI状态
/// </summary>
private void ChangeToInitUI()
{
Factory.SetCurrentState(LaunchType.InitUI);
}
/// <summary>
/// 所属应用
/// </summary>
/// <returns>IArchitecture</returns>
public IArchitecture GetArchitecture() => QuickCardApp.Interface;
}
}
这里面主要集中 OnEnter 方法, 内部只做了两件事:
- 推送指令:
InitNetworkCommand要求NetworkManager进行网络配置初始化 - 获取
IResourceSystem资源系统设置客户端资源更新
这里定义初始化的指令可以暂时编写等待后期调用, 内部具体不做太大实现:
using Event;
using QFramework;
// 指令
namespace Command
{
/// <summary>
/// 推送初始化指令
/// </summary>
public class InitNetworkCommand : AbstractCommand
{
protected override void OnExecute() => this.SendEvent(new InitNetworkEvent());
}
}
// 事件
namespace Event
{
/// <summary>
/// 推送初始化网络事件
/// </summary>
public class InitNetworkEvent
{
}
}
本质上就推送 InitNetworkEvent 信号而已, 等待后续其他控制器自行监听这个事件.
后续关键的就是 ResourceSystem 资源系统的构建:
using Cysharp.Threading.Tasks;
using QFramework;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace System
{
/// <summary>
/// 资源信息
/// </summary>
public class ResourceInfo
{
public static string BaseUrl { get; set; }
}
/// <summary>
/// 资源系统接口
/// </summary>
public interface IResourceSystem : ISystem
{
/// <summary>
/// 设置更新资源
/// </summary>
/// <param name="finish"></param>
public void SetUpdateResource(Action finish);
/// <summary>
/// 异步加载资源
/// AsyncOperationHandle 需要插件 Addressables 引入, 可以直接 PackageManager 切换 UnityRegistry 检索
/// </summary>
/// <param name="path"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public UniTask<AsyncOperationHandle<T>> LoadResourceAsync<T>(string path);
}
/// <summary>
///
/// </summary>
public class ResourceSystem : AbstractSystem, IResourceSystem
{
/// <summary>
/// 初始化方法
/// </summary>
protected override void OnInit()
{
}
/// <summary>
/// 设置更新资源
/// </summary>
/// <param name="finish"></param>
public void SetUpdateResource(Action finish)
{
// todo: 也许还需要提交本地版本给服务器验证是否版本匹配
// 按照网络开关判断是否要网络加载还是本地加载
// 类内部 static 只会被初始化一次, 也就是全局获取该值都变成初始化的值
ResourceInfo.BaseUrl = Application.streamingAssetsPath;
//ResourceInfo.BaseUrl = "https://example.assets.com/";
// 唤醒回调
finish?.Invoke();
}
/// <summary>
/// 异步引入资源
/// </summary>
/// <param name="path"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async UniTask<AsyncOperationHandle<T>> LoadResourceAsync<T>(string path)
{
var resource = Addressables.LoadAssetAsync<T>(path);
await resource;
return resource;
}
}
}
上面代码依赖 UniTask 和 Addressables 组件, 具体就是判断资源是否需要采用网络处理加载,
最终初始化网络 finish?.Invoke() 来唤醒回调触发切换到下个状态.
可以看到功能实际上相对简单, 内部其实忽略不少需要处理网络处理方式, 后期扩展也是在内部这些方法基础上编写代码.
这里要使用 IResourceSystem 需要在应用入口注册该系统:
using System;
using QFramework;
/// <summary>
/// 游戏应用对象
/// </summary>
public class QuickCardApp : Architecture<QuickCardApp>
{
/// <summary>
/// 初始化方法
/// </summary>
protected override void Init()
{
RegisterSystem<IResourceSystem>(new ResourceSystem());
}
}
后续就是切换到 InitUI 状态处理.
LaunchType.InitUI
初始化网络之后就要初始化基本的 UI 的状态机:
using Manager;
using QFramework;
using UnityEngine;
namespace State
{
/// <summary>
/// 初始化UI状态
/// </summary>
public class InitUIState : AbstractFSMState<LaunchType, LaunchManager>, IController
{
public InitUIState(FSMFactory<LaunchType> factory, LaunchManager target) : base(factory, target)
{
}
/// <summary>
/// 进入状态
/// </summary>
public override async void OnEnter()
{
Debug.Log("InitUIState:OnEnter");
// 这里需要初始化处理 UIManager 并切换成资源热更新状态
await UIManager.Instance.InitUI();
ChangeToUpgradeAssets();
}
/// <summary>
/// 切换到热更新
/// </summary>
private void ChangeToUpgradeAssets()
{
Factory.SetCurrentState(LaunchType.UpgradeAssets);
}
/// <summary>
/// 这里切换状态需要先设置 Loading 状态, 每个场景都需要先做好百分比回收情况
/// </summary>
public override void OnExit()
{
// 页面还没完全加载好
UIManager.Instance.SetUIPage(new UIPageInfo
{
UISceneType = UISceneType.Init,
UIPageType = UIPageType.LoadingUI
});
}
/// <summary>
/// 所属应用
/// </summary>
/// <returns>IArchitecture</returns>
public IArchitecture GetArchitecture() => QuickCardApp.Interface;
}
}
这里可以明显看到关键的单例调用: UIManager.Instance
这就是核心的场景UI管理器 UIManager, 也就是本篇章需要具体说明的管理器对象.
UIManager
UIManager 需要手动挂载在场景预制体当中, 之后就是代码内容:
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using QFramework;
using UI;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace Manager
{
/// <summary>
/// UI场景类型, 需要在 UIManager 下面添加节点, 需要按照顺序创建
/// </summary>
public enum UISceneType
{
Init = 0, // 初始化
Main, // 主场景
UIPage, // 子页面, 对应 UIPageType 枚举
Popup, // 弹窗, WebView 等窗口
Alert, // 警告|提示
Debug, // 调试窗口
}
/// <summary>
/// UISceneType = UIPage 对应的页面枚举
/// </summary>
public enum UIPageType
{
HomePageUI = 0, // 首页渲染UI
SettingsUI, // 设置界面UI
ProfileUI, // 玩家详情UI
WarningAlert, // 错误提示警告UI
LoginUI, // 登录UI
RegisterUI, // 注册UI, 可有可无
LoadingUI, // 读取中UI
DownloadResUI, // 资源下载UI
}
/// <summary>
/// UI页面详情
/// </summary>
public class UIPageInfo
{
/// <summary>
/// UI场景类型
/// </summary>
public UISceneType UISceneType { get; set; } = UISceneType.UIPage;
/// <summary>
/// UI页面类型
/// </summary>
public UIPageType UIPageType { get; set; }
/// <summary>
/// 是否要关闭其他相关UI
/// </summary>
public bool CloseOther { get; set; } = default;
/// <summary>
/// 附带数据
/// </summary>
public object Data { get; set; } = default;
}
/// <summary>
/// 持久化UI管理器
/// </summary>
public class UIManager : MonoSingleton<UIManager>, IController
{
/// <summary>
/// 关联的所有 Canvas 场景
/// </summary>
public Transform[] scenes;
/// <summary>
/// 返回应用实例
/// </summary>
/// <returns>IArchitecture</returns>
public IArchitecture GetArchitecture() => QuickCardApp.Interface;
/// <summary>
/// 子场景列表
/// </summary>
public Dictionary<UISceneType, LinkedList<UIPageType>> Scenes { private set; get; } = new();
/// <summary>
/// UI页面列表
/// </summary>
public Dictionary<UIPageType, GameObject> Pages { private set; get; } = new();
/// <summary>
/// 初始化UI
/// </summary>
public async UniTask InitUI()
{
// todo:这里按照可能需要加上新手指引功能
// 遍历场景枚举并且注入到场景列表
foreach (UISceneType value in Enum.GetValues(typeof(UISceneType)))
{
Scenes[value] = new LinkedList<UIPageType>();
}
}
/// <summary>
/// 通过页面检索所属场景
/// </summary>
/// <param name="ty">UIPageType</param>
/// <returns>UISceneType</returns>
private UISceneType GetSceneTypeByUIPageType(UIPageType ty)
{
foreach (var value in Scenes.Where(value => value.Value.Contains(ty)))
{
return value.Key;
}
return UISceneType.Main;
}
/// <summary>
/// 销毁UI页面
/// </summary>
/// <param name="ty"></param>
private void DestroyUIPageByUIPageType(UIPageType ty)
{
if (Pages.TryGetValue(ty, out var page))
{
Destroy(page);
Pages.Remove(ty);
}
else
{
Debug.Log($"Non Exists Page {ty}");
}
}
/// <summary>
/// 通过场景来激活|取消指定页面
/// </summary>
/// <param name="ty">UISceneType</param>
/// <param name="active">bool</param>
public void SetActiveByUISceneType(UISceneType ty, bool active)
{
foreach (var value in Scenes[ty].Where(value => Pages.ContainsKey(value)))
{
Pages[value].SetActiveFast(active);
}
}
/// <summary>
/// 通过页面来激活|取消指定页面
/// </summary>
/// <param name="ty">UIPageType</param>
/// <param name="active">bool</param>
public void SetActiveByUIPageType(UIPageType ty, bool active)
{
if (!Pages.ContainsKey(ty))
{
Debug.Log($"Non Exists Page {ty}");
return;
}
Pages[ty].SetActiveFast(active);
}
/// <summary>
/// 切换UI页面
/// </summary>
/// <param name="uiPageInfo">页面信息</param>
public void SetUIPage(UIPageInfo uiPageInfo)
{
SetUIPageAsync(uiPageInfo).Forget();
}
/// <summary>
/// 异步切换界面
/// </summary>
/// <param name="uiPageInfo">页面信息</param>
/// <returns></returns>
public async UniTask<bool> SetUIPageAsync(UIPageInfo uiPageInfo)
{
// 是否关闭其他页面
if (uiPageInfo.CloseOther)
{
foreach (var page in Pages)
{
page.Value.SetActiveFast(false);
}
}
// 确认目前是在UI所属对象场景
if (Pages.ContainsKey(uiPageInfo.UIPageType) &&
Scenes[uiPageInfo.UISceneType].Contains(uiPageInfo.UIPageType))
{
// 页面本身处于所属场景, 直接激活界面
Pages[uiPageInfo.UIPageType].SetActiveFast(true);
SetPageInfo(uiPageInfo);
}
else if (Pages.ContainsKey(uiPageInfo.UIPageType) &&
!Scenes[uiPageInfo.UISceneType].Contains(uiPageInfo.UIPageType))
{
// 页面不存在当前场景, 需要将移动到场景并激活
var ty = uiPageInfo.UISceneType;
Pages[uiPageInfo.UIPageType].transform.SetParent(
scenes[(int)ty], false);
Pages[uiPageInfo.UIPageType].SetActiveFast(true);
Scenes[GetSceneTypeByUIPageType(uiPageInfo.UIPageType)].AddLast(uiPageInfo.UIPageType);
Scenes[uiPageInfo.UISceneType].AddLast(uiPageInfo.UIPageType);
SetPageInfo(uiPageInfo);
}
else
{
// 这里还可以做判断网络|本地来动态加载
// 默认以 Assets/Prefabs/UIPage/xxx.prefab 动态加载
var resourcePath = GetUIPageResourceByUIPageType(uiPageInfo.UIPageType);
Debug.Log($"Load UI Resource({uiPageInfo.UIPageType}): {resourcePath}");
// 获取资源
var resource = await this
.GetSystem<IResourceSystem>()
.LoadResourceAsync<GameObject>(resourcePath);
// 资源加载成功
if (resource.Status == AsyncOperationStatus.Succeeded)
{
var page = Instantiate(resource.Result, scenes[(int)uiPageInfo.UISceneType], false);
Pages[uiPageInfo.UIPageType] = page;
Scenes[uiPageInfo.UISceneType].AddLast(uiPageInfo.UIPageType);
SetPageInfo(uiPageInfo);
}
else
{
Debug.LogError($"Not Found UI({uiPageInfo.UIPageType}): {resourcePath}");
return false;
}
}
return true;
}
/// <summary>
/// 设置界面信息
/// </summary>
/// <param name="uiPageInfo">UIPageInfo</param>
private void SetPageInfo(UIPageInfo uiPageInfo)
{
// UI面板初始化
var panel = Pages[uiPageInfo.UIPageType].GetComponent<UIPanel>();
if (uiPageInfo.Data != null && panel != null)
{
panel.InitPanel(uiPageInfo.Data);
}
// UI前置
Pages[uiPageInfo.UIPageType].transform.SetAsFirstSibling();
}
/// <summary>
/// 获取资源预制体, 可能是本地也可能是网络资源
/// </summary>
/// <param name="uiPageType"></param>
/// <returns></returns>
private string GetUIPageResourceByUIPageType(UIPageType uiPageType)
{
return $"{Configuration.Assets.UiPagePath}{uiPageType}{Configuration.Assets.FileSuffix}";
}
/// <summary>
/// 卸载回调
/// </summary>
private void OnDestroy()
{
foreach (var value in Pages.Where(value => value.Value != null))
{
Destroy(value.Value);
}
Pages.Clear();
}
}
}
这个类当中 InitUI 实际上就是就是遍历 UISceneType 内部所有场景, 给场景构建一张 Page 页面映射表;
剩下最关键的方法就是 SetUIPageAsync 切换 UI 页面, 管理器最核心的调用方法.
SetUIPageAsync 内部就是本地|网络加载资源预制体资源然后设置在 UI 框架,
GetUIPageResourceByUIPageType 方法加载 Configuration 对象实际上是全局配置类:
/// <summary>
/// 全局所需配置
/// </summary>
public class Configuration
{
/// <summary>
/// 资源配置
/// </summary>
public static class Assets
{
/// <summary>
/// 文件后缀
/// </summary>
public const string FileSuffix = ".prefab";
/// <summary>
/// UI页面预制体目录
/// </summary>
public const string UiPagePath = "Assets/Prefabs/UIPage/";
}
}
默认采用本地加载, 也就是回去项目下的 Assets/Prefabs/UIPage 目录动态加载指定预制体.
最后这里还需要对 UIManager 的 scenes 字段手动构建, 具体情况如下:
可以看到些 System.ResourceSystem 错误, 这是因为本地预制体资源本身不存在导致的,
这也是关键的需要切换的场景页面