Unity大场景切换进行异步加载时,如何设计加载进度条,并配置滑动条按照的曲线给定的速率滑动

一、异步加载场景的过程

1、异步加载场景用到的API

LoadSceneAsync

2、异步加载的参数说明

  • (1)默认参数:SceneManagement.LoadSceneAsync("SceneName");
csharp 复制代码
AsyncOperation task = SceneManager.LoadSceneAsync("SceneName");
  • (2)异步加载时的两种加载模式-Single和Additive

    关于【活动场景】和【非活动场景】有些什么差异,本文不做讨论。
csharp 复制代码
//定义加载任务
AsyncOperation loadTask = SceneManager.LoadSceneAsync("场景2", LoadSceneMode.Additive);
//AsyncOperation loadTask = SceneManager.LoadSceneAsync("场景2", LoadSceneMode.Single);

await loadTask;                          //实际加载过程
  • (3)Single异步加载的流程
csharp 复制代码
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("SceneName", LoadSceneMode.Single);

加载的步骤解释:

步骤 进度值 事项描述
第一步 0% ~ 90% 把资源加入到内存中
第二步 初始化新加入的场景,激活新加入的场景,设置为【活动场景】
第三步 新场景激活后,原来的场景会被卸载和清理
第四步 asyncLoad.isDone设置为true 当 isDone 属性为 true 时,表示场景切换已完成,新场景已经完全加载并激活,旧场景已经被卸载。

加载过程:

  • 1、进度值:0%~90% 加载新场景,当到进度值达到90%时,代表场景资源已经加入到内存中,后面10%的时间留给新旧场景的显示切换、新场景初始化、旧场景资源回收。
  • 2、场景要如何切换,【自动显示】还是【代码手动控制】它显示

下面是加载完毕自动显示:

csharp 复制代码
AsyncOperation task = SceneManager.LoadSceneAsync("SceneName");
await loadTask;  

下面是手工控制场景的显示

csharp 复制代码
//定义加载任务
AsyncOperation loadTask = SceneManager.LoadSceneAsync("SceneName");
loadTask.allowSceneActivation = false;   //加载完毕场景不立即显示

await loadTask;                          //实际加载过程

//此处,用户要进行其他操作,比如点击一个按钮,把数据上传到服务器
//...

loadTask.allowSceneActivation = true;    //设置成true后,系统开始后续的10%的工作,会把新场景显示,把老场景卸载

二、大场景切换面临的问题

1、理想情况下的加载

  • (1)、先定义一个Progress的对象(进度报告器),相当于一个事件,当进度值有变化的时候,就会回调该方法。
  • (2)、异步加载资源,并把Progress对象绑定到加载过程。
csharp 复制代码
// 定义进度报告器
var progress = new Progress<float>(value =>
{     
    //更新滑动条进度
    Debug.Log($"加载进度: {value * 100}%");
});

//实际加载
await SceneManager.LoadSceneAsync("场景2", LoadSceneMode.Single).ToUniTask(progress: progress);   

2、实际加载情况

  • (1)、存在的问题

    要加载的是小场景时,进度报告非常丝滑,一点也不卡顿。

    但是,让要加载的是大场景的时候,update主线程卡死,进度不会走,一直在0%,等几秒钟之后,加载完毕时,进度突然跳成100%,这就太刺激了。

    测试环境说明:Unity2021.3.40

  • (2)、改进方法

    把进度条改成加载前、加载中和加载后,真正的加载放在【加载中】。

三、进度条的设计

(1)如何划分进度条

我们把进度条分成三个部分,如下图的阶段一、二、三。

我们把加载的真正过程放在阶段一和阶段二之间,加载时,进度条暂停不动。

为了看起来丝滑一些,我们把进度条设置曲线动画

(2)加载效果

四、如何给进度条配上曲线动画

如何给滑动条设置曲线动画,如下图所示,在1秒内,让滑块从0%跑到33%,跑的时- 候不匀速,而是先快后慢。

