专栏/【Unity】中高级协程的强力扩展Moroutines

【Unity】中高级协程的强力扩展Moroutines

2024年10月24日 13:03--浏览 · --点赞 · --评论
粉丝:282文章:54

Unity商店有一个免费的携程扩展Moroutines(超协),弥补了内置携程的一些弱点.

侦测协程状态的面板

以下是内置协程的短板:

  • 协程不存储关于其状态的信息(重置、运行、停止、完成或销毁)。

  • 协程无法暂停/重置,然后继续执行。

  • 没有能力等待(yield)暂停或播放协程。

  • 协程不知道如何返回结果。

  • 协程不能被分组以便于控制。

  • 协程不存储关于其所有者对象的信息。

  • 游戏对象不显示与它们关联的协程信息。

  • 没有办法创建一个无所有者的协程。

  • 没有办法等待(yield)多个协程的执行。

  • 协程没有状态变更事件。

  • 协程B和C不能同时等待(yield)协程A。

  • 没有办法在启动现有协程之前添加延迟(以秒或帧为单位)。

  • 协程没有名称,这使得它们更难过滤和调试。

  • 以及其他缺点。

为什么选择 Moroutines(超协)?

Unity 已经有一个用于处理协程的 Coroutine 类。在我们的库中,协程被称为 moroutines(Moroutine - 比协程更多),并由 Moroutine 类表示。允许轻松地可同时使用纯协程和更高级的 moroutines(超协)。

下面把Moroutines简称为超协,超越协程的意思.

代码部分太多,排版可能不太好,可以复制到本地MD文件中.


使用

命名空间:using Redcode.Moroutines;

创建一个超协---Create


运行一个超协---Run

注意,超协和协程不同,创建后并不会自动运行.

停止超协---Stop

继续超协---Run

超协的结束

传递给moroutine的方法(TickEnumerator())内部有一个无限循环(循环是While语句造成的.)。

因此,这样的moroutine将永远不会结束。

否则,moroutine迟早会结束。例如下面的写法:


DelayEnumerator(float delay) 方法是终结的。这个方法生成一个代表该方法执行的 IEnumerator 对象。在 C# 中,IEnumerator 对象不能被重新启动。因此,当一个传递了 IEnumerator 对象的moroutine执行完成后,它会自动被标记为已销毁,这意味着您不能再次运行它。如果您的代码不包含对已销毁moroutine的引用,垃圾回收器会在一段时间后自动删除它。

然而,在方法声明中,您可以用 IEnumerable 替换 IEnumerator,在这种情况下,moroutine可以被重新启动,使其从最开始再次开始执行。正是因为这个原因,这样的moroutine不会自动被标记为已销毁。

总结:

超协在使用方法声明的时候.

  • IEnumerator执行一次就会终结.会被自动标记为已销毁.不能再次运行它.即使再做超协的重启啥的也没用.实际上不会做任何事了.

  • IEnumerable则可以被重新启动.---也就是,下面讲的Return和Reset之类的超协的方法.各种重启,重复的方法.

自动销毁超协的写法.

也可以设置自动运行后销毁使用.SetAutoDestroy或AutoDestroy

在示例中,moroutine使用了一个IEnumerable对象,因此不会自动销毁,但是通过使用SetAutoDestroy方法,我们指明了在完成后应该销毁它。同样地,你可以覆盖使用IEnumerator对象创建的moroutine的自动销毁,但这并没有太大意义,因为一旦完成,这样的moroutine即使被反复运行,也不会做任何事情

手动销毁超协

手动销毁moroutine可以使用Destroy方法来销毁moroutine:

✅重要提示!如果您的游戏中不再使用moroutine超协,那么必须销毁它(无论是自动销毁还是使用Destroy方法手动销毁),否则内存将不会被释放。此外,在销毁后,不要忘记“丢失”对moroutine的所有引用。

如果没有重新执行moroutine的需求,请使用IEnumerator方法。这将为您省去不必要的自动销毁设置。否则,您必须记得销毁moroutine。

重启超协

您可以重启moroutine(从最开始开始执行它),为此,请使用Reset方法。


注意,调用Reset方法会重置moroutine的状态并停止它。这意味着您必须自己负责其后续的启动。RunStopReset方法会返回它们所属的moroutine,这允许您将多个方法调用链接在一起,从而缩短代码。

mor.Reset().Run();

或者更更短:

mor.Rerun();

在执行了一个超协之后,您也可以调用它的Rerun方法来重新启动它,但是,您可以使用Run方法,它有一个rerunIfCompleted参数,默认为true,负责在moroutine已经完成时自动重新启动它。

超协的状态.

Moroutine状态

