Coroutine Kustom dalam Persatuan dengan Preferensi dan Pelacur


Anda sudah sangat keren sehingga Anda memutar coroutine di sekitar semua sumbu pada saat yang sama, dari satu pandangan Anda, mereka melakukan yield break dan bersembunyi di balik kanvas IDE. Pembungkus sederhana adalah tahap yang sudah lama berlalu.


Anda tahu cara memasaknya dengan sangat baik sehingga Anda bisa mendapatkan bintang Michelin (atau bahkan dua) jika Anda memiliki restoran sendiri. Tentu saja! Tidak ada yang akan acuh dengan mencicipi Bouillabaisse Anda dengan saus coroutine.


Selama satu minggu penuh, kode di prod tidak jatuh! Start/Stop Coroutine Wrappers, callback 'dan Start/Stop Coroutine adalah banyak budak. Anda membutuhkan lebih banyak kontrol dan kebebasan bertindak. Anda siap untuk naik ke langkah berikutnya (tetapi tidak berhenti dari coroutine, tentu saja).


Jika Anda mengenali diri Anda dalam garis-garis ini, selamat datang di kucing.


Saya mengambil kesempatan ini untuk menyapa salah satu pola favorit saya - Perintah .


Pendahuluan


Untuk semua instruksi yield disediakan oleh Unity ( Coroutine , WaiForSeconds , WaiForSeconds , dan lainnya), kelas dasar adalah kelas YieldInstruction ( docs ) yang biasa-biasa saja:


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

Di bawah tenda, coroutine adalah enumerator biasa ( IEnumerator ( docs )). Setiap frame disebut dengan metode MoveNext() . Jika nilai pengembalian true , eksekusi ditunda hingga pernyataan yield berikutnya, jika false , itu berhenti.


Untuk instruksi yield khusus, Unity menyediakan kelas CustomYieldInstruction ( docs ) dengan nama yang sama.


 public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); } 

Sudah cukup untuk mewarisi darinya dan mengimplementasikan properti keepWaiting. Kita sudah membahas logikanya di atas. Itu harus kembali true selama coroutine akan dieksekusi.


Contoh dari dokumentasi:


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

Menurutnya, pengambil keepWating dijalankan setiap frame setelah Update() dan sebelum LateUpdate() .


Jika Anda bekerja dengan kode di mana hal-hal tertentu harus bekerja pada saat tertentu, dan masih belum mempelajari halaman ini , saya sangat merekomendasikan memutuskan prestasi ini.


Instruksi hasil kustom



Tapi kami tidak datang ke sini dengan pembungkus biasa seperti CustomYieldInstruction diwarisi. Untuk ini, Anda dapat tetap tanpa Michelin.


Oleh karena itu, kami merokok dokumentasi lebih lanjut dan hampir di bagian bawah halaman yang sama kami menemukan paragraf paling penting.


Untuk memiliki lebih banyak kontrol dan menerapkan instruksi hasil yang lebih kompleks, Anda dapat mewarisi langsung dari kelas System.Collections.IEnumerator . Dalam hal ini, terapkan metode MoveNext() dengan cara yang sama seperti Anda menerapkan properti keepWaiting . Selain itu, Anda juga dapat mengembalikan objek di properti Current , yang akan diproses oleh penjadwal coroutine Unity setelah menjalankan metode MoveNext MoveNext() . Jadi misalnya jika Current mengembalikan objek lain yang diwarisi dari IEnumerator , maka enumerator saat ini akan ditangguhkan hingga yang dikembalikan selesai.


Dalam bahasa Rusia

Untuk mendapatkan lebih banyak kontrol dan menerapkan instruksi yield yang lebih kompleks , Anda bisa mewarisi langsung dari antarmuka System.Collections.IEnumerator . Dalam hal ini, terapkan metode MoveNext() dengan cara yang sama seperti properti keepWaiting . Selain itu, Anda bisa menggunakan objek di properti Current , yang akan diproses oleh penjadwal coroutine Unity setelah menjalankan metode MoveNext() . Misalnya, jika properti Current ini mengembalikan objek lain yang mengimplementasikan antarmuka IEnumerator , eksekusi enumerator saat ini akan ditunda hingga penyelesaian yang baru.


Kerang suci! Ini adalah kontrol dan kebebasan bertindak yang sangat saya inginkan. Nah itu saja, sekarang Anda akan membungkusnya begitu corutins sehingga tidak ada Gimbal Lock yang menakutkan. Hal utama, dari kebahagiaan, dorongan dari vertukha untuk tidak menggedor.


Antarmuka


Yah, akan lebih baik untuk memulai dengan memutuskan antarmuka interaksi mana dengan instruksi yang ingin kita miliki. Beberapa set minimal.


Saya menyarankan opsi berikut


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

Saya ingin menarik perhatian Anda pada fakta bahwa IsExecuting dan IsPaused tidak sebaliknya di sini. Jika eksekusi coroutine dihentikan sementara, itu masih dieksekusi.


Solitaire dan pelacur


