:使用 SceneSystem 管理 Unity 场景资源)
前面介绍的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、根节点、加载进度和场景资源释放。