Unity协程计时器详解:理解 ResetComboAfterTimeout 的工作原理

在我的代码中,协程计时器(主要是 ResetComboAfterTimeout)用于管理攻击连招的超时重置机制。本文将详细解析这部分代码的工作原理、目的和实现方式。

协程基础

首先,让我们理解一下 Unity 协程的基本概念:

  • 协程:一种可以暂停执行并在之后恢复的函数。
  • yield 语句:用于暂停和恢复协程。
  • 协程可以在不阻塞主线程的情况下等待特定的时间或条件。
  • 协程可以通过 StartCoroutine 启动,通过 StopCoroutine 停止。

代码中的协程计时器实现

1. 主要变量

1
2
private float _comboResetTime = 2f; // 超时重置时间
private Coroutine _resetCoroutine; // 用于存储计时器协程的引用

2. 协程启动时机

在您的代码中,协程在攻击冷却结束后启动:

1
2
3
4
5
6
7
8
9
10
11
TimeManger.MainInstance.TryGetOneTimer(_MaxColdTime, () =>
{
_CanInputAttack = true;
// 冷却结束后启动超时重置计时器
if (_resetCoroutine != null)
{
StopCoroutine(_resetCoroutine);
}

_resetCoroutine = StartCoroutine(ResetComboAfterTimeout());
});

3. 协程的实现

1
2
3
4
5
6
7
private IEnumerator ResetComboAfterTimeout()
{
yield return new WaitForSeconds(_comboResetTime);
CurrentComboIndex = 0; // 超时后重置连击索引

_resetCoroutine = null; // 清空引用
}

4. 协程的取消时机

当玩家继续输入攻击时,协程会被取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (CanCharaBaseAttack() && GameInputManger.Instance.LAttack)
{
// ...
if (_CanInputAttack)
{
if (_resetCoroutine != null)
{
StopCoroutine(_resetCoroutine);
_resetCoroutine = null;
}
ExecuteComboAttack();
CurrentComboIndex++;
}
}

工作流程详解

让我们完整梳理一下连击系统和协程计时器的工作流程:

1. 玩家发起第一次攻击

