
您已经很酷了,可以同时绕所有轴旋转协同程序,一眼便看到它们执行yield break
并隐藏在IDE画布后面。 简单的包装器是一个漫长的阶段。
您知道如何做得好,如果拥有自己的餐厅,您可以得到一颗米其林星(甚至两颗)。 当然可以! 没有人会喜欢用协程调味酱品尝法式海鲜汤。
整整一个星期,产品中的代码都不会丢失! 包装, callback
和Start/Stop Coroutine
是很多从属方法。 您需要更多的控制权和行动自由。 您已准备好迈出下一步(当然,不要退出协程)。
如果您在这些方面认出自己,欢迎来关注。
我借此机会向我最喜欢的模式之一-Command打招呼。
引言
对于Unity提供的所有yield
指令( Coroutine
, AsyncOperation
, WaiForSeconds
等),基类是不起眼的YieldInstruction
( docs )类:
[StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { }
在内部,协程是普通的枚举器( IEnumerator
( docs ))。 每个帧都由MoveNext()
方法MoveNext()
。 如果返回值为true
,则执行将延迟到下一个yield
语句;如果为false
,则它将停止。
对于自定义的yield
说明,Unity提供了相同名称的CustomYieldInstruction
( docs )类。
public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); }
从它继承并实现keepWaiting属性就足够了。 上面我们已经讨论了其逻辑。 只要要执行协程,它就应该返回true
。
文档中的示例:
using UnityEngine; public class WaitForMouseDown : CustomYieldInstruction { public override bool keepWaiting { get { return !Input.GetMouseButtonDown(1); } } public WaitForMouseDown() { Debug.Log("Waiting for Mouse right button down"); } }
据她介绍, keepWating
getter在Update()
和LateUpdate()
之前的每一帧执行。
如果您使用的代码在某些时候应该可以执行某些操作,但是仍然没有研究此页面,那么我强烈建议您选择此功能。
自定义产量说明

