Unityでコルーチンをもう少し便利にする方法

各開発者は、Unityでコルーチンを使用することの長所と短所を見つけます。 そして、どのシナリオでそれらを適用し、どのシナリオで代替案を優先するかを決定します。


日常業務では、さまざまなタイプのタスクにコルーチンをよく使用します。 かつて、私は彼らの多くの新人を悩ませ、拒否したのは私だと気づきました。


悪夢のインターフェース


エンジンは、コルーチンを操作するためのメソッドとそれらのオーバーロードのいくつかのみを提供します。


起動docs



停止docs



文字列パラメータを使用したオーバーロードは(その見た目が便利ですが)すぐにできます ゴミ箱に送る 少なくとも3つの理由で忘れてください。



一方で、提供される方法は基本的なニーズをカバーするのに十分です。 しかし、時間が経つにつれて、私は積極的に使用すると、大量の定型コードを書かなければならないことに気付き始めました-このタイヤは、読みやすさを損ないます。


ポイントに近い


この記事では、長い間使用している小さなラッパーについて説明します。 彼女のおかげで、コルーチンについての考えとともに、テンプレートコードの断片が頭の中に現れなくなりました。 さらに、コルーチンが使用されているコンポーネントをチーム全体で読みやすく理解できるようになりました。


次のタスクがあるとします:オブジェクトを特定のポイントに移動できるコンポーネントを作成する。


この時点では、どの方法でどの座標で動きが実行されるかは関係ありません。 多くのオプションのうち1つだけが選択されます-これらは補間とグローバル座標です。


RigidBodyコンポーネントが(特にUpdateメソッドで)使用されている場合、座標「額」、つまり、構造体transform.position = newPosition変更してオブジェクトを移動することは非常に推奨されないことに注意してください。


標準実装


必要なコンポーネントに次の実装オプションを提案します。


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

コードについてのビット

Moveメソッドでは、コルーチンがまだ実行されていない場合にのみコルーチンを起動することが非常に重要です。 それ以外の場合は、必要なだけ起動でき、それぞれがオブジェクトを移動します。


threshold -許容範囲。 言い換えれば、目標に到達したと仮定するポイントに近づく距離。


それは何のためですか?


Vector3構造体のすべてのコンポーネント( xyz )がfloat型であるとすると、ループ条件としてターゲットまでの距離と許容差のチェック結果を使用するのは悪い考えです。


ターゲットまでの距離をチェックして、この問題を回避できます。


また、必要に応じて、 Mathf.Approximatelydocs )メソッドを使用して、同等性の近似チェックを行うことができます。 いくつかの移動方法では、オブジェクトが1フレームでターゲットを「ジャンプ」するのに十分な速度になることがあります。 その後、サイクルは終了しません。 たとえば、 Vector3.MoveTowardsメソッドを使用する場合。


私の知るVector3Vector3構造のUnityエンジンでは、 Vector3演算子は、 Mathf.ApproximatelyがコンポーネントMathf.Approximately等価性をチェックするために呼び出されるように既に再定義されています。


これですべてです。コンポーネントは非常に単純です。 そして、現時点では問題はありません。 しかし、オブジェクトをポイントに移動できるが、停止する機会を提供しないこのコンポーネントとは何ですか。 この不正を修正しましょう。


あなたと私は悪の側に行かず、文字列パラメーターでオーバーロードを使用しないことにしたので、実行中のコルーチンへのリンクをどこかに保存する必要があります。 そうでなければ、彼女を止める方法は?


フィールドを追加します。


 private Coroutine moveRoutine; 

修正Move


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

ストップモーションメソッドを追加します。


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

全コード
 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; } } } 

完全に異なる問題! 傷に適用されますが。


だから。 タスクを実行する小さなコンポーネントがあります。 私のinりは何ですか?


問題とその解決策


時間が経つにつれて、プロジェクトは成長し、それに伴い、コルーチンを使用するコンポーネントを含むコンポーネントの数が増加します。 そして、毎回、これらのことはますます私を悩ませています:



 StartCoroutine(MoveRoutine()); 

 StopCoroutine(moveRoutine); 

