コルーチンは、複数のフレーム間でコードを分割して実行することを可能にする特別なタイプのイテレーターメソッドです。時間遅延、非同期操作、順次実行のタスクを処理するために使用でき、メインスレッドをブロックすることなく実行できます。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 コレクションを呼び出し側に公開する必要があり、これにより ClassRoom 内部の Student オブジェクトのストレージの詳細が公開されます。将来的に 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();
}
}
}
}
コンソール出力は期待通りです:
面白いですね!