但是我们没有像CustomYieldInstruction
这样的常规包装器CustomYieldInstruction
继承。 为此,您可以没有米其林。
因此,我们进一步抽查了文档,并且几乎在同一页面的底部找到了最重要的段落。
要拥有更多控制权并实施更复杂的收益指令,您可以直接从 System.Collections.IEnumerator
类 继承 。 在这种情况下, 以与实现 keepWaiting
属性 相同的方式来 实现 MoveNext()
方法 。 除此之外,您还可以在Current
属性中返回一个对象,该对象将在执行MoveNext MoveNext()
方法 后由Unity的协程调度程序处理 。 因此,例如,如果 Current
返回了另一个继承自 IEnumerator
对象 ,则当前枚举器将被挂起,直到返回的对象完成为止。
俄语为了获得更多控制权并实施更复杂的 yield
指令,您可以直接从 System.Collections.IEnumerator
接口继承 。 在这种情况下, 以与 keepWaiting
属性相同的方式 实现 MoveNext()
方法 。 另外,您可以在 Current
属性中使用该对象,该对象 将在执行 MoveNext()
方法之后由Unity协程调度程序处理 。 例如,如果 Current
属性 返回另一个实现 IEnumerator
接口的对象, 则当前枚举器的执行将延迟到新的枚举器完成为止。
c! 这些是我非常想要的控制和行动自由。 就是这样,现在您将包裹好许多小皮包,使万向节锁不至于令人恐惧。 最主要的是,从幸福开始,从vertukha生产出来,不要爆炸。
介面
好吧,最好先确定要与我们的指令进行交互的接口。 一些最小的设置。
我建议以下选项
public interface IInstruction { bool IsExecuting { get; } bool IsPaused { get; } Instruction Execute(); void Pause(); void Resume(); void Terminate(); event Action<Instruction> Started; event Action<Instruction> Paused; event Action<Instruction> Cancelled; event Action<Instruction> Done; }
我想提请您注意IsExecuting
和IsPaused
在这里并非相反的事实。 如果协程被暂停,它仍在执行。
纸牌和妓女
如文档所述,您需要实现IEnumerator
接口。 现在,在某些地方,让我们暂不讲讲,因为它的实现直接取决于我们要添加的功能。
using UnityEngine; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator { private Instruction current; object IEnumerator.Current => current; void IEnumerator.Reset() { } bool IEnumerator.MoveNext() { } }
值得考虑的是,至少有两种方法可以启动我们的指令:
StartCoroutine(IEnumerator routine)
方法StartCoroutine(IEnumerator routine)
:
StartCoroutine(new ConcreteInstruction());
收益yield
:
private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); }
我们上面在IInstruction
界面中介绍的Execute
方法将使用第一种方法。 因此,在这种情况下,我们添加了一些字段来帮助管理指令。
private object routine; public MonoBehaviour Parent { get; private set; }
现在是IInstruction
属性和事件。
using UnityEngine; using System; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator, IInstruction { private Instruction current; object IEnumerator.Current => current; private object routine; public MonoBehaviour Parent { get; private; } public bool IsExecuting { get; private set; } public bool IsPaused { get; private set; } private bool IsStopped { get; set; } public event Action<Instruction> Started; public event Action<Instruction> Paused; public event Action<Instruction> Terminated; public event Action<Instruciton> Done; void IEnumerator.Reset() { } bool IEnumerator.MoveNext() { } Instruction(MonoBehaviour parent) => Parent = parent; }
另外,用于处理子类中事件的方法:
protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { }
将在相应事件之前立即手动调用它们,以使子类具有处理的优先级。
现在剩下的方法。
public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } }
这里的一切都很简单。 仅在正在执行且尚未暂停的情况下才可以暂停该指令,仅在已暂停的情况下才可以继续执行。
public void Terminate() { if (Stop()) { OnTerminated(); Terminate?.Invoke(this); } } private bool Stop() { if (IsExecuting) { if (routine is Coroutine) Parent.StopCoroutine(routine as Coroutine); (this as IEnumerator).Reset(); return IsStopped = true; } return false; }
停止指令的基本逻辑已移至Stop
方法。 为了能够执行静默停止(没有触发事件),这是必需的。
检查if (routine is Coroutine)
必要,因为如上所述,该指令可以由yield
启动(即,无需调用StartCoroutine
),这意味着可能没有指向Coroutine
特定实例的Coroutine
。 在这种情况下,该routine
将仅具有一个存根对象。
public Instruction Execute(MonoBehaviour parent) { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = (Parent = parent).StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; }
主要的启动方法也非常简单-仅在尚未启动指令时才执行启动。
剩下要完成IEnumerator
的实现,因为我们在某些方法中留了空白。
IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; }
最有趣但又不复杂的是MoveNext
:
bool IEnumerator.MoveNext() { if (IsStopped) { (this as IEnumerator).Reset(); return false; } if (!IsExecuting) { IsExecuting = true; routine = new object(); OnStarted(); Started?.Invoke(this); } if (current != null) return true; if (IsPaused) return true; if (!Update()) { OnDone(); Done?.Invoke(this); IsStopped = true; return false; } return true; }
if (!IsExecuting)
-如果指令不是通过StartCoroutine
启动的,并且执行了这一行代码,那么yield
启动它。 我们在routine
和火灾事件中编写存根。
if (current != null)
-current用于子指令。 如果突然出现这种情况,我们正在等待其结束。 请注意,我将错过在本文中添加对子说明的支持的过程,以免进一步夸大其词。 因此,如果您不希望进一步添加此功能,则只需删除这些行。
if (!Update())
-我们指令中的Update
方法将与keepWaiting
中的CustomYieldInstruction
一样keepWaiting
,并应在子类中实现。 Instruction
只是一种抽象方法。
protected abstract bool Update();
完整指令码 using UnityEngine; using System; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator, IInstruction { private Instruction current; object IEnumerator.Current => current; private object routine; public MonoBehaviour Parent { get; private set; } public bool IsExecuting { get; private set; } public bool IsPaused { get; private set; } private bool IsStopped { get; set; } public event Action<Instruction> Started; public event Action<Instruction> Paused; public event Action<Instruction> Terminated; public event Action<Instruction> Done; void IEnumerator.Reset() { IsPaused = false; IsStopped = false; routine = null; } bool IEnumerator.MoveNext() { if (IsStopped) { (this as IEnumerator).Reset(); return false; } if (!IsExecuting) { IsExecuting = true; routine = new object(); OnStarted(); Started?.Invoke(this); } if (current != null) return true; if (IsPaused) return true; if (!Update()) { OnDone(); Done?.Invoke(this); IsStopped = true; return false; } return true; } protected Instruction(MonoBehaviour parent) => Parent = parent; public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public void Resume() { IsPaused = false; OnResumed(); } public void Terminate() { if (Stop()) { OnTerminated(); Terminated?.Invoke(this); } } private bool Stop() { if (IsExecuting) { if (routine is Coroutine) Parent.StopCoroutine(routine as Coroutine); (this as IEnumerator).Reset(); return IsStopped = true; } return false; } public Instruction Execute() { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = Parent.StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; } public Instruction Execute(MonoBehaviour parent) { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = (Parent = parent).StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; } public void Reset() { Terminate(); Started = null; Paused = null; Terminated = null; Done = null; } protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { } protected abstract bool Update(); }
例子
基类已准备就绪。 要创建任何指令,只需继承它并实现必要的成员即可。 我建议看一些例子。
移至点
public sealed class MoveToPoint : Instruction { public Transform Transform { get; set; } public Vector3 Target { get; set; } public float Speed { get; set; } public float Threshold { get; set; } public MoveToPoint(MonoBehaviour parent) : base(parent) { } protected override bool Update() { Transform.position = Vector3.Lerp(Transform.position, Target, Time.deltaTime * Speed); return Vector3.Distance(Transform.position, Target) >= Threshold; } }
启动选项 private void Method() { var move = new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; move.Execute(); }
private void Method() { var move = new MoveToPoint(this); move.Execute(transform, target, 1.0F, 0.05F); }
private IEnumerator Method() { yield return new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; }
private IEnumerator Method() { var move = new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; yield return move; }
private IEnumerator Method() { var move = new MoveToPoint(this); yield return move.Execute(transform, target, 1.0F, 0.05F); }
输入处理
public sealed class WaitForKeyDown : Instruction { public KeyCode Key { get; set; } protected override bool Update() { return !Input.GetKeyDown(Key); } public WaitForKeyDown(MonoBehaviour parent) : base(parent) { } public Instruction Execute(KeyCode key) { Key = key; return base.Execute(); } }
启动示例 private void Method() { car wait = new WaitForKeyDown(this); wait.Execute(KeyCode.Space).Done += (i) => DoSomething(); }
private IEnumerator Method() { yield return new WaitForKeyDown(this) { Key = KeyCode.Space };
等待并暂停
public sealed class Wait : Instruction { public float Delay { get; set; } private float startTime; protected override bool Update() { return Time.time - startTime < Delay; } public Wait(MonoBehaviour parent) : base(parent) { } public Wait(float delay, MonoBehaviour parent) : base(parent) { Delay = delay; } protected override void OnStarted() { startTime = Time.time; } public Instruction Execute(float delay) { Delay = delay; return base.Execute(); } }
在这里,似乎一切都尽可能简单。 首先,我们记录开始时间,然后检查当前时间与StartTime
之间的时差。 但是有一个细微之处,您不能立即注意到。 如果在执行一条指令期间您将其暂停(使用Pause
方法)并等待,然后在恢复( Resume
)后立即执行该指令。 都是因为现在此说明没有考虑到我可以暂停它。
让我们尝试解决这种不公正现象:
protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; }
做完了 现在,暂停结束后,该指令将继续执行与开始之前一样多的时间。
启动范例 private void Method() { var wait = new Wait(this); wait.Execute(5.0F).Done += (i) => DoSomething; }
private IEnumerator Method() { yield return new Wait(this, 5.0F);
总结
Unity中的协程使您可以完成出色的功能。 但是它们的复杂程度以及您是否完全需要它们是每个人的个人选择。
扩展Corutin的功能并不是最困难的任务。 您需要做的就是确定要与他们进行交互的界面以及他们的工作算法。
多谢您的宝贵时间。 在评论中留下您的问题/评论/补充。 我将很高兴进行交流。
PS:如果突然之间,您对使用Corutin感到厌倦,请阅读此评论并喝一些凉爽的东西。