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


您已经很酷了,可以同时绕所有轴旋转协同程序,一眼便看到它们执行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() { } } 

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


  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