質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.46%
Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

2回答

3707閲覧

コルーチンの無限呼び出し

退会済みユーザー

退会済みユーザー

総合スコア0

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

2クリップ

投稿2021/08/19 16:20

編集2021/08/21 07:48

前提・実現したいこと

呼び出したコルーチンが終了したら、再びそのコルーチンが開始されるように無限ループさせるには、どのように実装すればよいでしょうか。
1つのコルーチンをコルーチン内で無限にループさせるのではなく、1つのコルーチンの処理を一連の処理として、
そのコルーチンの呼び出しを無限にループさせる方法(一連の処理を無限に繰り返す方法)について考えています。
また、可能であれば、試したことで試みているように、
ループし続ける(呼び出され続ける)コルーチンと、ループさせ続ける(呼び出しを実行し続ける)コルーチンを別に分け、
ループし続けるコルーチンのほうは、引数で汎用的にセットできるようにしたいと思っています。
さらに、コルーチンの一時停止や、コルーチンの終了方法や再開についても考えています。
ご教示お願いします

試したこと

・質問1。

C#

1 void Start (){ 2 StartCoroutine(LoopCoroutine(MyCoroutine(3f))); 3 } 4 5 IEnumerator LoopCoroutine(IEnumerator coroutine){ 6 while(true){ 7 yield return coroutine; 8 Debug.Log("while"); 9 } 10 } 11 12 IEnumerator MyCoroutine(float animationLength){ 13 14 Debug.Log("MyCoroutine:開始"); 15 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 16 { 17 yield return null; 18 } 19 Debug.Log("MyCoroutine:終了"); 20 }

上記のように実装してみたのですが、最初だけMyCoroutineが動いているような挙動を示し、
それ以降はMyCoroutineがスキップされてwhile文でループし続けるような挙動となりました。
以下のようなログとなりました。なぜ、MyCoroutineは1回だけしか動かないのでしょうか。

MyCoroutine:開始 MyCoroutine:終了 while while while while while while

・質問2。
コルーチンの停止と終了は、どちらもStopCoroutine()で合っていますか?
StopCoroutine()で停止したコルーチンは、破棄されるという認識で合っていますか?
StopCoroutine()で停止したコルーチンは破棄されるけど、StartCoroutineで再開すると、
停止した箇所から再開されるという認識で合っていますか?
リファレンスには、StopCoroutineに関する、停止・破棄・再開に関する説明は書かれていませんでした。

・質問3。
質問1がわからない状態ですが、コルーチンの無限呼び出しに開始・停止・終了処理の操作ができるようにしたいと
思い、以下の実装を考えてみました。
質問1では、無限に呼び出すコルーチンを汎用的に引数でセットしたいと考えていましたが、
コルーチンの多重起動防止のnullチェックを考えると、そのフラグをフィールドで持たせるしかなさそうなので、
汎用的に無限に呼び出すコルーチンをセットすることは不可能になりそうと考えました(仮に可能であれば教えていただきたいです)。
これより、目的を妥協し、特定のコルーチンの無限呼び出しの開始・停止・終了ができるように以下のように
組みました。ただ、ゲームを再生してみると、
①左クリックで「開始」のログが出ることを確認する。
②右クリックで、「終了」のログが出る前に停止する。
③再び、左クリックすると、「開始」のログが出力される。
という挙動になり、③が意図しない挙動となりました。
本来であれば、②で停止しているので、③では再開に該当し、「終了」のログが出るはずと予想していたのですが、
「開始」のログが出力されてしまいます。
質問2にも関連してきますが、こちらの記事の「コルーチンの一時停止」の説明ににもあるように、
StopCoroutineしたコルーチンを、StartCoroutineすると再開されると思っていたのですが、違うのでしょうか?
また、質問1が未解決の為、検証できてないのですが、
StopCoroutine(myLoopCoroutine)で、中で動かしているMyCoroutineの方も停止できていますか?
MyCoroutineの方までは停止できなかった場合、どのような実装で停止できますか?