可以使用以下属性来检查moroutine的状态:

  • IsReseted - moroutine是否已重置到其初始状态?

  • IsRunning - moroutine是否正在运行?

  • IsStopped - moroutine是否已停止?

  • IsCompleted - moroutine是否已完成?

  • IsDestroyed - moroutine是否已销毁?

  • CurrentState - 返回一个枚举,代表上述状态之一。

前四个属性返回一个布尔值,代表相应的状态。例如:

var mor = Moroutine.Run(CountEnumerable());

print(mor.IsRunning);//会返回一个bool值.true

订阅事件和方法

Moroutines有以下事件:

  • Reseted - 当moroutine被重置到其初始状态时触发。

  • Running - 在调用Run方法后立即触发。

  • Stopped - 仅当moroutine停止时触发。

  • Completed - 当moroutine完成时触发。

  • Destroyed - 当moroutine被销毁时触发。

您可以在需要时订阅这些事件中的任何一个。订阅者方法必须符合以下签名:

moroutine参数将被设置为引发该事件的moroutine。


快速订阅事件.

您还可以使用以下方法快速订阅所需的事件:

  • OnReseted - 订阅重置事件。

  • OnRunning - 订阅启动事件。

  • OnStopped - 订阅停止事件。

  • OnCompleted - 订阅完成事件。

  • OnDestroyed - 订阅销毁事件。

var mor = Moroutine.Run(CountEnumerable()); 

mor.OnCompleted(c => print("Completed"));---快速订阅.后面是表达式.

几乎所有的超协方法都会返回它们被调用的moroutine,因此您可以形成如下这样的长链调用:

Moroutine.Create(CountEnumerable()).OnCompleted(c => print("Completed")).Run();

超协中的等待事件.---通常是多个超协互相等待时候用.

A,等待B和C完成,这种.

如果您需要等待(yield)moroutine的特定状态,请使用以下方法:

  • WaitForComplete - 返回一个对象以等待完成。

  • WaitForStop - 返回一个对象以等待停止。

  • WaitForRun - 返回一个对象以等待运行。

  • WaitForReset - 返回一个对象以等待重置。

  • WaitForDestroy - 返回一个对象以等待销毁。

如下例子:


或下面的

yield return Moroutine.Run(CountEnumerable()).WaitForComplete();

---超协B中写了A的运行.与等待 print("Awaited");

内置的协程引擎中,您不能同时让协程B和协程C等待协程A完成,否则您将在控制台窗口中收到一个警告:


正如您所看到的,这是真的,但是使用超协时就没有这样的问题,您可以创建任意多的moroutines,它们将等待任何其他moroutines


使用多个超协一起工作.

可以一次创建多个超协.


执行后显示:mor1.Tick!---mor2.Tick!---mor2.Tick!

等待多个超协执行完毕.

您还可以使用WaitForAll类对象同时等待多个moroutines。

WaitForAll类构造函数有以下重载:

WaitForAll(params Moroutine[]);

WaitForAll(IEnumerator[]); 

WaitForAll(IEnumerable<IEnumerator>);

等待至少一个多个协程完成

使用 WaitForAny 类,您可以等待指定的列表中至少一个协程被执行。

WaitForAny 类构造函数有以下重载:

WaitForAny(params Moroutine[]); 

WaitForAny(IEnumerator[]); 

WaitForAny(IEnumerable<IEnumerator>);

协程结果

协程可以存储执行的结果。为此有一个 LastResult 属性。这个属性存储了最后一次执行的 yield return 语句返回的对象。

有时候这可能会非常方便!

孤立协程

到目前为止,我们一直在学习如何创建孤立协程。没有所有者的协程是指没有附加到任何游戏对象的协程。这类协程的执行只能通过使用 StopReset 或 Destroy 方法来中断。孤儿协程即使在您加载游戏中的新场景时也会继续存在并执行,因此您需要更加关注它们。在底层,未归属的协程是在位于 DontDestroyOnLoad 场景中的游戏对象上启动的

协程及其所有者

您可以将协程与任何游戏对象关联,即将其设为协程的所有者。这意味着只有当所有者对象处于活动状态时,协程的执行才是可能的,否则协程将会被停止,并且您无法重新启动它或继续执行,直到所有者对象变为活动状态。尝试在非活动状态的所有者对象上运行协程将会抛出异常。如果所有者对象再次变为活动状态,那么您可以使用 Run 方法继续执行协程。

要指定协程的所有者,请在 Moroutine.Create 或 Moroutine.Run 方法中将它作为第一个参数进行指定。

var mor = Moroutine.Run(gameObject, CountEnumerable()); 

您可以传递任何其组件来代替所有者对象。结果将会是一样的

