前面介绍的GameScene和SceneProcedure负责管理游戏的业务流程,但它们并不直接加载 Unity 的.unity场景文件。
真正负责 Unity 场景注册、异步加载、显示隐藏和资源卸载的是SceneSystem。
项目地址:
https://github.com/ZHOURUIH/MyFramework
一、GameScene 和 SceneSystem 的区别
这两个系统的名字相似,但职责完全不同。
GameScene 管理游戏业务流程 SceneProcedure 管理一个业务流程中的不同阶段 SceneSystem 管理真正的 Unity Scene 资源 SceneInstance 封装一个已经加载的 Unity Scene例如进入战斗时,业务流程可能是:
MainSceneLoading ↓ MainSceneGaming ↓ MainSceneExit这是GameScene管理的内容。
而下面这些操作属于SceneSystem:
加载 Battle.unity 显示或隐藏 Battle_Root 将 Battle 设置为 Active Scene 卸载 Battle.unity 释放场景目录中的资源二者通常配合使用:
SceneProcedure 决定什么时候进入战斗 ↓ SceneSystem 负责加载战斗场景资源二、创建 SceneInstance
每个需要由SceneSystem管理的 Unity 场景,都可以创建一个对应的SceneInstance。
例如战斗场景:
public class BattleSceneInstance : SceneInstance { public override void init() { base.init(); // 获取场景中的固定节点 // 初始化场景逻辑 } public override void onShow() { // 场景被显示 } public override void onHide() { // 场景被隐藏 } public override void update(float elapsedTime) { // 只有场景处于显示状态时才会更新 } public override void lateUpdate(float elapsedTime) { } public override void destroy() { // 清理场景相关引用 base.destroy(); } }SceneInstance主要保存:
Unity Scene 对象 场景根节点 场景名称 场景逻辑类型 加载状态 加载进度回调 加载完成回调 是否显示 是否设置为主场景只有处于显示状态的场景,才会执行update()和lateUpdate()。
三、场景必须有固定根节点
SceneSystem加载完成后,会查找:
场景名_Root假设场景文件名为:
Battle.unity场景层级应该是:
Battle └── Battle_Root ├── Environment ├── Light ├── MonsterRoot ├── EffectRoot └── Other框架通过Battle_Root控制整个场景的显示和隐藏。
内部加载完成后会执行:
scene.setRoot( findRootGameObject( scene.getName() + "_Root", true));因此根节点名称必须正确,并且最好保证场景中的所有内容都放在这个根节点下。
调用:
mSceneSystem.hideScene("Battle");本质上不会卸载场景,而是关闭:
Battle_Root四、注册场景
场景使用前需要注册:
using static FrameBaseHotFix; public static class SceneRegister { public static BattleSceneInstance mBattleScene; public static void registeAll() { mSceneSystem.registeScene( typeof(BattleSceneInstance), "Scene/Battle/Battle.unity", (instance) => { mBattleScene = instance as BattleSceneInstance; }); } }然后在GameHotFix.registerAll()中调用:
protected override void registerAll() { LayoutRegisterHotFix.registeAll(); SceneRegister.registeAll(); }注册信息包括:
SceneInstance 类型 场景文件路径 场景实例变化回调路径相对于:
Assets/GameResources并且必须以.unity结尾:
Scene/Battle/Battle.unity场景名称会自动从文件名中获得:
Battle.unity ↓ Battle回调会在场景实例创建时传入对应的SceneInstance,卸载时传入null,因此可以用来保存和清空场景引用。
这个回调不表示 Unity 场景已经加载完成。真正的加载完成逻辑,应使用loadSceneAsync()的完成回调。
五、异步加载场景
SceneSystem只提供异步场景加载:
protected CustomAsyncOperation mLoadOperation; protected void loadBattleScene() { mLoadOperation = mSceneSystem.loadSceneAsync( "Battle", true, true, onBattleSceneLoaded, onBattleSceneLoading); } protected void onBattleSceneLoading(float progress) { mUILoading?.setLoadPercent(progress); } protected void onBattleSceneLoaded() { log("战斗场景加载完成"); }参数含义为:
Battle 注册时生成的场景名称 true 加载完成后立即显示 true 加载完成后设置为 Unity Active Scene onBattleSceneLoaded 加载完成回调 onBattleSceneLoading 加载进度回调返回的CustomAsyncOperation可以继续加入:
Waiting AsyncTaskGroup CustomMultiAsyncOperation例如同时等待场景、角色和服务器数据:
Waiting waiting = mWaitingManager.createWaiting( onPrepareFinished); waiting.addAsyncOperation( mSceneSystem.loadSceneAsync( "Battle", true, true, null)); waiting.addCondition( () => mPlayerCreated); waiting.addCondition( () => mServerDataReady);六、内部为什么统一使用 Additive
框架加载场景时固定使用:
SceneManager.LoadSceneAsync( scene.getName(), LoadSceneMode.Additive);也就是所有场景都以叠加模式加载。
这样SceneSystem才能同时管理多个场景:
基础场景 战斗场景 角色展示场景 家园场景 预览场景加载新场景时,不会由 Unity 自动销毁之前的场景,而是由框架决定:
哪些场景继续显示 哪些场景暂时隐藏 哪些场景真正卸载SceneManager.LoadScene()虽然名字看起来是同步加载,但函数返回时场景不一定已经完全可用,因此框架不再提供表面上的同步接口。
七、场景加载的内部流程
loadSceneAsync()的主要流程是:
根据场景名查找注册信息 ↓ 创建对应的 SceneInstance ↓ 通过 ResourceManager 预加载场景 AssetBundle ↓ 使用 Additive 异步加载 Unity Scene ↓ 持续发送加载进度 ↓ 获取 Unity Scene 对象 ↓ 查找 场景名_Root ↓ 执行 SceneInstance.init() ↓ 根据参数显示或隐藏场景 ↓ 执行加载完成回调核心逻辑可以简化为:
AsyncOperation operation = SceneManager.LoadSceneAsync( scene.getName(), LoadSceneMode.Additive); scene.setScene( SceneManager.GetSceneByName( scene.getName())); scene.setRoot( findRootGameObject( scene.getName() + "_Root", true)); scene.init();SceneInstance.init()中还会扫描场景节点使用的 Shader,保证动态加载后的材质能够正确找到对应 Shader。
八、显示和隐藏场景
隐藏场景:
mSceneSystem.hideScene("Battle");隐藏只会关闭根节点,并调用:
BattleSceneInstance.onHide();场景仍然保留在内存中,之后可以快速重新显示。
显示场景:
mSceneSystem.showScene("Battle");默认参数为:
showScene( "Battle", true, true);表示:
隐藏其他已加载场景 显示 Battle 将 Battle 设置为主场景需要同时显示多个场景时:
mSceneSystem.showScene( "CharacterPreview", false, false);表示不隐藏其他场景,也不修改当前主场景。
单独设置主场景:
mSceneSystem.setMainScene( "Battle");它会调用 Unity 的:
SceneManager.SetActiveScene(...);九、隐藏和卸载不是一回事
隐藏场景:
mSceneSystem.hideScene("Battle");结果是:
Scene 仍然已加载 资源仍然保留 Battle_Root 被关闭 停止 SceneInstance 更新 可以快速再次显示卸载场景:
mSceneSystem.unloadScene("Battle");结果是:
通知场景引用回调传入 null 销毁 SceneInstance 调用 SceneManager.UnloadSceneAsync 从已加载场景列表中移除 卸载场景目录中的资源因此,短时间后还会重新使用的场景可以先隐藏。
确定离开并且短时间内不会再回来时,再真正卸载。
十、场景目录与资源卸载
unloadScene()默认参数为:
mSceneSystem.unloadScene( "Battle", true);第二个参数表示是否卸载场景所在目录中的资源。
框架会调用:
mResourceManager.unloadPath( getFilePath(scenePath));因此推荐按场景组织独占资源:
GameResources └── Scene └── Battle ├── Battle.unity ├── Model ├── Material ├── Texture └── Effect这个目录中最好只放战斗场景独占资源。
多个场景共享的资源应放到公共目录,避免场景卸载时把资源目录一起清理。
不希望卸载目录资源时:
mSceneSystem.unloadScene( "Battle", false);卸载除指定场景以外的其他场景:
mSceneSystem.unloadOtherScene( "Battle");适合在进入新区域后清理旧场景。
十一、总结
MyFramework 中与场景相关的职责分为两层:
GameScene + SceneProcedure 管理游戏业务流程 SceneSystem + SceneInstance 管理 Unity 场景资源SceneSystem的基本使用流程是:
1. 创建 SceneInstance 子类 2. 为 Unity 场景添加 场景名_Root 3. 使用 registeScene 注册场景 4. 使用 loadSceneAsync 异步加载 5. 使用 showScene 和 hideScene 控制显示 6. 使用 setMainScene 设置 Active Scene 7. 不再使用时调用 unloadScene所有场景统一使用 Additive 模式加载,再由框架控制显示、更新和卸载。
这样业务流程只需要决定什么时候进入或离开场景,不需要在各个模块中重复处理 Unity Scene、根节点、加载进度和场景资源释放。