当玩家按下攻击按钮,触发 ExecuteComboAttack()

  • 播放攻击动画
  • 设置冷却时间
  • 禁用攻击输入(_CanInputAttack = false
  • 通过 TimeManger 启动冷却计时器

2. 攻击冷却结束

当攻击冷却结束,TimeManger 回调执行:

  • 重新启用攻击输入(_CanInputAttack = true
  • 启动协程计时器 ResetComboAfterTimeout()(关键步骤)

3. 玩家有两种选择

选择 A:在超时前继续攻击

1
玩家攻击 → 冷却结束 → 启动协程 → [玩家迅速输入] → 取消协程 → 执行下一招 → ...

当玩家在 _comboResetTime(2秒)内继续攻击:

  • 检测到 GameInputManger.Instance.LAttacktrue
  • 取消当前的协程计时器(StopCoroutine(_resetCoroutine)
  • 执行下一个连击动作
  • 增加 CurrentComboIndex
  • 重新开始冷却 → 协程循环

选择 B:超时不攻击

1
玩家攻击 → 冷却结束 → 启动协程 → [玩家没有输入] → 协程完成等待 → 重置连击索引

当玩家在 _comboResetTime(2秒)内没有继续攻击:

  • 协程完成等待 WaitForSeconds(_comboResetTime)
  • 执行 CurrentComboIndex = 0,重置连击索引
  • 玩家再次攻击时将从第一招开始

图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌─────────┐   按攻击键    ┌───────────────┐
│ 初始状态 ├──────────────→│ 执行连击动作 │
└─────────┘ └───────┬───────┘


┌───────────────┐
│ 攻击冷却中 │ ← _CanInputAttack = false
└───────┬───────┘

↓ 冷却结束
┌───────────────┐
│ 启用输入+启动 │ ← _CanInputAttack = true
│ 协程计时器 │ ← _resetCoroutine = StartCoroutine(...)
└───────┬───────┘

┌───────────────────┐ │ ┌───────────────────┐
│ │ │ │ │
↓ │ │ ↓ │
┌───────────────────┐ │ │ ┌───────────────────┐
│ 玩家在2秒内输入攻击 │ │ │ │ 玩家超过2秒未输入 │
└─────────┬─────────┘ │ │ └─────────┬─────────┘
│ │ │ │
↓ │ │ ↓
┌───────────────────┐ │ │ ┌───────────────────┐
│ 取消协程计时器 │◄─────────┘ │ │ 协程完成等待 │
└─────────┬─────────┘ │ └─────────┬─────────┘
│ │ │
↓ │ ↓
┌───────────────────┐ │ ┌───────────────────┐
│ 继续下一招连击 │ │ │ 重置连击索引 │
│ CurrentComboIndex++│ │ │ CurrentComboIndex=0│
└─────────┬─────────┘ │ └─────────┬─────────┘
│ │ │
└──────────────────────┘ │
↑ │
└──────────────────────────┘

代码分析的关键部分

1. 协程的存储与引用

1
private Coroutine _resetCoroutine;

该变量存储了对正在运行的协程的引用,使得您可以:

  • 检查协程是否正在运行(_resetCoroutine != null
  • 在需要时停止协程(StopCoroutine(_resetCoroutine)

2. 协程的启动

1
_resetCoroutine = StartCoroutine(ResetComboAfterTimeout());

StartCoroutine 方法返回一个 Coroutine 对象,它被存储在 _resetCoroutine 中。

3. 协程的取消

1
2
3
4
5
if (_resetCoroutine != null)
{
StopCoroutine(_resetCoroutine);
_resetCoroutine = null;
}

当需要取消协程时,使用 StopCoroutine 并将引用设为 null

4. 协程完成后的自清理

1
2
3
4
5
6
7
private IEnumerator ResetComboAfterTimeout()
{
yield return new WaitForSeconds(_comboResetTime);
CurrentComboIndex = 0;

_resetCoroutine = null; // 自清理引用
}

协程完成任务后,会自清理自己的引用,表示协程已经结束。

这种设计的好处

  1. 不阻塞游戏主线程:使用协程而不是传统的计时器可以避免阻塞主线程,保持游戏流畅性。
  2. 易于取消:可以根据玩家的输入立即取消计时器,灵活控制连击。
  3. 代码清晰:使用协程处理延时任务比回调函数更加直观。
  4. 内存效率:协程结束后会被垃圾回收,避免内存泄漏。

改进建议

虽然实现已经很好,但可以考虑以下改进:

1. 添加更多状态检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private IEnumerator ResetComboAfterTimeout()
{
yield return new WaitForSeconds(_comboResetTime);

// 确保协程不是在无效或过渡状态被调用
if (_animator.AnimationAtTag("Attack") || _animator.IsInTransition(0))
{
_resetCoroutine = null;
yield break;
}

CurrentComboIndex = 0;
_resetCoroutine = null;
}

2. 考虑使用可取消令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private CancellationTokenSource _resetTokenSource;

private void StartResetTimer()
{
CancelResetTimer();
_resetTokenSource = new CancellationTokenSource();
StartCoroutine(ResetComboAfterTimeout(_resetTokenSource.Token));
}

private void CancelResetTimer()
{
if (_resetTokenSource != null)
{
_resetTokenSource.Cancel();
_resetTokenSource.Dispose();
_resetTokenSource = null;
}
}

private IEnumerator ResetComboAfterTimeout(CancellationToken token)
{
float elapsedTime = 0;
while (elapsedTime < _comboResetTime)
{
if (token.IsCancellationRequested)
yield break;

elapsedTime += Time.deltaTime;
yield return null;
}

CurrentComboIndex = 0;
}

总结

在您的连击系统中,协程计时器用于实现以下功能:

  1. 连击超时重置:当玩家在指定时间内(_comboResetTime)没有继续攻击,系统会自动重置连击索引。
  2. 灵活的取消机制:当玩家继续输入攻击时,计时器会被取消,允许连击继续。
  3. 清晰的状态管理:无论计时器是被取消还是自然完成,都会清理自己的引用,避免内存泄漏。

这种机制是动作游戏中实现连击系统的常用方法,它允许玩家在一定时间窗口内继续连击,但不会无限期地保持连击状态,符合大多数动作游戏的设计习惯。

协程计时器是 Unity 中处理这类“延迟执行且可能需要取消”的任务的优雅解决方案。