C#

1 Coroutine myLoopCoroutine; 2 3 void Start (){ 4 5 } 6 7 void Update(){ 8 if (Input.GetMouseButton(0)){ 9 StartLoopCoroutine(2f); 10 } 11 if (Input.GetMouseButton(1)){ 12 StopLoopCoroutine(); 13 } 14 } 15 16 void StartLoopCoroutine(float second){ 17 if (myLoopCoroutine == null){ 18 myLoopCoroutine = StartCoroutine(LoopCoroutine(MyCoroutine(second))); 19 } 20 } 21 22 void StopLoopCoroutine(){ 23 if (myLoopCoroutine != null){ 24 StopCoroutine(myLoopCoroutine); 25 myLoopCoroutine = null; 26 } 27 } 28 29 30 IEnumerator LoopCoroutine(IEnumerator coroutine){ 31 while(true){ 32 yield return coroutine; 33 Debug.Log("while"); 34 } 35 } 36 37 IEnumerator MyCoroutine(float animationLength){ 38 39 Debug.Log("MyCoroutine:開始"); 40 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 41 { 42 yield return null; 43 } 44 Debug.Log("MyCoroutine:終了"); 45 }

・質問4。
コルーチンを動かしているスクリプトがアタッチしているゲームオブジェクトを破棄する、
もしくはそのスクリプトコンポーネントを破棄すれば、
そのスクリプト内のコルーチンは、質問1のような入れ子のコルーチンも含めて、全て破棄されるという認識で合っていますか?
こちらは、質問1のスクリプトにおいて、Update()に以下のようなスクリプトやゲームオブジェクトを破棄する処理を書いて試してみた所、
whileのログが停止することが確認できました。
ただ、質問1で入れ子のコルーチンが1回しか動かない状態であることや、ログの停止は確認できていても、
本当にそれでコルーチンを正しく破棄できているか?という疑問があり、念のため、質問させていただきました。

C#

