Cara membuat coroutine di Unity sedikit lebih nyaman

Setiap pengembang menemukan kelebihan dan kekurangan menggunakan coroutine di Unity. Dan dia memutuskan di mana skenario untuk menerapkannya, dan di mana untuk memberikan preferensi terhadap alternatif.


Dalam latihan sehari-hari, saya sering menggunakan coroutine untuk berbagai jenis tugas. Suatu kali, saya menyadari bahwa sayalah yang mengganggu dan menolak banyak pendatang baru di dalamnya.


Antarmuka mimpi buruk


Mesin hanya menyediakan beberapa metode dan beberapa kelebihannya untuk bekerja dengan coroutine:


Luncurkan ( dokumen )


  • StartCoroutine(string name, object value = null)
  • StartCoroutine(IEnumerator routine)

Stop ( dokumen )


  • StopCoroutine(string methodName)
  • StopCoroutine(IEnumerator routine)
  • StopCoroutine(Coroutine routine)

Overload dengan parameter string (meskipun mudah menipu) dapat segera kirim ke tempat sampah lupakan setidaknya tiga alasan.


  • Penggunaan nama metode string secara eksplisit akan mempersulit analisis kode di masa mendatang, debugging, dan lainnya.
  • Menurut dokumentasi, string overload membutuhkan waktu lebih lama dan hanya memungkinkan satu parameter tambahan untuk diteruskan.
  • Dalam latihan saya, cukup sering ternyata tidak ada yang terjadi ketika string StopCoroutine overload StopCoroutine . Corutin terus dieksekusi.

Di satu sisi, metode yang disediakan cukup untuk memenuhi kebutuhan dasar. Namun seiring waktu, saya mulai memperhatikan bahwa dengan penggunaan aktif saya harus menulis sejumlah besar kode boilerplate - ini melelahkan, dan merusak keterbacaannya.


Lebih dekat ke intinya


Pada artikel ini saya ingin menjelaskan bungkus kecil yang telah saya gunakan untuk waktu yang lama. Berkat dia, dengan pemikiran tentang coroutine, potongan-potongan kode templat tidak lagi muncul di kepalaku yang dengannya aku harus menari di sekitar mereka. Selain itu, seluruh tim menjadi lebih mudah membaca dan memahami komponen-komponen di mana coroutine digunakan.


Misalkan kita memiliki tugas berikut - untuk menulis komponen yang memungkinkan Anda untuk memindahkan objek ke titik tertentu.


Pada saat ini, tidak masalah metode apa yang akan digunakan untuk bergerak dan koordinat apa. Hanya satu dari banyak opsi yang akan saya pilih - ini adalah interpolasi dan koordinat global.


Harap dicatat bahwa sangat disarankan untuk tidak memindahkan objek dengan mengubah koordinatnya โ€œdi dahiโ€, yaitu, transform.position = newPosition jika komponen RigidBody digunakan dengannya (terutama dalam metode Update ) ( docs ).


Implementasi standar


Saya mengusulkan opsi implementasi berikut untuk komponen yang diperlukan:


 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; public void Move() { if (moveRoutine == null) StartCoroutine(MoveRoutine()); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

Sedikit tentang kode

Dalam metode Move , sangat penting untuk menjalankan coroutine hanya ketika belum dijalankan. Kalau tidak, mereka dapat diluncurkan sebanyak yang diinginkan dan masing-masing dari mereka akan memindahkan objek.


threshold - toleransi. Dengan kata lain, jarak ke titik, mendekati yang akan kita asumsikan bahwa kita telah mencapai tujuan.


Untuk apa ini?


Mengingat bahwa semua komponen ( x , y , z ) dari struktur Vector3 adalah tipe float , menggunakan hasil dari memeriksa kesetaraan jarak ke target dan toleransi sebagai kondisi loop adalah ide yang buruk .


Kami memeriksa jarak ke target untuk lebih / kurang, yang memungkinkan kami untuk menghindari masalah ini.


Selain itu, jika diinginkan, Anda dapat menggunakan metode Mathf.Approximately ( docs ) untuk pemeriksaan perkiraan kesetaraan. Perlu dicatat bahwa dengan beberapa metode bergerak, kecepatan mungkin berubah menjadi cukup besar sehingga objek "melompat" target dalam satu bingkai. Maka siklus tidak akan pernah berakhir. Misalnya, jika Anda menggunakan metode Vector3.MoveTowards .


Vector3 saya tahu, di mesin Unity untuk struktur Vector3 , operator Vector3 sudah didefinisikan ulang sedemikian rupa sehingga Mathf.Approximately dipanggil untuk memeriksa kesetaraan komponen-bijaksana.


Itu saja untuk saat ini, komponen kami cukup sederhana. Dan saat ini tidak ada masalah. Tapi, apa komponen ini yang memungkinkan Anda untuk memindahkan objek ke suatu titik, tetapi tidak memberikan kesempatan untuk menghentikannya. Mari kita perbaiki ketidakadilan ini.


Karena Anda dan saya memutuskan untuk tidak pergi ke sisi jahat, dan tidak menggunakan kelebihan dengan parameter string, sekarang kita perlu menyimpan suatu tempat tautan ke coroutine yang sedang berjalan. Kalau tidak, bagaimana cara menghentikannya?


Tambahkan bidang:


 private Coroutine moveRoutine; 

Perbaiki Move :


 public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } 

