GameFrameWork框架(Unity3D)使用笔记(八) 实现场景加载进度条

前言:

        游戏在转换场景的时候,需要花费时间来加载相关的资源。而这个过程往往因为游戏场景的规模和复杂度以及玩家电脑配置的原因花费一小段时间(虽然这个项目里用不到)。

        所以,如果这一小段时间,画面就卡在这里,啥也做不了,玩家也不知道啥时候能加载好。这个等待的时间实际上非常地影响玩家的使用体验。

        目前大多数游戏在转换关卡这种时候都会有个加载界面,显示加载进度。这样玩家可以对啥时候能加载好有个心理预估(判断要不要因为加载太久浪费时间不如卸载游戏(开个玩笑))。

        一般加载场景显示进度条的方法搜搜就有了,就是利用Unity自带的异步加载函数SceneManager.LoadSceneAsync()加载场景,并且通过AsyncOperation跟踪加载进度,从而设置进度条之类的。

        不过,在GameFramework框架下,加载场景的模块被进一步封装,那怎么在UGF下实现加载的进度条就是本篇的主要内容。


一、实现过程讲解

        我看过一些非GF的加载场景的方案,大多数都是:对于从场景a-->场景b的过程,将其变为从场景a-->场景c-->场景b

        其中,场景c里面主要就只有一个加载界面,主要用来显示进度条等内容。这样的话,从a->c可以非常快速地跳转(因为c中就只有个UI所以即便配置不高也能很快跳转)然后玩家在c中观看进度条的时候,后台异步加载场景b,加载完毕后立刻转到场景b。

        但是,在GF框架下,有个不同的地方。就是GF框架预制体所在的Launcher场景是一直存在的,并且框架的UI统一在这个场景里管理。

        所以在GF里实现进度条的功能,我的方案是直接在ProcedureChangeScene流程里面加载新场景的同时显示进度条UI,并且在加载完成后关闭UI。

        此外,由于一些原因(参考这一篇:http://t.csdn.cn/65FDe),想要测试这个进度条的真正效果,需要把当前工程Build再运行测试。


一、拼制UI

 在场景里面拼个进度条UI:

然后写UI的逻辑脚本,这里实现控制UI进度条的函数方便之后调用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace ShadowU
{
    public class LoadingForm: UGuiForm
    {
        public GameObject mask;     //进度条的遮罩
        public Text loadingText;    //加载中的文字

        protected override void OnClose(bool isShutdown, object userData)
        {
            base.OnClose(isShutdown, userData);
        }

        protected override void OnInit(object userData)
        {
            base.OnInit(userData);
        }

        protected override void OnOpen(object userData)
        {
            base.OnOpen(userData);
            loadingText.text = "加载中.  .  .  .  .  .  ";
        }

        protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(elapseSeconds, realElapseSeconds);
            //控制加载中文字后的句号浮动
            string temp = loadingText.text;
            temp.Insert(3, temp[temp.Length - 1].ToString()).Remove(temp.Length - 1);
            loadingText.text = temp;
        }
        void SetProcess(float a)
        {
            a = Mathf.Clamp(a,0f,1.0f);//将数值控制在0-1
            mask.transform.SetLocalScaleX(a);
        }
    }
}

接上脚本,设置好public参数,做成预制体:


二、修改ProcedureChangeScene流程代码

         ProcedureChangeScene开始的时候,加载出Loading界面,同时开始异步加载新场景。

        通过LoadSceneUpdateEvent事件来更新加载界面进度条的进度。

        最后,在场景加载完成后关闭Loading界面即可。

        新的ProcedureChangeScene代码如下:


using GameFramework.Fsm;
using GameFramework.Event;
using GameFramework.DataTable;
using UnityGameFramework.Runtime;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;

namespace ShadowU
{
    public class ProcedureChangeScene : ProcedureBase
    {
        private const int MenuSceneId = 1;   //菜单场景的ID 

        private bool m_ChangeToMenu = false; //是否转换为菜单
        private bool m_IsChangeSceneComplete = false; //转换场景是否结束

        private LoadingForm m_LoadingForm;    //加载界面
        public override bool UseNativeDialog
        {
            get
            {
                return true;
            }
        }