(1)曲线运动速度的设置

csharp 复制代码
public class JumpScene : MonoBehaviour
{
    /// <summary>
    /// 【第一段进度条动画】的动画曲线
    /// </summary>
    [Header("【第一段进度条动画】的动画曲线")]
    public AnimationCurve startAnim;
    //......
}

在面板上设置曲线速率

(2)曲线速率与运动的实现:按照给定的曲线速度运动,而不是匀速运动

csharp 复制代码
public async UniTask test4()
{
    var progress = 0.0f;         //当前累计耗时
    var duration = 3f;           //给定的动画时间,3秒
    var progressCurve = 0f;      //曲线进度,按照曲线速度运动,而不是匀速运动
    while (true)
    {
        progress += Time.deltaTime;
        progressCurve = startAnim.Evaluate(progress / duration) / duration;            
        Debug.Log(progressCurve);
        if (progress > duration) break;
        await UniTask.Yield();
    }
}

五、主要代码

(1)过程说明

大场景加载的时候,异步加载的进度值会卡死,因为update主线程卡死了

(无论是协程还是用异步,都没有改观,都是卡死状态)。

出现该情况的时候,进度会卡住,直到加载完毕,进度值突然从【0%】调到【90%】,

90%代表资源加载完毕,余下10%的时间是用来进行场景显示和资源回收处理的。

1、前面三分之一:初始化...

2、...实际加载...

3、中间三分之一:假装在加载...

4、后面三分之一:加载后资源处理...实际加载的时候,资源调入完毕时进度

(2)代码清单

csharp 复制代码
/// <summary>
/// 场景跳转:异步加载一个场景,显示进度条,进度条滑动动画
/// 
/// **************************************************************************
/// 大场景加载的时候,异步加载的进度值会卡死,因为update主线程卡死了
/// (无论是协程还是用异步,都没有改观,都是卡死状态)。
/// 出现该情况的时候,进度会卡住,直到加载完毕,进度值突然从【0%】调到【90%】,
/// 90%代表资源加载完毕,余下10%的时间是用来进行场景显示和资源回收处理的。
///   1、前面三分之一:初始化.........
///   2、..............实际加载....... 
///   3、中间三分之一:假装在加载.....
///   4、后面三分之一:加载后资源处理...实际加载的时候,资源调入完毕时进度
///   
/// **************************************************************************  
/// 
/// </summary>
/// <param name="onValueChangedAction">加载进度变化时的回调方法</param>
/// <param name="sceneName">要加载的场景名字</param>
/// <param name="totalSliderTime">加载进度动画时间</param>
/// <param name="startAnim">开始加载的动画曲线</param>
/// <param name="midAnim">中间加载的动画曲线</param>
/// <param name="endAnim">加载后处理的动画曲线</param>
/// <returns></returns>
public static async UniTask LoadSceneAsyncWithSliderCurve(Action<float,string> onValueChangedAction,string sceneName,float totalSliderTime,AnimationCurve startAnim,AnimationCurve midAnim,AnimationCurve endAnim,CancellationToken ctk)
{       
    float progress = 0;          //每次循环的累计耗时
    float duration = 0;          //本次循环给定的用时
    float progressCurve = 0;     //曲线百分比进度 

    //第一段:
    Debug.Log("执行第一段");

    progress = 0.0f;
    duration = totalSliderTime / 3f;
    while (true)
    {            
        progress += Time.deltaTime;
        progressCurve = startAnim.Evaluate(progress / duration)/3f;
        onValueChangedAction?.Invoke(progressCurve,"开始加载...");         
        Debug.Log(progressCurve);
        if (progress > duration) break;
        await UniTask.Yield(ctk);
    }

    //第二段:
    //Debug.Log("执行第二段");

    //实际加载
    onValueChangedAction?.Invoke(progressCurve, "卖力加载...");
    await SceneManager.LoadSceneAsync(sceneName);
    
    //完毕后补加动画
    progress = 0.0f;
    duration = totalSliderTime / 3f;
    while (true)
    {
        progress += Time.deltaTime;
        progressCurve = 1f/3f + midAnim.Evaluate(progress / duration) / 3f;
        
        onValueChangedAction?.Invoke(progressCurve, "卖力加载...");          
        Debug.Log(progressCurve);
        if (progress > duration) break;
        await UniTask.Yield(ctk);
    }

    //第三段
    //Debug.Log("执行第三段");   
    progress = 0.0f;
    duration = totalSliderTime / 3f;
    while (true)
    {
        progress += Time.deltaTime;
        progressCurve = 2f/3f + endAnim.Evaluate(progress / duration) / 3f;
        onValueChangedAction?.Invoke(progressCurve, "继续加载...");         
        //Debug.Log(progressCurve);
        if (progress > duration) break;
        await UniTask.Yield(ctk);
    }

    onValueChangedAction?.Invoke(progressCurve, "加载完毕...");
}