Tambahkan metode stop motion:


 public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } 

Seluruh kode
 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

Masalah yang sama sekali berbeda! Setidaknya berlaku untuk luka.


Jadi Kami memiliki komponen kecil yang melakukan tugas. Apa geramanku?


Masalah dan solusinya


Seiring waktu, proyek tumbuh, dan bersamanya jumlah komponen, termasuk yang menggunakan coroutine. Dan setiap kali, hal-hal ini semakin menghantuiku:


  • Panggilan Sandwich yang sedang berlangsung

 StartCoroutine(MoveRoutine()); 

 StopCoroutine(moveRoutine); 

Hanya dengan melihat mereka membuat mata saya berkedut, dan membaca kode seperti itu adalah kesenangan yang meragukan (saya setuju, ini bisa lebih buruk). Tetapi akan jauh lebih bagus dan lebih visual untuk memiliki sesuatu seperti itu:


 moveRoutine.Start(); 

 moveRoutine.Stop(); 

  • Setiap kali Anda menelepon StartCoroutine Anda harus ingat untuk menyimpan nilai pengembalian:

 moveRoutine = StartCoroutine(MoveRoutine()); 

Kalau tidak, karena kurangnya referensi ke coroutine, Anda tidak bisa menghentikannya.


  • Pemeriksaan konstan:

 if (moveRoutine == null) 

 if (moveRoutine != null) 

  • Dan satu lagi hal jahat yang harus selalu Anda ingat (dan yang saya lupa lagi terlewatkan). Di akhir coroutine dan sebelum setiap keluar dari itu (misalnya, menggunakan yield break ), perlu untuk mengatur ulang nilai bidang.
     moveRoutine = null; 

Jika Anda lupa, Anda akan menerima coroutine satu kali. Setelah jalankan pertama di moveRoutine , tautan ke coroutine akan tetap, dan jalankan baru tidak akan berfungsi.


Dengan cara yang sama, Anda harus melakukannya jika berhenti secara paksa:


 public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } 

Kode dengan semua perubahan
 public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null; } } 

Pada satu titik, saya benar-benar ingin sekali menyapu seluruh penyamaran ini di suatu tempat, dan meninggalkan sendiri hanya metode yang diperlukan: Start , Stop dan beberapa lagi acara dan properti.


