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

时间:2024-02-18 12:59:34

概述地址: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开始加载场景

 

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