六、测试

(1) 脚本挂载:

(2)测试的关键代码

点击按钮,显示进度条,加载中进度变化,加载完毕进度条隐藏

csharp 复制代码
  //按钮逻辑------加载场景
  myButton.onClick.AddListener(async() => 
  {
      //显示进度UI
      mySlider.gameObject.SetActive(true);
      myText.gameObject.SetActive(true);
      await UniTask.DelayFrame(1, cancellationToken: this.GetCancellationTokenOnDestroy());

      //加载的过程
      await LoadSceneAsyncWithSliderCurve(actionOnValueChanged,sceneName, totalSliderTime,startAnim,midAnim,endAnim,this.GetCancellationTokenOnDestroy());

      //加载完毕隐藏进度的UI...
      await UniTask.Delay(500, cancellationToken: this.GetCancellationTokenOnDestroy());
      mySlider.gameObject.SetActive(false);
      myText.gameObject.SetActive(false);
  });

七、Code source附录

本文测试环境Editor,Unity2021.3.40,Win11

csharp 复制代码
using UnityEngine;
using Cysharp.Threading.Tasks;//github-> UniTask包
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;
using System;
using System.Threading;
using UnityEditor;

/// <summary>
/// 场景跳转的进度条效果:
///     1、异步加载场景
///     2、进度条分成三段:初始阶段、加载阶段、回复场景阶段
///     3、各段滑动条的动画用【动画曲线】控制
/// </summary>
public class JumpScene : MonoBehaviour
{
    /// <summary>
    /// 【第一段进度条动画】的动画曲线
    /// </summary>
    [Header("【第一段进度条动画】的动画曲线")]
    public AnimationCurve startAnim;

    /// <summary>
    /// 【第二段进度条动画】的动画曲线
    /// </summary>
    [Header("【第二段进度条动画】的动画曲线")]
    public AnimationCurve midAnim;

    /// <summary>
    /// 【第三段进度条动画】的动画曲线
    /// </summary>
    [Header("【第三段进度条动画】的动画曲线")]
    public AnimationCurve endAnim;

    /// <summary>
    /// 跳转按钮
    /// </summary>
    [Header("跳转按钮")]
    public Button myButton;

    /// <summary>
    /// 要跳转的场景名字
    /// </summary>
    [Header("要跳转的场景名字")]
    public string sceneName;

    /// <summary>
    /// 进度比值显示框
    /// </summary>
    [Header("进度比值显示框")]
    public TMP_Text myText;

    /// <summary>
    /// 进度条
    /// </summary>
    [Header("进度条")]
    public Slider mySlider;

    /// <summary>
    /// 进度条给定的动画时间
    /// </summary>
    [Header("进度条给定的动画时间:三个动画阶段平分时间")]
    public float totalSliderTime = 2f;

