WaitForSeconds在暂停时的表现
当在协程中使用WaitForSeconds(2)
指令等待2秒,其行为与Unity的时间缩放系统(Time.timeScale
)直接相关。
基本行为
WaitForSeconds
依赖于Time.timeScale
来计算实际等待时间- 当
Time.timeScale = 0
(游戏暂停)时,WaitForSeconds
会无限期等待 - 这是因为
WaitForSeconds
实际上是等待的游戏时间而非真实时间
行为示例
IEnumerator ExampleCoroutine()
{
Debug.Log("协程开始");
yield return new WaitForSeconds(2); // 应等待2秒
Debug.Log("协程结束"); // 如果Time.timeScale=0,这一行永远不会执行
}
void PauseGame()
{
Time.timeScale = 0; // 暂停游戏
}
当Time.timeScale = 0
时,上面的协程会永远停在WaitForSeconds(2)
处,因为游戏时间不再流逝,2秒的等待永远不会结束。
让协程在暂停时完全冻结的方法
有几种方法可以确保协程在游戏暂停时也完全冻结:
1. 使用WaitForSecondsRealtime
WaitForSecondsRealtime
使用真实时间而非游戏时间,不受Time.timeScale
影响:
IEnumerator ExampleCoroutine()
{
Debug.Log("协程开始");
if (Time.timeScale > 0)
{
// 游戏运行时使用游戏时间
yield return new WaitForSeconds(2);
}
else
{
// 游戏暂停时立即继续而不等待
yield return null;
}
Debug.Log("协程结束");
}
2. 自定义暂停感知的协程系统
创建一个可以检测游戏暂停状态的协程管理系统:
public class PauseAwareCoroutineManager : MonoBehaviour
{
private static PauseAwareCoroutineManager _instance;
public static PauseAwareCoroutineManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("PauseAwareCoroutineManager");
_instance = go.AddComponent<PauseAwareCoroutineManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
private bool isPaused = false;
private List<(Coroutine coroutine, MonoBehaviour owner)> activeCoroutines =
new List<(Coroutine, MonoBehaviour)>();
// 启动可暂停的等待协程
public Coroutine WaitForSecondsWithPause(MonoBehaviour owner, float seconds, Action onComplete)
{
Coroutine coroutine = StartCoroutine(WaitForSecondsCoroutine(owner, seconds, onComplete));
activeCoroutines.Add((coroutine, owner));
return coroutine;
}
private IEnumerator WaitForSecondsCoroutine(MonoBehaviour owner, float seconds, Action onComplete)
{
float timer = 0;
while (timer < seconds)
{
if (!isPaused)
{
timer += Time.deltaTime;
}
yield return null;
}
onComplete?.Invoke();
activeCoroutines.RemoveAll(x => x.coroutine == null || x.owner == null);
}
// 暂停所有协程
public void PauseAllCoroutines()
{
isPaused = true;
}
// 恢复所有协程
public void ResumeAllCoroutines()
{
isPaused = false;
}
}
使用方法:
// 替代WaitForSeconds
PauseAwareCoroutineManager.Instance.WaitForSecondsWithPause(this, 2f, () => {
Debug.Log("2秒后执行,即使游戏暂停也会正确冻结和恢复");
});
// 暂停游戏时
void PauseGame()
{
Time.timeScale = 0;
PauseAwareCoroutineManager.Instance.PauseAllCoroutines();
}
// 恢复游戏时
void ResumeGame()
{
Time.timeScale = 1;
PauseAwareCoroutineManager.Instance.ResumeAllCoroutines();
}
3. 使用自定义的YieldInstruction
创建一个自定义的PauseAwareWaitForSeconds
类:
public class PauseAwareWaitForSeconds : CustomYieldInstruction
{
private float targetTime;
private GamePauseManager pauseManager;
public PauseAwareWaitForSeconds(float seconds, GamePauseManager pauseManager)
{
this.targetTime = Time.time + seconds;
this.pauseManager = pauseManager;
}
public override bool keepWaiting
{
get
{
if (pauseManager.IsPaused)
{
// 游戏暂停时,更新目标时间以"冻结"等待
targetTime = Time.time + (targetTime - Time.time);
return true;
}
return Time.time < targetTime;
}
}
}
// 使用方法
IEnumerator WaitWithPause(float seconds)
{
yield return new PauseAwareWaitForSeconds(seconds, gamePauseManager);
Debug.Log("等待完成,正确处理了暂停");
}
4. 协程暂停标记法
在全局创建一个暂停标记,在协程中检查该标记:
public static class GameManager
{
public static bool IsGamePaused { get; private set; }
public static void PauseGame()
{
Time.timeScale = 0;
IsGamePaused = true;
}
public static void ResumeGame()
{
Time.timeScale = 1;
IsGamePaused = false;
}
}
// 在协程中使用
IEnumerator DelayedAction(float seconds)
{
float timer = 0;
while (timer < seconds)
{
// 只有在游戏未暂停时才增加计时器
if (!GameManager.IsGamePaused)
{
timer += Time.deltaTime;
}
yield return null;
}
Debug.Log("计时完成,正确处理了暂停");
}
最佳实践总结
- 避免直接使用WaitForSeconds:在可能暂停的游戏中,不要直接使用WaitForSeconds,除非你希望它在暂停时无限等待
- 使用暂停感知的计时器:自己实现计时逻辑,在暂停时不增加计时器
- 分离游戏时间和真实时间:对于需要在暂停时仍然运行的协程(如UI动画),使用WaitForSecondsRealtime
- 创建集中的协程管理系统:实现一个中央协程管理器,统一处理暂停和恢复
- 使用标志变量:在全局维护一个暂停标志,所有协程检查这个标志
通过这些方法,你可以确保协程在游戏暂停时正确冻结,并在游戏恢复时继续执行,从而实现更加可靠的游戏时间系统。