コルーチンは、複数のフレーム間でコードを分割して実行することを可能にする特別なタイプのイテレーターメソッドです。時間遅延、非同期操作、順次実行のタスクを処理するために使用でき、メインスレッドをブロックすることなく実行できます。Unity のコルーチンの実装は、C# 言語が提供するイテレーター関連の言語機能に依存しているため、Unity のコルーチンの基礎原理を理解するには、まず C# のイテレーターの基本機能を理解する必要があります。
C# イテレーター#
イテレーターの基本概念#
-
イテレーターとは? イテレーターは、コレクションやシーケンスを簡素化して反復処理するためのツールです。複雑なループロジックを自分で書くことなく、コレクション内の各要素に逐次アクセスするために使用できます。イテレーターは、列挙可能なシーケンスを生成することで、要素を一つずつ取得できるようにします。
-
yield
キーワード。C# において、yield
キーワードはイテレーターの核心です。これは、停止と再開が可能なイテレーションプロセスを作成するのに役立ちます。yield
キーワードを使用することで、すべての要素を一度に生成するのではなく、シーケンス内の各要素を段階的に生成できます。yield return
:シーケンス内の 1 つの要素を返し、次のリクエストがあるまでイテレーターの実行を一時停止します。yield break
:シーケンスの生成を終了し、これ以上の要素を返しません。
C# イテレーターの役割#
C# イテレーター Enumerator は、任意のカスタムタイプを foreach で反復処理する手段を提供します。IEnumerable インターフェースと IEnumerator インターフェースを実装した任意のタイプは、foreach 文を使用してコレクションのようにオブジェクトを反復処理できます。
クラスを定義し、いくつかの学生で構成される:
public class Student
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}
public class ClassRoom : IEnumerable
{
private List<Student> students;
public ClassRoom()
{
students = new List<Student>();
}
public void Add(Student student)
{
if (!students.Contains(student))
{
students.Add(student);
}
}
public void Remvoe(Student student)
{
if (students.Contains(student))
{
students.Remove(student);
}
}
public IEnumerator GetEnumerator()
{
return new StudentEnumerator(students);
}
}
public class StudentEnumerator : IEnumerator
{
public StudentEnumerator(List<Student> students)
{
this.students = students;
}
private List<Student> students;
private int currentIndex = -1;
public object Current
{
get
{
if(0 <= currentIndex && currentIndex<students.Count)
{
return students[currentIndex];
}
return null;
}
}
public bool MoveNext()
{
currentIndex++;
return currentIndex<students.Count;
}
public void Reset()
{
currentIndex = -1;
}
}
ClassRoom クラス内の Student オブジェクトを反復処理するためにコードを書く必要がある場合、イテレーターを使用しないと、ClassRoom 内部の students コレクションを呼び出し側に公開するしかなくなります。これにより、Student オブジェクトのストレージの詳細が ClassRoom 内部に露出してしまいます。将来的に Student オブジェクトのストレージ構造が変更された場合(例えば、List 構造から配列や辞書に変更された場合)、対応する呼び出し側のすべてのコードも変更する必要があります。students メンバーを直接公開する以外に、ClassRoom が IEnumerable インターフェースを実装することで、foreach 文を使用して Student オブジェクトを反復処理できるようになります。
検証コード:
ClassRoom c = new ClassRoom();
c.Add(new Student() { Name = "zzz"});
c.Add(new Student() { Name = "yyy"});
foreach (Student s in c)
{
Debug.Log(s.ToString());
}
Debug.Log("......等価出力........");
//foreachの等価書き方
IEnumerator enumerator = c.GetEnumerator();
while (enumerator.MoveNext())
{
Debug.Log(((Student)(enumerator.Current)).ToString());
}
コンソール出力:
Unity コルーチン#
通常、私たちが書くコードの各部分は、Unity の更新ロジック内で同じフレームで全て実行されます。特定のコードのロジックを異なるフレームに分割して実行する必要がある場合、状態機械を手動で実装する以外に、Unity コルーチンを使用する方が簡単で便利です。一般的に、Unity コルーチンは、アプリケーション全体がシングルスレッドモードを維持しながら、コルーチン関数を記述し、コルーチンを開始するメソッド(StartCoroutine)を呼び出すことで、タスクを異なる時間帯に非同期に実行することを可能にします。
Unity はコルーチンの開始と停止のために 3 つのオーバーロードメソッドを提供しています。以下の表に示すメソッドは、コルーチンの開始と停止の使用法にそれぞれ対応しており、混用することはできません。
コルーチン開始メソッド | コルーチン停止メソッド |
---|---|
StartCoroutine(string methodName)/StartCoroutine(string methodName, object value) | StopCoroutine (string methodName) および StopCoroutine (Coroutine) |
StartCoroutine(IEnumerator routine)/StartCoroutine(IEnumerator routine) | StopCoroutine (Coroutine routine) および StopCoroutine (IEnumerator routine) |
Yield Return 遅延関数#
Unity コルーチンのコルーチン関数は、yield return の後にある WaitForSeconds、WaitForEndOfFrame などを使用して、何秒、何フレーム遅延してから実行するかを制御できます。このような効果はどのように実現されるのでしょうか?重要なポイントは、yield return 文の後のオブジェクトの型です。Unity コルーチンで一般的な yield return には以下のようなものがあります:
yield return new WaitForSeconds(1);
yield return new WaitForEndOfFrame();
yield return new WaitForFixedUpdate();
上記の 3 つの関数定義のソースコードに移動すると、すべてYieldInstructionを継承していることがわかります。Unity はyield returnで返されるオブジェクトの型に基づいて、次のコードを実行するまでの遅延時間を判断します。
まとめ#
Unity のコルーチンの実装原理は、C# 言語のイテレーター機能に基づいています。コルーチン関数を定義し(yield returnで返す)、コルーチン関数をIEnumeratorオブジェクトとしてキャッシュし、そのオブジェクトのCurrent(YieldInstruction オブジェクトまたは null)に基づいて次回の実行に必要な間隔時間を判断します。間隔時間が終了すると、MoveNextを実行して次のタスクを実行し、新しいCurrentに基づいて次回の待機時間を決定します。MoveNextがfalseを返すと、コルーチンが終了したことを示します。
以下のフローチャートで Unity コルーチンの実行プロセスを示すことができます:
自分で面白いコルーチンメソッドを実装する#
Unity コルーチンの実装原理を理解した後、Unity のStartCoroutineの効果を実現するために自分でコードを書くことができます。例えば、IEnumerator を返すコルーチン関数を受け入れる独自の開始コルーチンメソッドを作成します。このメソッドは、yield return の後に返される文字列の長さに基づいて、対応する秒数待機することを規定します。例えば、**yield return "1234"** の場合、4 秒待機してから次のコードを実行します。yield return "100"の場合、3 秒待機してから次のコードを実行します。yield returnの後のオブジェクトがstringでない場合、デフォルトで 1 フレーム待機してから実行します。前述の基礎をもとに、以下のようなコードを書くことができます:
/// <summary>
/// 作成したイテレーターオブジェクトを保存するためのもの
/// </summary>
private IEnumerator taskEnumerator = null;
/// <summary>
/// タスクが完了したかどうかを記録するフラグ
/// </summary>
private bool isDone = false;
private float currentDelayTime = 0f;
private float currentPassedTime = 0f;
private int delayFrameCount = 1;
private bool delayFrame = false;
private bool isCoroutineStarted = false;
private void MyStartCoroutine(IEnumerator enumerator)
{
if (enumerator == null) return;
isCoroutineStarted = true;
taskEnumerator = enumerator;
PushTaskToNextStep();
}
private void Start()
{
MyStartCoroutine(YieldFunction());
}
private IEnumerator YieldFunction()
{
//第一段コード
Debug.Log("first step......");
yield return 1;
//第二段コード
Debug.Log("second tep......");
yield return 2;
//第三段コード
Debug.Log("third step......");
yield return 3;
//第四段コード
Debug.Log("forth step......");
yield return 4;
}
private void PushTaskToNextStep()
{
isDone = !taskEnumerator.MoveNext();
if (!isDone)
{
if (taskEnumerator.Current is string)
{
currentDelayTime = (taskEnumerator.Current as string).Length;
currentPassedTime = 0f;
delayFrame = false;
}
else
{
delayFrame = true;
delayFrameCount = 1;
}
}
else
{
isCoroutineStarted = false;
}
}
private void Update()
{
if (isCoroutineStarted)
{
if (delayFrame)
{
delayFrameCount--;
if (delayFrameCount == 0)
{
Debug.Log(string.Format("第{0}帧(运行数:{1})结果:阶段任务已完成!", Time.frameCount, Time.time));
PushTaskToNextStep();
}
}
else
{
currentPassedTime += Time.deltaTime;
if (currentPassedTime >= currentDelayTime)
{
Debug.Log(string.Format("第{0}帧(运行数:{1})结果:阶段任务已完成!", Time.frameCount, Time.time));
PushTaskToNextStep();
}
}
}
}
コンソール出力は期待通りです:
面白いですね!