    // Start is called before the first frame update
    void Start()
    {
        DontDestroyOnLoad(this);

        //加载进度变化的回调方法,进度值变化的时候调用
        //  value:进度条的值,从0-1
        //  tips:提示信息,加载中,努力加载中...加载完毕
        Action<float,string> actionOnValueChanged = (value,tips) => 
        {
            mySlider.value = value;                                //进度条更新
            myText.text = $"{tips} : {(int)(value * 100)}%";       //匹配的提示信息
            //Debug.Log($"加载进度为:{(int)(value * 100)}%"); 
        };
        
        //按钮逻辑------加载场景
        myButton.onClick.AddListener(async() => 
        {
            //显示进度UI
            mySlider.gameObject.SetActive(true);
            myText.gameObject.SetActive(true);
            await UniTask.DelayFrame(1, cancellationToken: this.GetCancellationTokenOnDestroy());

            //加载的过程
            await LoadSceneAsyncWithSliderCurve(actionOnValueChanged,sceneName, totalSliderTime,startAnim,midAnim,endAnim,this.GetCancellationTokenOnDestroy());

            //加载完毕隐藏进度的UI...
            await UniTask.Delay(500, cancellationToken: this.GetCancellationTokenOnDestroy());
            mySlider.gameObject.SetActive(false);
            myText.gameObject.SetActive(false);
        });
    }

    /// <summary>
    /// 场景跳转:异步加载一个场景,显示进度条,进度条滑动动画
    /// 
    /// **************************************************************************
    /// 大场景加载的时候,异步加载的进度值会卡死,因为update主线程卡死了
    /// (无论是协程还是用异步,都没有改观,都是卡死状态)。
    /// 出现该情况的时候,进度会卡住,直到加载完毕,进度值突然从【0%】调到【90%】,
    /// 90%代表资源加载完毕,余下10%的时间是用来进行场景显示和资源回收处理的。
    ///   1、前面三分之一:初始化.........
    ///   2、..............实际加载....... 
    ///   3、中间三分之一:假装在加载.....
    ///   4、后面三分之一:加载后资源处理...实际加载的时候,资源调入完毕时进度
    ///   
    /// **************************************************************************  
    /// 
    /// </summary>
    /// <param name="onValueChangedAction">加载进度变化时的回调方法</param>
    /// <param name="sceneName">要加载的场景名字</param>
    /// <param name="totalSliderTime">加载进度动画时间</param>
    /// <param name="startAnim">开始加载的动画曲线</param>
    /// <param name="midAnim">中间加载的动画曲线</param>
    /// <param name="endAnim">加载后处理的动画曲线</param>
    /// <returns></returns>
    public static async UniTask LoadSceneAsyncWithSliderCurve(Action<float,string> onValueChangedAction,string sceneName,float totalSliderTime,AnimationCurve startAnim,AnimationCurve midAnim,AnimationCurve endAnim,CancellationToken ctk)
    {       
        float progress = 0;          //每次循环的累计耗时
        float duration = 0;          //本次循环给定的用时
        float progressCurve = 0;     //曲线百分比进度 

        //第一段:
        Debug.Log("执行第一段");

        progress = 0.0f;
        duration = totalSliderTime / 3f;
        while (true)
        {            
            progress += Time.deltaTime;
            progressCurve = startAnim.Evaluate(progress / duration)/3f;
            onValueChangedAction?.Invoke(progressCurve,"开始加载...");         
            Debug.Log(progressCurve);
            if (progress > duration) break;
            await UniTask.Yield(ctk);
        }

        //第二段:
        //Debug.Log("执行第二段");

        //实际加载
        onValueChangedAction?.Invoke(progressCurve, "卖力加载...");
        await SceneManager.LoadSceneAsync(sceneName);
        
        //完毕后补加动画
        progress = 0.0f;
        duration = totalSliderTime / 3f;
        while (true)
        {
            progress += Time.deltaTime;
            progressCurve = 1f/3f + midAnim.Evaluate(progress / duration) / 3f;
            
            onValueChangedAction?.Invoke(progressCurve, "卖力加载...");          
            Debug.Log(progressCurve);
            if (progress > duration) break;
            await UniTask.Yield(ctk);
        }

        //第三段
        //Debug.Log("执行第三段");   
        progress = 0.0f;
        duration = totalSliderTime / 3f;
        while (true)
        {
            progress += Time.deltaTime;
            progressCurve = 2f/3f + endAnim.Evaluate(progress / duration) / 3f;
            onValueChangedAction?.Invoke(progressCurve, "继续加载...");         
            //Debug.Log(progressCurve);
            if (progress > duration) break;
            await UniTask.Yield(ctk);
        }

        onValueChangedAction?.Invoke(progressCurve, "加载完毕...");
    }

