unity游戏框架学习-场景管理

概述地址:http://www.javashuo.com/article/p-nggymcxb-bw.htmlhtml

unity SceneManager API:https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.html,咱们用到的接口主要有如下三个缓存

SceneManager.GetActiveScene 获取当前活动场景app

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.async

SceneManager.LoadSceneAsync(int sceneBuildIndex, SceneManagement.LoadSceneMode mode = LoadSceneMode.Single); 异步加载场景,异步加载可以得到加载过程的进度和是否加载完成,经过这种方式你能够在切换中增减进度条或者其余表现ui

参数mode说明:lua

LoadSceneMode.Single :Closes all current loaded Scenes and loads a Scene.在加载完成后以后将会马上销毁原先场景中的物体
LoadSceneMode.Additive :Adds the Scene to the current loaded Scenes.加载后将会保留原先的场景中的物体spa

 

在概述里咱们说到,场景模块的功能主要如下几个3d

1.场景的加载、卸载、回到上一个场景,这边不涉及ab包的加、卸载,ab包的维护在资源管理模块,这边在ab加载完成的回调里调用Unity的API就好了code

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

 

场景模块到这边就结束了~