それらを見るだけで目が震えますが、そのようなコードを読むのは疑わしい喜びです(私は同意します、それはもっと悪いことです)。 しかし、次のようなものを持っている方がはるかに見栄えがよくなります。


 moveRoutine.Start(); 

 moveRoutine.Stop(); 


 moveRoutine = StartCoroutine(MoveRoutine()); 

それ以外の場合、コルーチンへの参照がないため、単にコルーチンを停止することはできません。



 if (moveRoutine == null) 

 if (moveRoutine != null) 


忘れると、1回限りのコルーチンが届きます。 moveRoutineでの最初の実行後、コルーチンへのリンクは残り、新しい実行は機能しません。


同様に、強制停止の場合に行う必要があります。


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

すべての変更を含むコード
 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; } } 

ある時点で、このマスカレード全体を一度だけ取り出して、必要なメソッド: StartStopさらにいくつかのイベントとプロパティだけを残したいです。


やっとやろう!


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

デブリーフィング

Owner -コルーチンがアタッチされるMonoBehaviourインスタンスへのリンク。 ご存知のように、 StopCoroutineメソッドとStopCoroutineメソッドがStopCoroutineは特定のコンポーネントであるため、特定のコンポーネントのコンテキストで実行する必要があります。 したがって、コルーチンの所有者となるコンポーネントへのリンクが必要です。


Coroutine MoveToPointコンポーネントのmoveRoutineフィールドに類似しており、現在のコルーチンへのリンクが含まれています。


Routine -コルーチンとして機能するメソッドが通信されるデリゲート。


Process()は、メインRoutineメソッドの小さなラッパーです。 コルーチンの実行がいつ完了したかを追跡し、そのリンクをリセットし、その時点で別のコードを実行するために必要です(必要な場合)。


IsProcessingコルーチンが現在実行中かどうかを確認できます。


したがって、大量の頭痛を取り除き、コンポーネントの外観は完全に異なります。


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

残っているのは、コルーチン自体と、それを使用する数行のコードだけです。 大幅に改善されました。


新しいタスクが来たとします-オブジェクトが目標に到達した後、コードを実行する機能を追加する必要があります。


最初のバージョンでは、各コルーチンに追加のデリゲートパラメーターを追加する必要があり、完了後にプルできます。


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

そして、次のように呼び出します。


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

そして、ハンドラーとしてラムダがある場合、さらに悪いように見えます。


ラッパーを使用すると、このイベントを一度だけ追加するだけで十分です。


 public Action Finish; 

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

そして、必要に応じて、サブスクライブします。


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

ラッパーの現在のバージョンは、パラメーターなしのコルーチンのみで機能することを提供していることにお気づきだと思います。 したがって、1つのパラメーターでコルーチンの汎用ラッパーを作成できます。 残りは類推によって行われます。


しかし、良いことのために、同じものを書かないように、すべてのラッパーで同じコードを最初に何らかの基本クラスに入れるとよいでしょう。 私たちはこれと戦っています。


削除します:



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

リファクタリング後のパラメーターレスラッパー
 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; } } } 

そして今、実際には、1つのパラメーターを持つコルーチンのラッパー:


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

ご覧のとおり、コードはほとんど同じです。 引数の数に応じたフラグメントは、一部の場所でのみ追加されました。


エディターのInspectorウィンドウではなく、 Moveメソッドが呼び出されたときにコードでポイントを設定できるように、MoveToPointコンポーネントを更新するように求められたとします。


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

このラッパーの機能を可能な限り拡張するための多くのオプションがあります。遅延起動、パラメーター付きイベント、コルーチンの進行状況の追跡などを追加します。 ただし、この段階で停止することをお勧めします。


この記事の目的は、私が遭遇した差し迫った問題を共有し、それらに対する解決策を提案することであり、すべての開発者の可能なニーズをカバーすることではありません。


初心者と経験豊富な同志の両方が私の経験から利益を得ることを願っています。 おそらく彼らはコメントを共有したり、私が犯したかもしれないエラーを指摘したりするでしょう。




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


All Articles