zoukankan      html  css  js  c++  java
  • 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开始加载场景

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

  • 相关阅读:
    day25:接口类和抽象类
    vue1
    How the weather influences your mood?
    机器学习实验方法与原理
    How human activities damage the environment
    Slow food
    Brief Introduction to Esports
    Massive open online course (MOOC)
    Online learning in higher education
    Tensorflow Dataset API
  • 原文地址:https://www.cnblogs.com/wang-jin-fu/p/11128974.html
Copyright © 2011-2022 走看看