        protected override void OnEnter(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner)
        {
            base.OnEnter(procedureOwner);

            Debug.Log("ChangeScene!");
            m_IsChangeSceneComplete = false; //如果为true的话 则在update中会切换流程

            //打开加载界面
            GameEntry.UI.OpenUIForm(AssetUtility.GetUIFormAsset("LoadingForm"), "Default", 1, this);

            //注册事件
            GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId,OnLoadSceneSuccess);
            GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //隐藏所有实体
            GameEntry.Entity.HideAllLoadingEntities();
            GameEntry.Entity.HideAllLoadedEntities();

            //卸载所有场景
            string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames();

            foreach (string sn in loadedSceneAssetNames)
                GameEntry.Scene.UnloadScene(sn);

            //还原游戏速度
            GameEntry.Base.ResetNormalGameSpeed();

            
            //获取下一个场景的ID
            int sceneId = procedureOwner.GetData<VarInt32>("NextSceneId");
            m_ChangeToMenu = sceneId == MenuSceneId; //判断是否是转到菜单
            IDataTable<DRScene> dtScene = GameEntry.DataTable.GetDataTable<DRScene>();
            DRScene drScene = dtScene.GetDataRow(sceneId); //获取数据表行
            if(drScene == null)
            {
                Log.Warning("Can not load scene '{0}' from data table.", sceneId.ToString());
                return;
            }
            GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName),Constant.AssetPriority.SceneAsset,this);
        }



        protected override void OnLeave(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, bool isShutdown)
        {
            base.OnLeave(procedureOwner, isShutdown);

            GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
            GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //关闭UI
            if (m_LoadingForm != null)
            {
                m_LoadingForm.Close(isShutdown);
                m_LoadingForm = null;
            }
        }

        protected override void OnUpdate(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);


            if (!m_IsChangeSceneComplete)   
            {
                return;  //还没完成场景切换
            }

            if (m_ChangeToMenu)
            {
                ChangeState<ProcedureMenu>(procedureOwner);   //菜单
            }else
            {
                ChangeState<ProcedureMain>(procedureOwner);   //游戏运行流程
            }
        }
        
        private void OnLoadSceneSuccess(object sender,GameEventArgs e)
        {
            LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e;
            if(ne.UserData != this)
            {
                return;
            }
            Log.Info("Load scene '{0}' OK.", ne.SceneAssetName);
            m_IsChangeSceneComplete = true;
        }
        private void OnLoadSceneFailure(object sender, GameEventArgs e)
        {
            LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage);
        }

        private void OnLoadSceneUpdate(object sender, GameEventArgs e)
        {
            LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2"));
            //更新加载界面的进度条
            if(m_LoadingForm != null)
                m_LoadingForm.SetProcess(ne.Progress);
        }

        private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e)
        {
            LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName, ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
        }
        private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
        {
            OpenUIFormSuccessEventArgs ne = (OpenUIFormSuccessEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            m_LoadingForm = (LoadingForm)ne.UIForm.Logic;
        }
    }
}

 三、测试效果

        直接编辑器模式运行的话看场景加载并不是异步的,所以要测试这个进度条的真正效果需要打包运行。

        关于打包的过程之后的文章里会讲,这里就先跳过了。

        测试:

 进度条的功能是完成了!

但是。。。又出了新的问题:菜单界面在加载界面之上。

不过编辑器模式运行并没有这个问题:

    

但是没有问题只是看上去的。。。如果我立即暂停后观察,其实这个模式下也会两个UI同屏,只不过相对来说时间比较短暂。我经过一段时间分析后,想到UGuiForm里面在关闭和打开界面的时候都有淡入淡出的逻辑,于是作出如下假设:

加载界面看到菜单界面并不是渲染层级先后的问题,而是它们都在淡入或淡出过程中。所以说,他们都处于半透明状态所以能都看得见。

(因为基于这个假设能解决问题那我就不去证明我这个假设到底对不对了嘿嘿~) 

那解决办法就很灵活了。比如可以判断取消打开加载界面的淡入效果。不过我觉得这样改结构会很不美观。

于是我打算这样处理:让加载界面至少停留一秒。同时减小淡入淡出的时间。

在UGuiForm.cs里面修改淡入淡出时间

修改一下ProcedureChangeScene的代码(增加了和loadingTimer变量有关的部分):