    public async UniTask LoadTest1()
    {
        //定义加载任务
        AsyncOperation loadTask = SceneManager.LoadSceneAsync("SceneName");
        loadTask.allowSceneActivation = false;   //加载完毕场景不立即显示

        await loadTask;                          //实际加载过程

        //此处,用户要进行其他操作,比如点击一个按钮,把数据上传到服务器
        //...

        loadTask.allowSceneActivation = true;    //设置成true后,系统开始后续的10%的工作,会把新场景显示,把老场景卸载
    }

    public async UniTask LoadTest2()
    {
        //定义加载任务
        AsyncOperation loadTask = SceneManager.LoadSceneAsync("茶树叶片形态观察", LoadSceneMode.Additive);

        //SceneManager.SetActiveScene(SceneManager.GetSceneByName("新加入的场景名字"));

        //loadTask.allowSceneActivation = false;   //加载完毕场景不立即显示,处于隐藏状态

        await loadTask;                          //实际加载过程

        //await UniTask.Delay(1000);               //等待1秒     
        //loadTask.allowSceneActivation = true;    //把新场景显示出来
    }

    [ContextMenu("Additive加载模式测试")]
    void test2()
    {
        LoadTest2().Forget();
    }

    void test()
    {
        var myScene = SceneManager.GetSceneByName("场景名字");
        SceneManager.SetActiveScene(myScene);
    }

    public async UniTask test3()
    {
        // 定义进度报告器
        var progress = new Progress<float>(value =>
        {     
            //更新滑动条进度
            Debug.Log($"加载进度: {value * 100}%");
        });

        //实际加载
        await SceneManager.LoadSceneAsync("场景2", LoadSceneMode.Single).ToUniTask(progress: progress);      
    }

    public async UniTask test4()
    {
        var progress = 0.0f;         //当前累计耗时
        var duration = 3f;           //给定的动画时间,3秒
        var progressCurve = 0f;      //曲线进度,按照曲线速度运动,而不是匀速运动
        while (true)
        {
            progress += Time.deltaTime;
            progressCurve = startAnim.Evaluate(progress / duration) / duration;            
            Debug.Log(progressCurve);
            if (progress > duration) break;
            await UniTask.Yield();
        }
    }
}
相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭42 分钟前
C#都可以找哪些工作?
开发语言·c#
boligongzhu2 小时前
Dalsa线阵CCD相机使用开发手册
c#
YY-nb3 小时前
Unity Apple Vision Pro 开发教程:物体识别跟踪
unity·游戏引擎·apple vision pro
Cool-浩3 小时前
Unity 开发Apple Vision Pro物体识别追踪ObjectTracking
unity·ar·apple vision pro·mr·物体识别·vision pro教程·objecttracking
向宇it15 小时前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则
sukalot15 小时前
windows C#-命名实参和可选实参(下)
windows·c#
小码编匠15 小时前
.NET 下 RabbitMQ 队列、死信队列、延时队列及小应用
后端·c#·.net
Cool-浩1 天前
Unity 开发Apple Vision Pro空间锚点应用Spatial Anchor
unity·游戏引擎·apple vision pro·空间锚点·spatial anchor·visionpro开发
吴梓穆1 天前
unity 最小后监听键盘输入
unity