一、异步加载场景的过程
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();
}
}
}