using GameFramework.Fsm;
using GameFramework.Event;
using GameFramework.DataTable;
using UnityGameFramework.Runtime;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;

namespace ShadowU
{
    public class ProcedureChangeScene : ProcedureBase
    {
        private const int MenuSceneId = 1;   //菜单场景的ID 

        private bool m_ChangeToMenu = false; //是否转换为菜单
        private bool m_IsChangeSceneComplete = false; //转换场景是否结束

        private LoadingForm m_LoadingForm;    //加载界面
        private float loadingTimer;           //加载界面停留的计时器
        public override bool UseNativeDialog
        {
            get
            {
                return true;
            }
        }

        protected override void OnEnter(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner)
        {
            base.OnEnter(procedureOwner);

            Debug.Log("ChangeScene!");
            m_IsChangeSceneComplete = false; //如果为true的话 则在update中会切换流程

            //打开加载界面
            GameEntry.UI.OpenUIForm(AssetUtility.GetUIFormAsset("LoadingForm"), "Default", 1, this);
            loadingTimer = 1.0f;         //加载界面至少停留一秒

            //注册事件
            GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId,OnLoadSceneSuccess);
            GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //隐藏所有实体
            GameEntry.Entity.HideAllLoadingEntities();
            GameEntry.Entity.HideAllLoadedEntities();

            //卸载所有场景
            string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames();

            foreach (string sn in loadedSceneAssetNames)
                GameEntry.Scene.UnloadScene(sn);

            //还原游戏速度
            GameEntry.Base.ResetNormalGameSpeed();

            
            //获取下一个场景的ID
            int sceneId = procedureOwner.GetData<VarInt32>("NextSceneId");
            m_ChangeToMenu = sceneId == MenuSceneId; //判断是否是转到菜单
            IDataTable<DRScene> dtScene = GameEntry.DataTable.GetDataTable<DRScene>();
            DRScene drScene = dtScene.GetDataRow(sceneId); //获取数据表行
            if(drScene == null)
            {
                Log.Warning("Can not load scene '{0}' from data table.", sceneId.ToString());
                return;
            }
            GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName),Constant.AssetPriority.SceneAsset,this);
        }



        protected override void OnLeave(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, bool isShutdown)
        {
            base.OnLeave(procedureOwner, isShutdown);

            GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
            GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //关闭UI
            if (m_LoadingForm != null)
            {
                m_LoadingForm.Close(isShutdown);
                m_LoadingForm = null;
            }
        }

        protected override void OnUpdate(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);


            if (!m_IsChangeSceneComplete)   
            {
                return;  //还没完成场景切换
            }

            m_LoadingForm.SetProcess(1);

            if(loadingTimer > 0)  
            {
                loadingTimer -= elapseSeconds;//更新计时器
                return;
            }

            if (m_ChangeToMenu)
            {
                ChangeState<ProcedureMenu>(procedureOwner);   //菜单
            }else
            {
                ChangeState<ProcedureMain>(procedureOwner);   //游戏运行流程
            }
        }
        
        private void OnLoadSceneSuccess(object sender,GameEventArgs e)
        {
            LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e;
            if(ne.UserData != this)
            {
                return;
            }
            Log.Info("Load scene '{0}' OK.", ne.SceneAssetName);
            m_IsChangeSceneComplete = true;
        }
        private void OnLoadSceneFailure(object sender, GameEventArgs e)
        {
            LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage);
        }

        private void OnLoadSceneUpdate(object sender, GameEventArgs e)
        {
            LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2"));
            //更新加载界面的进度条
            if(m_LoadingForm != null)
                m_LoadingForm.SetProcess(ne.Progress);
        }

        private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e)
        {
            LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName, ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
        }
        private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
        {
            OpenUIFormSuccessEventArgs ne = (OpenUIFormSuccessEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            m_LoadingForm = (LoadingForm)ne.UIForm.Logic;
        }
    }
}

此外还有个细节:就是进度条到90就不动了。其实场景加载到90%的时候就已经加载完了,最后的10%就在于有没有把场景显示出来。那为了看起来不膈应,我上面的代码里在加载完场景后手动把进度条设置为100:

打包看下最终效果:

 效果挺理想的,那就这样了!

        

猜你喜欢

转载自blog.csdn.net/HowToPause/article/details/128431853