自定义协程与偏好和妓女的统一


您已经很酷了,可以同时绕所有轴旋转协同程序,一眼便看到它们执行yield break并隐藏在IDE画布后面。 简单的包装器是一个漫长的阶段。


您知道如何做得好,如果拥有自己的餐厅,您可以得到一颗米其林星(甚至两颗)。 当然可以! 没有人会喜欢用协程调味酱品尝法式海鲜汤。


整整一个星期,产品中的代码都不会丢失! 包装, callbackStart/Stop Coroutine是很多从属方法。 您需要更多的控制权和行动自由。 您已准备好迈出下一步(当然,不要退出协程)。


如果您在这些方面认出自己,欢迎来关注。


我借此机会向我最喜欢的模式之一-Command打招呼。


引言


对于Unity提供的所有yield指令( CoroutineAsyncOperationWaiForSeconds等),基类是不起眼的YieldInstructiondocs )类:


 [StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { } 

在内部,协程是普通的枚举器( IEnumeratordocs ))。 每个帧都由MoveNext()方法MoveNext() 。 如果返回值为true ,则执行将延迟到下一个yield语句;如果为false ,则它将停止。


对于自定义的yield说明,Unity提供了相同名称的CustomYieldInstructiondocs )类。


 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; } 

我想提请您注意IsExecutingIsPaused在这里并非相反的事实。 如果协程被暂停,它仍在执行。


纸牌和妓女


如文档所述,您需要实现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() { } } 

值得考虑的是,至少有两种方法可以启动我们的指令:


  1. StartCoroutine(IEnumerator routine)方法StartCoroutine(IEnumerator routine)


     StartCoroutine(new ConcreteInstruction()); 

  2. 收益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 }; // do something; } 

等待并暂停


 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); // do something; } 

总结


Unity中的协程使您可以完成出色的功能。 但是它们的复杂程度以及您是否完全需要它们是每个人的个人选择。


扩展Corutin的功能并不是最困难的任务。 您需要做的就是确定要与他们进行交互的界面以及他们的工作算法。


多谢您的宝贵时间。 在评论中留下您的问题/评论/补充。 我将很高兴进行交流。




PS:如果突然之间,您对使用Corutin感到厌倦,请阅读评论并喝一些凉爽的东西。

Source: https://habr.com/ru/post/zh-CN442820/


All Articles