var mor = Moroutine.Run(this, CountEnumerable()); 

您还可以使用 SetOwner 和 MakeUnowned 方法来设置不同的所有者或使协程变为无所有者状态。

使用 this 而不是 gameObject 因为它更短。

您还可以使用 mor.SetOwner((GameObject)null) 来使一个协程变为孤儿。

当在协程执行过程中调用 SetOwner 时,其所有者将静默地更改为指定的对象。

MoroutinesOwner组件

每当一个游戏对象被指定为协程的所有者时,会自动为其添加一个 MoroutinesOwner 组件。这个组件始终维护着它所拥有的协程的最新列表,并且还负责在游戏对象被禁用或删除时停用这些协程。

“显示协程”按钮显示所有当前协程的详细信息(它们在列表中的索引、名称、状态和最后结果):

如果您需要获取一个协程的所有者,请使用其 Owner 属性。

var mor = Moroutine.Run(gameObject, CountEnumerable()); print(mor.Owner.name);

Owner 是指向协程所有者的 MoroutinesOwner 组件的引用。没有所有者的协程的 Owner 等于 null。如果您需要知道一个协程是否有所有者,请使用布尔属性 IsOwned

MoroutinesOwner 组件将一直存在,只要它至少有一个实际的协程。被销毁的协程与此无关。

协程名称

您可能已经注意到,MoroutinesOwner 组件在协程列表的“名称”列中显示“[noname]”。这意味着您没有给这些协程指定特殊的名称。大多数时候您的协程将保持未命名状态,但有时为了调试目的,给其中一个协程命名是很方便的。为此,请使用协程的 SetName 方法或 Name 属性:

Moroutine.Run(_owner, TurningEnumerator()).SetName("Turning"); Moroutine.Run(_owner, MovingEnumerator(2f)).Name = "Moving"; Moroutine.Create(_owner, JumpingEnumerator()).SetName("Jumping");

获取所有者的所有协程

给定一个 MoroutinesOwner 实例,您可以使用 Moroutines 属性来获取其所有实际协程的列表:

在上面的例子中,创建了3个具有相同所有者的协程。使用 Owner 属性,我们得到了所有者,然后是它的所有协程。Moroutines 属性返回一个新的 ReadOnlyCollection 实例,因此您无法更改原始集合。

获取游戏对象所有协程的另一种方法是使用 GetMoroutines() 扩展方法。不要忘记包含 Redcode.Moroutines.Extensions 命名空间:

您还可以使用状态掩码来过滤掉协程。

超协执行器---MoroutinesExecuter物体

在您的游戏开始之前,场景中将会创建一个 MoroutinesExecuter 对象,它将被隔离并隐藏在 DontDestroyOnLoad 场景中,因此您不会注意到它。您也无法从代码中访问这个类。这个对象是所有无主协程的所有者。

获取所有孤立的(无所有者)超协

使用 Moroutine.GetUnownedMoroutines 静态方法来获取无所有者的协程。您还可以使用状态掩码。

var mors = Moroutine.GetUnownedMoroutines(Moroutine.State.Running); 

超协设置

设置窗口位于菜单 窗口 > 协程 > 设置,包含以下项目:

  • 隐藏协程执行器 - 是否需要在编辑器中隐藏所有孤立协程的所有者?

设置页面

使用 MoroutinesGroup 对超协进行分组

有时将协程分组到一个对象中并通过它来管理所有协程是方便的。为此,请使用 MoroutinesGroup 类。

您还可以将协程列表传递给构造函数:

此外,您可以引入 Redcode.Moroutines.Extensions 命名空间,并在一组协程上使用 ToMoroutinesGroup() 方法来从这组协程创建一个分组:

超协分组管理

使用 RunStopResetRerun 和 Destroy 方法来控制整个组。对这些方法的调用基本上就是对组中的每个协程调用这些方法:

更改组的组成

组成该组的协程列表由 Moroutines 属性表示。这是一个常规列表,您可以像处理任何其他列表一样与之交互:

group.Moroutines.Add(mor7);

group.Moroutines.RemoteAt(4);

为组中的所有协程设置所有者

要设置组中所有协程的所有者,请使用 SetOwner 或 MakeUnowned 方法。您还可以使用 Owner 属性来获取所有协程的所有者,但是如果有至少一个协程的所有者不同,那么这个属性将返回 null

所有协程的状态

您可以使用以下任何属性来确定组中的所有协程是否匹配此状态:

  • IsReseted - 所有协程都已重置。

  • IsRunning - 所有协程都在运行。

  • IsStopped - 所有协程都已停止。

  • IsCompleted - 所有协程都已完成。

  • IsDestroyed - 所有协程都被标记为已销毁。

  • IsOwned - 所有协程都有所有者。

