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开始加载场景