Akhirnya mari kita lakukan!


 using System.Collections; using System; using UnityEngine; public sealed class CoroutineObject { public MonoBehaviour Owner { get; private set; } public Coroutine Coroutine { get; private set; } public Func<IEnumerator> Routine { get; private set; } public bool IsProcessing => Coroutine != null; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if (IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

Tanya jawab

Owner - tautan ke instance MonoBehaviour yang akan dilampirkan coroutine. Seperti yang Anda ketahui, itu harus dijalankan dalam konteks komponen tertentu, karena StopCoroutine StartCoroutine dan metode StopCoroutine . Karenanya, kita memerlukan tautan ke komponen yang akan menjadi pemilik coroutine.


Coroutine - analog dari bidang moveRoutine di komponen MoveToPoint , berisi tautan ke coroutine saat ini.


Routine - delegasi dengan siapa metode bertindak sebagai coroutine akan dikomunikasikan.


Process() adalah pembungkus kecil di atas metode Routine utama. Hal ini diperlukan agar dapat dilacak ketika eksekusi coroutine selesai, setel ulang tautannya dan jalankan kode lain pada saat itu (jika perlu).


IsProcessing - memungkinkan Anda untuk mengetahui apakah coroutine sedang berjalan.


Dengan demikian, kami menghilangkan sejumlah besar sakit kepala, dan komponen kami memiliki tampilan yang sangat berbeda:


 using IEnumerator = System.Collections; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private CoroutineObject moveRoutine; private void Awake() { moveRoutine = new CoroutineObject(this, MoveRoutine); } public void Move() => moveRoutine.Start(); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

Yang tersisa hanyalah coroutine itu sendiri dan beberapa baris kode untuk bekerja dengannya. Secara signifikan lebih baik.


Misalkan tugas baru telah tiba - Anda perlu menambahkan kemampuan untuk mengeksekusi kode apa pun setelah objek telah mencapai tujuannya.


Dalam versi asli, kita harus menambahkan parameter delegasi tambahan ke masing-masing coroutine yang dapat ditarik setelah selesai.


 private IEnumerator MoveRoutine(System.Action callback) { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null callback?.Invoke(); } 

Dan teleponlah sebagai berikut:


 moveRoutine = StartCoroutine(moveRoutine(CallbackHandler)); private void CallbackHandler() { // do something } 

Dan jika ada beberapa lambda sebagai pawang, maka itu terlihat lebih buruk.


Dengan pembungkus kami, cukup menambahkan acara ini hanya sekali.


 public Action Finish; 

 private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finish?.Invoke(); } 

Dan kemudian, jika perlu, berlangganan.


 moveRoutine.Finished += OnFinish; private void OnFinish() { // do something } 

Saya percaya bahwa Anda telah memperhatikan bahwa versi pembungkus saat ini menyediakan kemampuan untuk bekerja hanya dengan coroutine tanpa parameter. Oleh karena itu, kita dapat menulis pembungkus umum untuk coroutine dengan satu parameter. Sisanya dilakukan dengan analogi.


Tapi, demi kebaikan, alangkah baiknya untuk pertama-tama memasukkan kode, yang akan sama untuk semua pembungkus, ke beberapa kelas dasar, agar tidak menulis hal yang sama. Kami sedang melawan ini.


Kami menghapusnya:


  • Owner Properti, Coroutine , sedang IsProcessing
  • Acara Finished

 using Action = System.Action; using UnityEngine; public abstract class CoroutineObjectBase { public MonoBehaviour Owner { get; protected set; } public Coroutine Coroutine { get; protected set; } public bool IsProcessing => Coroutine != null; public abstract event Action Finished; } 

Pembungkus tanpa parameter setelah refactoring
 using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject : CoroutineObjectBase { public Func<IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finished?.Invoke(); } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

Dan sekarang, pada kenyataannya, pembungkus untuk coroutine dengan satu parameter:


 using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject<T> : CoroutineObjectBase { public Func<T, IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<T, IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process(T arg) { yield return Routine.Invoke(arg); Coroutine = null; Finished?.Invoke(); } public void Start(T arg) { Stop(); Coroutine = Owner.StartCoroutine(Process(arg)); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

Seperti yang Anda lihat, kodenya hampir sama. Hanya di beberapa tempat ditambahkan fragmen, tergantung pada jumlah argumen.


Misalkan kita diminta untuk memperbarui komponen MoveToPoint sehingga titik tersebut tidak dapat ditetapkan melalui jendela Inspector di editor, tetapi dengan kode ketika metode Move dipanggil.


 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public float speed; public float threshold; private CoroutineObject<Vector3> moveRoutine; public bool IsMoving => moveRoutine.IsProcessing; private void Awake() { moveRoutine = new CoroutineObject<Vector3>(this, MoveRoutine); } public void Move(Vector3 target) => moveRoutine.Start(target); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine(Vector3 target) { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed); yield return null; } } } 

Ada banyak opsi untuk memperluas fungsionalitas pembungkus ini sebanyak mungkin: menambahkan peluncuran yang tertunda, acara dengan parameter, pelacakan kemungkinan kemajuan coroutine, dan banyak lagi. Tapi saya sarankan berhenti pada tahap ini.


Tujuan artikel ini adalah keinginan untuk berbagi masalah mendesak yang saya temui dan mengusulkan solusi kepada mereka, dan bukan untuk menutupi kemungkinan kebutuhan semua pengembang.


Saya berharap bahwa kawan pemula dan yang berpengalaman akan mendapat manfaat dari pengalaman saya. Mungkin mereka akan membagikan komentar mereka atau menunjukkan kesalahan yang bisa saya lakukan.


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


All Articles