所有协程的销毁和设置自动销毁

使用 AutoDestroy 属性或 SetAutoDestroy 方法来读取/设置整个组的自动销毁行为。如果您想要彻底销毁所有协程,请使用 Destroy 方法。

组事件

当您调用组管理方法(RunStop 等其他方法)时,将在组中触发相应的事件。以下事件被支持:

  • Reseted - 组中的所有协程都已重置。

  • Running - 组中的所有协程都在运行。

  • Stopped - 组中的所有协程都已停止。

  • Destroyed - 组中的所有协程都已销毁。

您可以使用 OnResetedOnRunningOnStopped 和 OnDestroyed 方法快速订阅这些事件,分别如下:


在组中搜索孤立的协程

使用 GetUnownedMoroutines 方法(您可以指定一个搜索掩码)来获取组中无所有者的协程列表。

等待组事件

您可以等待您想要的事件。为此,请使用以下方法之一:

  • WaitForComplete - 等待直到组中的所有协程完成。

  • WaitForStop - 等待直到组中的所有协程停止。

  • WaitForRun - 等待直到组中的所有协程开始运行。

  • WaitForReset - 等待直到组中的所有协程重置。

  • WaitForDestroy - 等待直到组中的所有协程被标记为销毁。

在下面的示例中,让我们启动一个协程,它将等待直到组开始执行:


正如您所看到的,WaitEnumerator 协程等待直到组开始执行。

辅助类 Routines

静态类 Routines 存储了最常用于组织协程执行逻辑的方法。所有方法都会生成并返回一个 IEnumerable 对象,该对象可以用于替换到其他方法中。特别是,以下方法可用:

  • Delay - 在方法执行之前添加时间延迟。

  • FrameDelay - 在方法执行之前添加帧延迟。

  • Repeat - 指定次数重复执行方法。

  • Wait - 等待 YieldInstruction 和 CustomYieldInstruction 对象的执行。

使用 Delay 的示例:


这个示例使用了 Delay 方法,它在执行 CountEnumerator 枚举器之前添加了一秒的延迟,这是通过 Routines.Delay(1f, CountEnumerable()) 这行代码实现的。如上所述,Routines 类的所有方法都返回一个 IEnumerable 对象,因此,为了将从 Delay 和 CountEnumerable 方法合并的结果变成一个协程,您需要将其代入 Moroutine.Run 方法中。

Delay 方法也可以与 Action 方法一起使用,这实际上为您提供了快速组织所需方法延迟执行的能力,例如:

private void Start() => 

Moroutine.Run(Routines.Delay(1f, () => print("Delayed print!")));

或者

正如您所看到的,这种方式非常方便,并且能够缩短代码。

这些方法可以用于 IEnumerable 和 IEnumerator 对象,但是,如果您打算重新启动枚举器,您必须使用 IEnumerable 对象。

FrameDelay 方法在执行枚举器之前添加一个帧延迟。例如,如果您需要等待 1 个游戏帧,然后执行枚举器代码,它将看起来像这样:

private void Start() => 

Moroutine.Run(Routines.FrameDelay(1, () => print("1 frame skipped!")));

这个方法,以及 Delay 方法,都可以与 Action 方法一起工作。

Repeat 方法重复指定枚举器指定的次数。如果您需要无限次重复执行枚举器,那么可以将 Repeat 方法的 count 参数指定为 -1。例如:

因此,文本 “Hello!” 将每秒在控制台中显示 3 次。

您可以将 DelayFrameDelay 和 Repeat 方法相互组合使用,例如,如果您需要以1秒的延迟执行某个函数3次,那么它将如下所示:

方法之间的这种嵌套使用可以是无限的。

Wait 方法允许您快速将一个 YieldInstruction 或 CustomYieldInstruction 对象包装在一个 IEnumerable 中,这个 IEnumerable 将简单地等待它们被执行。例如,如果您想要将一个 YieldInstruction 对象包装在一个协程中,以便稍后通过这个协程跟踪 YieldInstruction 的执行状态,您可以编写如下代码:

var moroutine = Moroutine.Run(Routines.Wait(instruction));

其中 instruction 是 YieldInstruction 类的一个对象。

最后---超协的一些扩展

看到这里的,一定都是大佬了!

Redcode.Moroutines.Extensions 命名空间包含了 YieldInstruction 和 CustomYieldInstruction 类的扩展方法。这些方法允许您快速在 MoroutineYieldInstruction 和 CustomYieldInstruction 之间进行转换。例如:

您可能很少需要这样的转换,但存在这种可能性。

现在就这些了,您现在已经准备好使用 超协了!


投诉或建议