コルーチンについての教科書的なQiita
https://qiita.com/4_mio_11/items/3030f9a39f59cd68f705
基本的にUpdateで1フレームごとの処理しかできないUnity
コルーチンを使うと、処理を1フレーム遅らせたり、1秒遅らせたりできる。
非同期処理と呼ばれるものらしい。
やりたいこと3つのタイプ
ゲームでコルーチンを使うってどういう状況?
①簡単なのは使い捨ての処理
1秒使ってアニメーションした後に、処理を終了する。
②自分が詰まってるのはメインの処理の一部として動いている処理
この場合返り値を設定する必要がある
③もっと複雑になった処理。複数の返り値を待ってメインの処理に戻る
どう実装したらいいの?
すごいかっこ悪い実装
void Start(){
StartCoroutine(Coro());
}
public void NextStep() {
Debug.Log("次の処理");
}
IEnumerator Coro() {
yield return new WaitForSeconds(1.0f);//1秒待ってから
NextStep();
}
コルーチンの終了を待ってからその中で次の処理を動かす。
ただしオブジェクト指向的な考えだとかっこ悪い感じ。
判定の変数を用意してあげる
private bool check;
void Start(){
check = false;
StartCoroutine(Coro());
}
private void Update() {
if (check) {
NextStep();
}
}
public void NextStep() {
Debug.Log("次の処理");
}
IEnumerator Coro() {
yield return new WaitForSeconds(1.0f);//1秒待ってから
check = true;
}
このやり方も簡単ではある。
がしかし、わかりずらい。checkってなんやねん。
STARTもIEnumeratorにして、コルーチンの終了を待つ
bool TF = true;
IEnumerator Start(){
Debug.Log("START : " + Time.time);
IEnumerator ie = CoroutineA();
Coroutine c = StartCoroutine(ie);//コルーチンを開始する
Debug.Log("START Coroutine : " + Time.time);
yield return c; //コルーチンの終了を待つ
Debug.Log("FINISHED Coroutine : " + Time.time);
yield return null;
}
private void Update() {
if (TF) {Debug.Log("Update : " + Time.time);TF = false;}
}
IEnumerator CoroutineA() {
yield return new WaitForSeconds(1.0f);//1秒待ってから
yield return null;
}
yield return c;
でコルーチンの終了を待っている。よってSTARTもIEnumerator扱い。
しかし、この遅延がUpdateを遅らせる能力はない。
正直なんでyield return c;で遅延(待機?)できるのかわからないが、なんかそういうもんらしい。かしこい人教えてください。
複数コルーチンを待つ実装で躓いた
IEnumerator Start() {
//リストから全部順番に実行する
List<IEnumerator> ie = new List<IEnumerator>();
ie.Add(CoroutinA());
ie.Add(CoroutinB());
foreach (IEnumerator item in ie){
StartCoroutine(item);
yield return item.Current;// <===ここが重要
}
yield return null;
}
IEnumerator CoroutinA() {
Debug.Log("CA SATRT" + Time.time);
yield return new WaitForSeconds(1.0f);
Debug.Log("CA FIN" + Time.time);
yield return null;
}
IEnumerator CoroutinB() {
Debug.Log("CB SATRT" + Time.time);
yield return new WaitForSeconds(2.0f);
Debug.Log("CB FIN" + Time.time);
}
やりたいこと
CoroutineAをやり切った後にCoroutinBを発動させたい。
yield return item.Current; の .Current がなかった場合の結果↓
WaitForSecondsが全く機能せず、出力されている。
どうやら、IEnumaratorのreturnにはコルーチン機能とか使って時間稼がないで早く返事(return)くれという強烈な能力があるらしい。中にあるコルーチンの情報は無意味になるが、CA FINが実行されているので、中身の完遂はされ、1フレーム内でコルーチンを無視して終了させる。まあーあんまりそういう目的で使わない方がいいと思う。
yield return item.Current; の .Current がある場合の結果↓
ちゃんと思い通りの挙動をしてくれました。
Aが動いて、A終了後、Bが動いて、Bが終了する。
IEnumarator.Currentをreturnするとコルーチン終了まで待ってくれるみたいです。
複数処理を同時に初めて、すべての終了を検知する
失敗したコード
IEnumerator Start() {
List<IEnumerator> ie = new List<IEnumerator>();
ie.Add(CoroutinA());
ie.Add(CoroutinB());
foreach (IEnumerator item in ie) {
StartCoroutine(item);
}
foreach (IEnumerator item in ie) {
yield return item.Current;
}
Debug.Log("FIN ALL " + Time.time);
}
もう一度foreachを回して、すべて終了するまで待つやり方なのだが、ご覧の通りFIN ALLが3秒後になっている。
理由はよくわからないがitem.Currentを呼び出したタイミングで再度StartCoroutin()の時間経過を計算しているように見える。
解決策としてはIEnumeratorのCurrentの終了を待つのではなくCoroutineの終了を待つ方法を試してみる。
IEnumerator Start() {
List<IEnumerator> ie = new List<IEnumerator>();
List<Coroutine> clist = new List<Coroutine>();
ie.Add(CoroutinA());
ie.Add(CoroutinB());
foreach (IEnumerator item in ie) {
clist.Add(StartCoroutine(item));
}
foreach (Coroutine c in clist) {
yield return c;
}
Debug.Log("FIN ALL " + Time.time);
}
うまくいった!
無事、すべてのコルーチン終了を確認してFIN ALLを表示することができた。
STARTをIEnumeratorにしない別のやり方
コールバックを使うとコルーチンの終了を検知できる
正確に言うと、終了を検知ではなく、終了したときに実行する処理を実装できる。
簡単でわかりやすい記事(Wixなのでいつか消えそう)
がっつり解説記事
using UnityEngine.Events;//これが必要だったはず
void Start(){
StartCoroutine(Test(()=>{
Debug.Log("FIN");//終了したときの処理を記述する
}));
}
IEnumerator Test(System.Action callback){
yield return new WaitForSeconds(1.0f);
callback();
}
記述方法にラムダ式が使われているため、ラムダ式初心者には難しいかもしれないが読める程度には理解しておくと便利
これをどう活かすか考えたとき、enumとの相性いいかな?と思う