Seperti yang dikatakan dalam dokumentasi, Anda perlu mengimplementasikan antarmuka IEnumerator . Di beberapa tempat, untuk saat ini, mari kita tinggalkan stub, karena implementasinya secara langsung tergantung pada fungsionalitas apa yang ingin kita masukkan ke dalamnya.


 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() { } } 

Perlu dipertimbangkan bahwa setidaknya ada dua cara di mana instruksi kami dapat diluncurkan:


  1. Metode StartCoroutine(IEnumerator routine) :


     StartCoroutine(new ConcreteInstruction()); 

  2. yield hasil:


     private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); } 


Metode Execute , yang kami jelaskan di atas di antarmuka IInstruction , akan menggunakan metode pertama. Oleh karena itu, kami menambahkan beberapa bidang yang akan membantu mengelola instruksi dalam kasus ini.


 private object routine; public MonoBehaviour Parent { get; private set; } 

Sekarang properti dan acara untuk 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; } 

Juga, metode untuk menangani acara di kelas anak:


 protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { } 

Mereka akan dipanggil secara manual segera sebelum acara yang sesuai untuk memberikan kelas anak-anak prioritas pemrosesan mereka.


Sekarang metode yang tersisa.


 public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } } 

Semuanya sederhana di sini. Instruksi dapat dijeda hanya jika sedang dieksekusi dan jika belum ditangguhkan, dan melanjutkan eksekusi hanya jika ditangguhkan.


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

Logika dasar untuk menghentikan instruksi telah dipindahkan ke metode Stop . Ini diperlukan untuk dapat melakukan penghentian diam (tanpa memicu peristiwa).
Memeriksa if (routine is Coroutine) diperlukan karena, seperti yang saya tulis di atas, instruksi dapat dimulai dengan yield (yaitu, tanpa memanggil StartCoroutine ), yang berarti bahwa mungkin tidak ada tautan ke instance khusus Coroutine . Dalam hal ini, routine hanya akan memiliki objek rintisan.


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

Metode peluncuran utama juga sangat sederhana - peluncuran akan dilakukan hanya jika instruksi belum diluncurkan.


Tetap menyelesaikan implementasi IEnumerator , karena kami meninggalkan ruang kosong dalam beberapa metode.


 IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; } 

Dan yang paling menarik, tetapi tidak lebih rumit, adalah 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) - jika instruksi tidak diluncurkan melalui StartCoroutine , dan baris kode ini dieksekusi, maka yield meluncurkannya. Kami menulis tulisan rintisan dalam acara routine dan kebakaran.


if (current != null) - current digunakan untuk instruksi anak. Jika tiba-tiba ini muncul, kami menunggu akhirnya. Harap dicatat bahwa saya akan kehilangan proses menambahkan dukungan untuk instruksi anak di artikel ini agar tidak semakin mengembang. Oleh karena itu, jika Anda tidak tertarik untuk menambahkan fungsionalitas ini lebih lanjut, Anda dapat menghapus baris-baris ini.


if (!Update()) - metode Update dalam instruksi kami akan berfungsi seperti keepWaiting di CustomYieldInstruction dan harus diimplementasikan dalam kelas anak. Instruction hanyalah metode abstrak.


 protected abstract bool Update(); 

Seluruh Kode Instruksi
 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(); } 

Contohnya


Kelas dasar sudah siap. Untuk membuat instruksi apa pun, cukup mewarisi dan menerapkan anggota yang diperlukan. Saya sarankan untuk melihat beberapa contoh.


Pindah ke titik


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

Opsi peluncuran
 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); } 

Pemrosesan input


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

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

Tunggu dan jeda


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

Di sini, tampaknya, semuanya sesederhana mungkin. Pertama, kami mencatat waktu mulai, dan kemudian memeriksa perbedaan antara waktu saat ini dan StartTime . Namun ada satu nuansa yang tidak bisa langsung Anda perhatikan. Jika selama pelaksanaan instruksi Anda menjeda (menggunakan metode Pause ) dan menunggu, maka setelah melanjutkan ( Resume ) itu akan langsung dieksekusi. Dan semua karena sekarang instruksi ini tidak memperhitungkan bahwa saya dapat menghentikannya.


Mari kita coba untuk memperbaiki ketidakadilan ini:


 protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; } 

Selesai Sekarang, setelah akhir jeda, instruksi akan terus dieksekusi sebanyak waktu yang tersisa sebelum dimulai.


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

Ringkasan


Coroutines in Unity memungkinkan Anda melakukan hal-hal luar biasa dan fungsional. Tetapi betapa rumitnya mereka dan apakah Anda memerlukannya sama sekali adalah pilihan pribadi setiap orang.


Memperluas fungsi corutin bukanlah tugas yang paling sulit. Yang perlu Anda lakukan adalah memutuskan antarmuka mana yang Anda ingin berinteraksi dengan mereka dan apa algoritma pekerjaan mereka nantinya.


Terima kasih untuk waktu anda Tinggalkan pertanyaan / komentar / tambahan Anda di komentar. Saya akan senang berkomunikasi.




NB Jika, tiba-tiba, Anda terbakar karena menggunakan corutin, baca komentar ini dan minum sesuatu yang keren.

Source: https://habr.com/ru/post/id442820/


All Articles