我们知道Unity中的协程提供了类似"多段代码并行执行"的功能,在我们定义一个协程函数的时候,需要遵守类似这样的语法:
IEnumerator MyCoroutine(){
//......
yeild return null;
}
不难发现,其中的IEnumerator和yield return部分看起来很是奇怪,和一般的函数不太一样。如果返回值不为 IEnumerator 或者缺少yeild return null 语句都不会称为一个协程;
为了解答这个问题,我们需要了解这个IEnumerator的真面目。
枚举器接口**IEnumerator 接口**
这就是该接口的全部成员
public interface IEnumerator
{
bool MoveNext(); // 尝试移动到下一个元素,返回是否成功
object Current { get; } // 获取当前位置的元素
void Reset(); // 重置到初始位置(-1),很少使用
}
IEnumerator 是迭代协议 ,协程是它的一个创造性应用------Unity 用它来实现"等待-继续"机制,而不是真正的数据遍历。
核心作用:让对象支持 foreach 遍历
任何实现 IEnumerator 的类都可以用 foreach 循环**(协程的本质是枚举器)**:
枚举器的概念:枚举器 通常指用于遍历集合(如数组、列表、字典等)中元素的对象。它提供了一种标准方式来逐个访问容器中的项
// 编译器会将 foreach 翻译成 while 循环调用 MoveNext()
foreach(var item in collection) { ... }
// 等价于:
var enumerator = collection.GetEnumerator();
while(enumerator.MoveNext()) {
var item = enumerator.Current;
// ...
}
该接口与 Unity 协程的关系
Unity 的协程就是基于 IEnumerator 实现的:
// 你的协程代码
IEnumerator MyCoroutine() {
Debug.Log("开始");
yield return new WaitForSeconds(1); // 返回一个 IEnumerator 对象
Debug.Log("1秒后");
}
// Unity 引擎在背后做的事:
var enumerator = MyCoroutine();
while(enumerator.MoveNext()) {
// 检查 Current 返回的 yield 对象
// 如果是 WaitForSeconds,就计时,时间到了再调 MoveNext()
}
yield return的本质:
每次
yield return X时,MoveNext()返回true,Current返回XUnity 根据
Current的类型决定等待条件(WaitForSeconds,WaitForEndOfFrame等)条件满足后,再次调用
MoveNext()继续执行
协程的原理
Unity中的协程,利用了迭代器的机制,实现了"执行一系列操作"而非"遍历一个集合"的功能(相当于把该 IEnumerator 当作一个状态机来逐步执行。)
IEnumerator MyCoroutine()
{
//真正有用的语句
yield return null;
//真正有用的语句
yield return new WaitForSeconds(5);
}
-
每一帧调用MoveNext(),这样可以执行协程直到遇到yield return
-
通过yield return返回的Current值,我们做一系列特定的操作 比如WaitForSeconds就是延迟下一次MoveNext()的执行。
事件
C#中的事件系统中有两个常用的组件:Action 和Func。
可以把它们想象成存储了许多函数的List。当一个Action/Func执行时,其中所有的函数都会一起执行。(这是不是和之前玩家受击的需求很像?)
Action/Func是一个类型,我们可以定义类型为Action/Func的变量并用它来存放函数。
比如:
public Action onPlayerInjure;
void Start(){
onPlayerInjure += ScreenFlash;
}
上方的代码所做的事是:定义了 onPlayerInjure 这个 Action 类型变量,并在游戏开始运行时将 ScreenFlash 这个函数放进 onPlayerInjure 里。
说明:
Action是一个无返回值的委托类型,可以用来存储无参数或带参数的方法。Func是一个有返回值的委托类型,通常用于需要返回值的场景。
Action:常用操作
- Action 位于 namespace System 中(这意味着什么?)
- 它能够存放的函数必须是无返回值的!(也就是 void 函数)
- 有关的常用操作有这些:
- 向一个 Action 中添加一个函数:
+=- 比如:
onDamage += ScreenFlash; - 注意,
+=右侧的值为想要添加的函数名
- 比如:
- 从一个 Action 中删除一个函数:
-=- 比如:
onDamage -= UpdateHPBar;
- 比如:
- 调用一个 Action 中所有的函数:把它当作一个函数来用即可
- 比如:
onDamage();
- 比如:
- 向一个 Action 中添加一个函数:
代码使用案例:
cs
public class L07_Events : MonoBehaviour
{
public Action onPressF;
void Start()
{
onPressF += PrintHello;
onPressF += PrintNiHao;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.F))
{
onPressF();
}
}
void PrintHello()
{
print("Hello");
}
void PrintNiHao()
{
print("Ni Hao");
}
}
和invoke的区别
直接调用
onPressF()和显式调用onPressF?.Invoke()有什么区别?功能上完全等价 ------
onPressF();是onPressF.Invoke();的语法糖(简写形式)。但 安全性和空值处理 上有细微差别,推荐使用
onPressF?.Invoke()。
1. onPressF();
-
这是 C# 提供的便捷语法。
-
编译器会自动将其转换为
onPressF.Invoke();。 -
但如果
onPressF为null,会抛出NullReferenceException!if (Input.GetKeyDown(KeyCode.F))
{
onPressF(); // 如果没人订阅(即 null),程序崩溃!
}
在你的代码中,
Start()里已经添加了两个方法,所以运行时不会为 null。
但如果之后用-=全部移除了,或者某些路径没执行Start(),就可能出错。
2. onPressF?.Invoke();
-
使用 空条件运算符
?.,先检查是否为null。 -
如果
onPressF == null,则 什么也不做,不调用,也不报错。if (Input.GetKeyDown(KeyCode.F))
{
onPressF?.Invoke(); // 安全!即使没人订阅也不会崩溃
}
Action:有参数的函数
- Action 能够存放的函数必须是无返回值的 ,但是可以有参数!
- 每个 Action 类型的变量中的函数的参数必须都是相同的。
- 比如:
void A(int a)void B(float b)void C(float c)
- 则 B 和 C 可以放在同一个 Action 里,而 A 不行(因为参数类型不同)。
- 比如:
正确使用有参函数Action:
- 定义一个有参数的 Action:
Action<参数1类型, 参数2类型, ......>- 比如:
private Action<float> onDamage; - 这代表
onDamage中的函数都是接受一个 float 类型的参数,且无返回值的函数。
- 比如:
- 这时,使用它的时候就可以传入一个参数了:
onDamage(45.2f);
Func:有返回值的函数
- Func 和 Action 是同一"家族"的,而它们唯一的区别就是有无返回值。
- 在定义 Func 的时候,需要在所有参数之后加上返回值的类型:
cs
private Func<int, float> func1; // func1 中的函数接受 1 个 int 类型的变量,并返回 float
private Func<string> func2; // func2 中的函数不接受变量,返回 string
private Func<int, int, int> func3;// func3 中的函数接受 2 个 int 类型的变量,并返回 int
private Func func4; // func4 会报错,为什么?
但是一般Action用的多,而不怎么用Func类型
为什么需要这两个函数
- "一个存放函数的变量" 。我们可以在 Action/Func 中存放函数并动态修改其中的函数。这在许多时候都会带来很大的便利。
- 比如:一个四则运算程序中有加减乘除四个功能。使用 Action/Func 的话,我们只需调用一个 Action/Func 并动态修改其中真正的函数是加减乘除中的哪一个
- 促进代码的模块化。我们可以在多个其他位置访问一个事件并向其中添加函数。相反地,如果我们把事件写成一个(调用很多其他位置函数的)函数的话会很乱。
- "只在需要时做一件事"。使用 Action/Func 后,我们能确保与其相关的函数只在其出发时执行一次,这样避免了多余的浪费