unity游戏框架学习-场景管理
概述地址:https://www.cnblogs.com/wang-jin-fu/p/10975660.html
unity SceneManager API:https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.html,我们用到的接口主要有以下三个
SceneManager.GetActiveScene 获取当前活动场景
SceneManager.LoadScene(int sceneBuildIndex, SceneManagement.LoadSceneMode mode = LoadSceneMode.Single); 同步加载场景,同步加载会有延迟一帧,大场景还会导致游戏卡顿,建议使用异步加载。官方说明如下:
When using SceneManager.LoadScene, the loading does not happen immediately, it completes in the next frame. This semi-asynchronous behavior can cause frame stuttering and can be confusing because load does not complete immediately.
SceneManager.LoadSceneAsync(int sceneBuildIndex, SceneManagement.LoadSceneMode mode = LoadSceneMode.Single); 异步加载场景,异步加载能够获得加载过程的进度和是否加载完成,通过这种方式你可以在切换中增减进度条或者其他表现
参数mode说明:
LoadSceneMode.Single :Closes all current loaded Scenes and loads a Scene.在加载完成后之后将会立刻销毁原先场景中的物体
LoadSceneMode.Additive :Adds the Scene to the current loaded Scenes.加载后将会保留原先的场景中的物体
在概述里我们说到,场景模块的功能主要以下几个
1.场景的加载、卸载、回到上一个场景,这边不涉及ab包的加、卸载,ab包的维护在资源管理模块,这边在ab加载完成的回调里调用Unity的API就行了
2.加载新的场景时需要卸载旧场景的的资源,清除GC
3.支持场景资源的预加载,部分场景可能会很大,例如战斗场景,可以预先加载部分模型,后面使用会比较流畅
那么加载一个新的场景大概是以下流程:(使用AssetBundle,不使用ab包可忽略1.4步骤)
1.卸载上一个场景的ab资源(可选)
2.打开场景过渡界面或过渡场景
3.通知ui退出当前场景的界面,关闭场景ui,回收资源(缓存的gameobject,正在加载的资源)
4.加载当前场景的ab包(可选)
5.加载场景(调用unity的SceneManager.LoadScene或SceneManager.LoadSceneAsync接口)
6.预加载资源(可选)
7.清除gc,清除无用的资源
LuaModule.Instance.LuaGCCollect();
Resources.UnloadUnusedAssets();
System.GC.Collect();
8.通知ui进入新的场景,打开场景ui
9.关闭场景过渡界面或过渡场景
好了。场景模块的代码分两块,一块是场景的基类,这个类的周期随着场景的加载开始,场景的销毁结束,每个场景都应该有自己的场景类,并在这个类里实现自己的逻辑(如打开场景ui,加载场景对象),他的结构是这样子的:
local _PATH_HEAD = "game.modules.scene." local SceneBase = class("SceneBase", ObjectBase) --场景加载前会new一个SceneBase,通知业务做一些初始化的东西(这个时候场景是还没加载,对象是找不到的) function SceneBase:ctor(sceneConfig) SceneBase.super.ctor(self) self._sceneType = sceneConfig.stype self._sceneID = sceneConfig.id self._sceneName = sceneConfig.scene self._sceneFolder = sceneConfig.sceneFolder self._loadState = LoadState.NONE self._sceneMusic = sceneConfig.music self.SceneRoot = nil -- 根节点 Transform类型 self._isEnter = false self._businessCollect = {} self._sceneParam = nil -- 切换场景 外部传进来的参数 end -- 加载场景,先加载ab包,ab包加载完成后会调用unity的SceneManager.LoadSceneAsync接口,异步加载完成后回调DoSceneLoaded function SceneBase:Load(param, onComplete, isback) self._sceneParam = param self._onLoadComplete = onComplete self._loadState = LoadState.LOADING self._isback = isback me.modules.load:LoadScene(self._sceneName, self._sceneFolder, handler(self, self.DoSceneLoaded)) end function SceneBase:DoSceneLoaded(data) self._loadState = LoadState.LOADED me.modules.ui:SceneEnter(self._sceneID, self._isback) if self._sceneMusic then SoundUtil.PlayMusic(self._sceneMusic) end self:OnLoaded(self._sceneParam) self:Enter() if self._onLoadComplete then self._onLoadComplete(self) end end -- 场景加载完成,通知业务可以实例化对象了 function SceneBase:OnLoaded() local root = GameObject.Find("SceneRoot") if root then HierarchyUtil.ExportToTarget(root, self) self.SceneRoot = root.transform me.MainCamera = self.MainCamera me.SceneUIRoot = self.SceneUIRoot Config.Instance.MainCamera = self.MainCamera if self.SceneUIRoot then me.SceneCanvas = self.SceneUIRoot.gameObject:GetComponent("Canvas") end end end function SceneBase:Enter() if not self._isEnter then self._isEnter = true self:OnStart() end end --通知业务开始监听事件,打开界面等等 function SceneBase:OnStart() end function SceneBase:Update(dt) end --通知业务取消监听事件,关闭界面等等 function SceneBase:Exit() if self._isEnter then self._isEnter = false self:OnEnd() self:OnExit() end end function SceneBase:OnEnd() end -- 退出场景 function SceneBase:OnExit() end --场景销毁前调用,通知业务移除事件,删除对象 function SceneBase:Dispose() self._loadState = LoadState.NONE if self.SceneRoot then HierarchyUtil.RemoveFromTarget(self) end for i = 1, #self._businessCollect do self._businessCollect[i]:Dispose() end self._businessCollect = {} SceneBase.super.Dispose(self) end ----------------------------------- function SceneBase:IsEnter() return self._isEnter end function SceneBase:OnBeforeRelogin() self:Exit() end --断线重连 function SceneBase:OnRelogin() self:Enter() end function SceneBase:IsLoading() return self._loadState==LoadState.LOADING end return SceneBase
他的生命周期是这样子的:ctor-Load-DoSceneLoaded-OnLoaded-Enter-OnStart-Update-Exit-OnEnd-Dispose,Enter(Exit)和OnStart(OnEnd)的区别是,前者是基类的私有方法,用于维护基类的self._isEnter属性,后者是由子类继承重现的方法。OnLoaded方法用于子类监听按钮事件,实例化对象,OnStart主要是给业务处理逻辑的。
场景模块的另一块是SceneBase的管理类,用于维护场景类的生命周期。
local CURRENT_MODULE_NAME = ... local SceneModule = class("SceneModule", ModuleBase) function SceneModule:ctor() SceneModule.super.ctor(self) self._currScene = nil self._sceneBackStack = {} GameMsg.AddMessage("GAME_RELOGIN_FINISH", self, self.OnRelogin) end -- 正在加载场景 function SceneModule:IsLoading() if not self._currScene then return false end return self._currScene:IsLoading() end -- 获取场景类型 function SceneModule:GetSceneID() if not self._currScene then return -1 end return self._currScene:GetSceneID() end function SceneModule:Update(dt) if self._currScene and self._currScene:IsEnter() then self._currScene:Update(dt) end end function SceneModule:OnRelogin() self._currScene:OnRelogin() end function SceneModule:Back(param, onloaded) if self._lastSceenID then self:ChangeScene(self._lastSceenID, param, onloaded, false, true) end end --返回到上次记录的场景,如果上次记录为空,则返回上个场景 function SceneModule:PopSceneStack(param, onloaded) local count = #self._sceneBackStack if count > 0 then local sceneId = self._sceneBackStack[count] self._sceneBackStack[count] = nil self:ChangeScene(sceneId, param, onloaded, false, true) else self:Back(param,onloaded) end end --清空场景记录 function SceneModule:ClearSceneStack() self._sceneBackStack = {} end -- 切换场景 function SceneModule:ChangeScene(sceneID, param, onloaded, sceneUIPush, isback, addSceneBackStack) local currSceneID = self:GetSceneID() if currSceneID==sceneID then printWarning("ChangeScene current scene is the target... sceneID:", sceneID) return end if self:IsLoading() then printWarning("ChangeScene current scene is loading....:", currSceneID, sceneID) return end if currSceneID ~= -1 then printWFF("====StopMusic ", currSceneID) SoundUtil.StopMusic() end self:LoadScene(sceneID, param, onloaded, sceneUIPush, isback, addSceneBackStack) end function SceneModule:LoadAdditiveScene(sceneID, param) local newScene = self:CreateScene(sceneID) if not newScene then return end newScene:Load(param) self._bgScene = newScene end function SceneModule:FocusBgScene() self:ExitCurrent() self._currScene = self._bgScene me.MainScene = self._currScene end --根据场景id加载新的场景 function SceneModule:LoadScene(sceneID, param, onloaded, sceneUIPush, isback, sceneBackStackPush) local newScene = self:CreateScene(sceneID) if not newScene then return end local lastSceneId = self:GetSceneID() if sceneBackStackPush then self._sceneBackStack[#self._sceneBackStack + 1] = lastSceneId end -- 卸载旧的场景 self._lastSceenID = lastSceneId local lastScene = self._currScene if lastScene~= nil then local lastSceneName = lastScene:GetSceneName() local lastSceneType = lastScene:GetSceneType() self:ExitCurrent(sceneUIPush) if lastSceneType~=newScene:GetSceneType() then LuaHelper.UnloadSceneAB(lastSceneName, false) end me.modules.resource:ClearLoad() -- 清除资源 me.modules.resource:ClearPool() me.MainScene = nil me.MainCamera = nil me.SceneUIRoot = nil -- 除了登录场景 其他场景切换都有场景过渡 if lastSceneType ~= SceneDefine.SceneType.LOGIN then -- 如果参数里标记了使用CUTSCENE过渡,那么这边不要打开这个普通过渡界面 local useNormalTransition = true if param then if param.UseCutSceneTransition then useNormalTransition = false elseif param.useCivSceneTransition then useNormalTransition = false me.modules.ui:OpenView(ViewID.CIV_PRE_SCENE,param) end end if useNormalTransition then me.modules.ui:OpenView(ViewID.TRANSITION) end end end self._currScene = newScene --加载新场景 me.MainScene = newScene newScene:Load(param, onloaded, isback) -- 发送场景切换事件 GameMsg.SendMessage("SCENE_CHANGED") end --生成一个SceneBase function SceneModule:CreateScene(sceneID) local sceneConfig = SceneDefine.SceneConfig[sceneID] if not sceneConfig then printError("Can't find scene config... sceneID:",sceneID) return end local sceneClass = import(sceneConfig.path, CURRENT_MODULE_NAME) if not sceneClass then printError("Import new scene fail:",sceneID) return end -- 新场景加载前的准备 local newScene = sceneClass.new(sceneConfig) return newScene end function SceneModule:ExitCurrent(sceneUIPush) if not self._currScene then return end local sceneID = self._currScene:GetSceneID() me.modules.ui:SceneExit(sceneID, sceneUIPush) self._currScene:Exit() self._currScene:Dispose() self._currScene = nil end -- 停止当前逻辑 function SceneModule:OnBeforeRelogin() if not self._currScene then return end self._currScene:OnBeforeRelogin() end return SceneModule
SceneModule最主要的四个方法,
1.ChangeScene:业务调用该接口,用于切换到指定名字的场景
2.Back:我们的UI界面都有返回键,当没有可返回的界面时,会返回到上一场景,也就是这个Back方法
3.PopSceneStack:有些游戏需要记录玩家上一次进入的场景,举个例子。玩家从场景A的a界面进入了场景B,当玩家退出场景B时,需要还原到场景A并打开a界面(a可能是经过c-d-f界面才打开的,这时候还需要还原到上一次的界面栈,这个功能会在后面的UIModule实现)
4.LoadScene:这个是私有方法(lua里面没有这个概念,可以理解成只有SceneModule可以调用这个方法),这是切换代码的核心功能,他完成的内容按顺序如下:
(1.新建下一个场景的SceneBase newScene
(2.退出当前场景并通知ui关闭当前场景ui
(3.清理当前场景缓存的对象、终止正在加载的队列
(4.打开场景过渡界面
(5.通知newScene开始加载场景
场景模块到这边就结束了~