1 void Update(){ 2 if (Input.GetMouseButton(0)){ 3 // Destroy(this); 4 Destroy(this.gameObject); 5 } 6 }

追記

・質問5で試みた実装。

C#

1 IEnumerator myLoopCoroutine; 2 3 bool loopCoroutineIsRunning = false; 4 5 void Start (){ 6 } 7 8 void Update(){ 9 if (Input.GetMouseButtonDown(0)){ 10 // コルーチンを動かす。起動もしくは再開。 11 StartLoopCoroutine(2f); 12 } 13 if (Input.GetMouseButtonDown(1)){ 14 // コルーチンの停止。 15 StopLoopCoroutine(); 16 } 17 } 18 19 void StartLoopCoroutine(float second){ 20 Debug.Log("Start coroutine"); 21 if (loopCoroutineIsRunning){ 22 Debug.LogError("Stop coroutine first!"); 23 return; 24 } 25 if (myLoopCoroutine == null){ 26 myLoopCoroutine = LoopCoroutine(); 27 StartCoroutine(myLoopCoroutine); 28 Debug.Log("New coroutine is started"); 29 } 30 if (myLoopCoroutine != null){ 31 StartCoroutine(myLoopCoroutine); 32 Debug.Log("Resume paused coroutine"); 33 } 34 loopCoroutineIsRunning = true; 35 } 36 37 void StopLoopCoroutine(){ 38 Debug.Log("Stop coroutine"); 39 if (myLoopCoroutine == null){ 40 Debug.LogError("Start coroutine first!"); 41 return; 42 } 43 if (loopCoroutineIsRunning){ 44 StopCoroutine(myLoopCoroutine); 45 } 46 loopCoroutineIsRunning = false; 47 Debug.Log("Coroutine is completely stopped"); 48 } 49 50 IEnumerator LoopCoroutine(){ 51 while(true){ 52 yield return MyCoroutine(3f); 53 } 54 } 55 56 IEnumerator MyCoroutine(float animationLength){ 57 Debug.Log("MyCoroutine:開始"); 58 for (float time = 0.0f; time < animationLength; time += Time.deltaTime){ 59 Debug.Log(time); 60 yield return null; 61 } 62 Debug.Log("MyCoroutine:終了"); 63 }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

質問1について

ご参考になさった「UnityのCoroutine(コルーチン)でできる事のメモ - テラシュールブログ」の「コルーチンの一時停止」に、

なお一度完走したコルーチンを最初からやり直す場合、新しくメソッドからIEnumeratorを取得する必要があります。

との注意書きがありますね。LoopCoroutineの引数のcoroutineはループ1周目で反復処理を完走してしまっているので、2周目以降は何も起こらないんだろうと思います。

コルーチンを最初からやり直すには、IEnumeratorを再作成する必要がありそうです。代替案として下記のような形はどうでしょうか。

lang

1// LoopCoroutineの変更に合わせて、StartCoroutine部分を変更する 2void Start (){ 3 StartCoroutine(LoopCoroutine(() => MyCoroutine(3f))); 4} 5 6// LoopCoroutineの引数を、IEnumeratorそのものの代わりに 7// 「IEnumeratorを生成する手続き」とする 8IEnumerator LoopCoroutine(Func<IEnumerator> coroutineProvider){ 9 while(true){ 10 yield return coroutineProvider(); 11 Debug.Log("while"); 12 } 13} 14 15// MyCoroutineは変更なし 16IEnumerator MyCoroutine(float animationLength){ 17 Debug.Log("MyCoroutine:開始"); 18 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 19 { 20 yield return null; 21 } 22 Debug.Log("MyCoroutine:終了"); 23}

あるいは下記のようなスタイルも思いついたのですが、こちらだとMyCoroutineの方にも修正を加えることになるため、いまいちかもしれませんね。

lang

1// こちらはStartCoroutine部分の形は変化しない 2void Start (){ 3 StartCoroutine(LoopCoroutine(MyCoroutine(3f))); 4} 5 6// LoopCoroutineの引数をIEnumerable型に変更し、 7// 各ループでIEnumeratorを生成して待機する 8IEnumerator LoopCoroutine(IEnumerable coroutineProvider){ 9 while(true){ 10 yield return coroutineProvider.GetEnumerator(); 11 Debug.Log("while"); 12 } 13} 14 15// MyCoroutineの返す型を「IEnumerable」にする 16IEnumerable MyCoroutine(float animationLength){ 17 Debug.Log("MyCoroutine:開始"); 18 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 19 { 20 yield return null; 21 } 22 Debug.Log("MyCoroutine:終了"); 23}

質問2について

StartCoroutineの引数として渡すコルーチンの正体はただのIEnumeratorであって、StartCoroutineを実行したMonoBehaviourの側が自動的に適切なタイミングでMoveNextを行うことで順次処理が実行されるのだろうと思います。

StopCoroutineを行うと、この自動的なMoveNext実行がストップするのでしょう。IEnumeratorの側は、そのオブジェクトへのすべての参照がなくならない限りガベージコレクションの対象とはならず、メモリ上に生き残っているはずです。生き残っているIEnumeratorはどこまで処理を進行したかを覚えており、再びそのオブジェクトを引数にStartCoroutineを行うと、最後の中断地点からMoveNextの自動実行が始まるものと思われます。

質問3について

ご提示のコードだとIEnumeratorを再生成する形になっていますね。一時停止した時のIEnumeratorではなく新規にIEnumeratorを作ってStartCoroutineを行っているため、あらためて最初からコルーチンの処理が行われているのかと思います。
下記のような形だとどうでしょうかね?

lang

1// 型をIEnumeratorに変える 2IEnumerator myLoopCoroutine; 3 4// コルーチンが動作中かどうかを示すフィールドを用意する 5bool loopCoroutineIsRunning; 6 7void Start (){ 8} 9 10void Update(){ 11 if (Input.GetMouseButtonDown(0)){ 12 // 左クリックでコルーチンを再開する 13 // スペースキーを押しながら左クリックでコルーチンを新規スタートする 14 if (Input.GetKey(KeyCode.Space)){ 15 StartLoopCoroutine(2f); 16 }else{ 17 ResumeLoopCoroutine(); 18 } 19 } 20 if (Input.GetMouseButtonDown(1)){ 21 // 右クリックでmyLoopCoroutineを一時停止する 22 // スペースキーを押しながら右クリックでmyLoopCoroutineを停止する 23 if (Input.GetKey(KeyCode.Space)){ 24 StopLoopCoroutine(); 25 }else{ 26 PauseLoopCoroutine(); 27 } 28 } 29} 30 31void StartLoopCoroutine(float second){ 32 Debug.Log("Start new coroutine"); 33 if (myLoopCoroutine != null){ 34 Debug.Log("Stop previous coroutine"); 35 StopLoopCoroutine(); 36 } 37 myLoopCoroutine = LoopCoroutine(() => MyCoroutine(second)); 38 loopCoroutineIsRunning = true; 39 StartCoroutine(myLoopCoroutine); 40 Debug.Log("New coroutine is started"); 41} 42 43void PauseLoopCoroutine(){ 44 Debug.Log("Pause running coroutine"); 45 if (myLoopCoroutine == null){ 46 Debug.LogError("Start coroutine first!"); 47 return; 48 } 49 if (!loopCoroutineIsRunning){ 50 Debug.LogError("Start or resume coroutine first!"); 51 return; 52 } 53 StopCoroutine(myLoopCoroutine); 54 loopCoroutineIsRunning = false; 55 Debug.Log("Coroutine is paused"); 56} 57 58void ResumeLoopCoroutine(){ 59 Debug.Log("Resume paused coroutine"); 60 if (myLoopCoroutine == null){ 61 Debug.LogError("Start coroutine first!"); 62 return; 63 } 64 if (loopCoroutineIsRunning){ 65 Debug.LogError("Pause coroutine first!"); 66 return; 67 } 68 StartCoroutine(myLoopCoroutine); 69 loopCoroutineIsRunning = true; 70 Debug.Log("Coroutine is resumed"); 71} 72 73void StopLoopCoroutine(){ 74 Debug.Log("Stop coroutine"); 75 if (myLoopCoroutine == null){ 76 Debug.LogError("Start coroutine first!"); 77 return; 78 } 79 if (loopCoroutineIsRunning){ 80 StopCoroutine(myLoopCoroutine); 81 } 82 myLoopCoroutine = null; 83 loopCoroutineIsRunning = false; 84 Debug.Log("Coroutine is completely stopped"); 85} 86 87IEnumerator LoopCoroutine(Func<IEnumerator> coroutineProvider){ 88 while(true){ 89 yield return coroutineProvider(); 90 Debug.Log("while"); 91 } 92} 93 94IEnumerator MyCoroutine(float animationLength){ 95 Debug.Log("MyCoroutine:開始"); 96 for (float time = 0.0f; time < animationLength; time += Time.deltaTime){ 97 Debug.Log(time); 98 yield return null; 99 } 100 Debug.Log("MyCoroutine:終了"); 101}

なお、LoopCoroutineMyCoroutineは「質問1について」で例示したコードのうちの前者と同じ形にしています。
また注意点として、一時停止したあと再開する場合はコルーチンの1段目...つまりLoopCoroutineの中断地点からの再開となってしまい、その中で実行させていたはずのMyCoroutineの中断地点から再開することはできないようですね(中断地点が失われてしまっているのを明確にするため、MyCoroutine内でコンソールにtimeを出力するよう改変しています)。

この問題を回避するとなるとどうやらやっかいそうで、おそらく自前でコルーチン実行システムを作り、MoveNext実行を詳しく制御する必要があるかもしれません...

質問4について

コルーチン - Unity マニュアル」に...

Note: You can stop a Coroutine with StopCoroutine and StopAllCoroutines. A coroutines also stops when the GameObject it is attached to is disabled with SetActive(false). Calling Destroy(example) (where example is a MonoBehaviour instance) immediately triggers OnDisable and the coroutine is processed, effectively stopping it. Finally, OnDestroy is invoked at the end of the frame.

Coroutines are not stopped when disabling a MonoBehaviour by setting enabled to false on a MonoBehaviour instance.

との記述がありました。ですので、オブジェクトを破棄すればコルーチンは停止し、他のスクリプトでそのゲームオブジェクトやコンポーネントを参照している変数がなければC#スクリプト上のGameObjectMonoBehaviourもやがて解放され、スクリプトコンポーネント上で使用していたIEnumeratorへの参照も失われて解放対象になるだろうと思います。

使用していたIEnumeratorが別のスクリプトからも参照されていれば解放されることはないでしょうが、一般的なコルーチンの使用方法では生成したIEnumeratorを外へ持ち出して参照を保持し続けておくようなことはしないでしょうから、お気になさる必要はないかと思います。
仮にそのような状況になってIEnumeratorがメモリ上に生き残っていたとしても、メモリは少々占有し続けるでしょうが、コルーチンは停止していますので処理上の負荷はかからないだろうと思います。

投稿2021/08/19 22:31

編集2021/08/19 22:55
Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2021/08/21 07:51 編集

ご回答ありがとうございます。 コメントが遅くなり申し訳ございません。 コードを考えるのに時間がかかってしまいました。 ・質問1について。  コルーチンを最初からやり直す方法のご教示ありがとうございます。  理解できました。 ・質問2について。  なるほど、StopCoroutineだけでは破棄にまでは至らず、コルーチンの終了とはならないわけですね。  また、質問4のご回答で「コルーチンは停止していますので処理上の負荷はかからないだろう思います」  とのことで、  そのコルーチンを2度と呼び出すことがないとわかっていても、完全な破棄まではせずに、  そのコルーチンの終了処理は、StopCoroutineで止めておく程度でも問題ないでしょうか?  それとも、ガベージコレクションの対象になるように処理したほうがよいでしょうか?  その場合は、StopCoroutineで止めて、コルーチンを参照していたmyLoopCoroutineをnullにすれば、  ガベージコレクションの対象になるということでしょうか? ・質問3について。  ご回答ありがとうございます。  ご提示いただいたコードを試して、それぞれの動作を確認いたしました。  ご教示ありがとうございます。 ・質問4について。  ご回答ありがとうございます。  基本的には、オブジェクトを破棄すれば、コルーチンの終了として問題ないということですね。  すみません、念の為確認させていただきたいのですが、ゲームオブジェクトの破棄だけでなく、  スクリプトコンポーネントの破棄も、オブジェクトの破棄に含まれるということでしょうか?  (スクリプトコンポーネントの破棄でも、コルーチンの終了として問題ないということでしょうか? ・質問5。  質問が1点増えてしまい、申し訳ございません。  以下の仕様の処理も、使う用途がありそうなので、実装してみたいと思い、  ご教示いただいた質問3のコードを参考に実装を試みたのですが、  コードとしておかしい箇所や改善点はありますか?  質問の追記欄にコードを記載しました。  (仕様) ・特定のコルーチン(決め打ちのコルーチン)の繰り返し処理を行う。 ・開始・停止・再開の実装。  (終了は停止で問題ない(破棄までする必要はない)と仮定しています)。 ・操作としては、以下の2つ。  コルーチンを動かす(起動していなければ起動し、停止中であれば再開)。  コルーチンを止める。 ・多重起動を防止する。    試したところ、問題はなさそうでしたが、何か見落としている箇所があるかもしれないと思い、  質問させていただきました。  質問3でも確認しましたが、StopCoroutineで、LoopCoroutineだけではなく、  入れ子のMyCoroutineまで停止ができてしまうのですね。  一時停止したコルーチンは再開時に、入れ子のMyCoroutineは途中から再開できない(実装が大変)という点は承知しました。
Bongo

2021/08/21 15:14

質問2について 私の考えでは、普通はStopCoroutineで止めておくだけで十分かと思います。確かにおっしゃる通りmyLoopCoroutineをnullにすればガベージコレクションの対象になるはずですが、この程度の小さなオブジェクト1個のためにそこまで神経質になることはないんじゃないでしょうか。 チリも積もれば...なんて言葉もありますが、優先して対処するべきなのは「チリが削除されないまま蓄積され続ける構造になっている」とか「短時間で大量のチリが発生して簡単に山になる」とか「チリどころではない巨大廃棄物」だとかのケースでしょう。 質問4について そうですね。回答中のマニュアルの引用部分に「Destroy(example) (where example is a MonoBehaviour instance)」との言葉が出てきますが、ゲームオブジェクトは破棄せずにスクリプトコンポーネントだけを破棄した場合でも、そのスクリプトコンポーネント上で開始したコルーチンは停止するようです(念のためちょこっと試してみましたが、ちゃんとそのような挙動になっていました)。 質問5について 見たところ深刻な問題を起こしそうな部分はなさそうに思いました。あえて申し上げるとすると... void StartLoopCoroutine(float second){ Debug.Log("Start coroutine"); if (loopCoroutineIsRunning){ Debug.LogError("Stop coroutine first!"); return; } if (myLoopCoroutine == null){ myLoopCoroutine = LoopCoroutine(); StartCoroutine(myLoopCoroutine); Debug.Log("New coroutine is started"); } if (myLoopCoroutine != null){ StartCoroutine(myLoopCoroutine); Debug.Log("Resume paused coroutine"); } loopCoroutineIsRunning = true; } の部分でしょうかね? 新規にコルーチンをスタートする場合、まず「if (myLoopCoroutine == null){」のブロックでmyLoopCoroutineに新規コルーチンをセットおよびスタートし、これによりmyLoopCoroutineがnullでなくなるので、その下の「if (myLoopCoroutine != null){」のブロックも常に実行されることになるでしょう。 同じIEnumeratorオブジェクトに対して2回連続でStartCoroutineを行っても、どうやら二重にスタートしてしまうことはなさそうなので動作上は問題ないようですが、ちょっと紛らわしいかもしれません。下記のような形でもいいんじゃないでしょうか。 void StartLoopCoroutine(float second){ Debug.Log("Start coroutine"); if (loopCoroutineIsRunning){ Debug.LogError("Stop coroutine first!"); return; } if (myLoopCoroutine == null){ myLoopCoroutine = LoopCoroutine(); Debug.Log("New coroutine is started"); }else{ Debug.Log("Resume paused coroutine"); } StartCoroutine(myLoopCoroutine); loopCoroutineIsRunning = true; } また、引数のsecondは使用されていないようです。今後使うつもりでしたらかまわないのですが、引数が不要なら削除してしまった方が紛らわしくないんじゃないかと思います。
退会済みユーザー

退会済みユーザー

2021/08/22 06:28

ご回答ありがとうございます。 ・質問2について。  コルーチンの終了についてのご教示ありがとうございます。  理解しました。勉強になります。 ・質問4について。  スクリプトコンポーネントの破棄でも、コルーチンの終了として問題ないこと承知しました。  ご教示ありがとうございます。 ・質問5について。  StartLoopCoroutineのmyLoopCoroutineのセット後に、無駄に後のif文に引っ掛かってしまう件、  ご指摘ありがとうございます。こちら全然気づきませんでした。  修正案のご教示もありがとうございます。  StartCoroutineを最後に1回だけ呼び出して、コードが簡略化できているわけですね。  とても勉強になります。  また、StartLoopCoroutineの引数のsecondは間違って付与していました。  ご指摘ありがとうございます。 全ての疑問点が解決しました。 とても勉強になりました。 たくさんのご教示大変ありがとうございました。
guest

0

そのコルーチンの終了時に StartCoroutineを呼んでやればいいだけなんでは

投稿2021/08/19 16:26

y_waiwai

総合スコア87800

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.46%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問