コルーチンを使おう!

まえがき

 AmusementCreatorsアドベントカレンダー2018の11日目の記事です。
 Qiitaに投稿しようと思ったら、なぜかメアドの認証がうまくいかなかったので、はてなブログで書くことにしました。また一つアカウントが増えてしまった…

 前置きはさておき、本題。ゲームプログラミングで、(比較的)簡単にアニメーションやらを作りたい。面倒くさがらずに、こだわって見た目をいい感じにしたい、そんな人のためのお話です。
とりあえずコルーチンについて、自分のゲーム制作の余裕があれば後でモデルビュー分離とイベントの話なんかもそのうち加筆したい…
 また、基本的に、使い方がさっぱりわからんという人向けの記事です。(間違ったことを書いている可能性もあるので)使い方が分かったら、是非自分で調べて理解を深めることをお勧めします。
 想定環境はC#+Altseedです。

コルーチン(coroutine)の基本

 まず、一番(?)重要なこととして、調べるとたくさん出てくるUnityのコルーチンとは、別物(って言うほどではない)だと思ってください*1。Unity向けコルーチンの記事は参考にはなりますが、そのまま使おうとしても動きませんので注意(自分はそれでつまづいた)

 では、コルーチンとは何ぞや?って人のために、最低限の説明から。

コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。
Wikipediaより引用

はい、専門用語が多くて、わかりにくいですね。

 重要なのは、"いったん処理を中断した後、続きから処理を再開できる。"という部分だけです。
簡単なコードで説明しましょう。

IEnumerator<int> GetCoroutine()
{
    for (int i = 0; i < 100; i++)
    {
        yield return i;
    }
}

 こんなメソッドがコルーチンです。宣言するには戻り値をIEnumeratorにして、関数の中でyield returnを使えばいいだけです。(後述する、実際の使い方とは異なりますが、)まずは感覚的な動作イメージから説明します。

 このメソッドを呼び出すと、コルーチンでない普通のメソッド(yieldとかが無い)と考えれば、5行目のreturnで関数が終わって、i=0が帰ってきて終了すると思います。これはコルーチンであっても同じです。
 しかし、次にメソッドが呼ばれた時が違います。普通のメソッドならば、次に呼ばれた時もメソッドの初めから実行されるので、戻り値は0です。何度呼んでも0。
ですが、コルーチンの場合、yield returnした行から再開するので、次の戻り値は1となります。その後も、呼ぶたびに2,3…となっていき、最終的に100回呼んだら終わりで、それ以上は何も起こりません。

と、まあこれがイメージですが、こう書くと、
「ああ、そういうことね。コルーチン完全に理解したわ」
と、OnUpdateの中でそのままコルーチンを呼ぼうとします。
動きません。
たぶん、コルーチンでつまづくとしたら、このあたりの使い方に関してが一番だと思います。

正しい使い方

実際の使い方はこんな感じ

using System;
using System.Collections.Generic;

namespace MyGame
{
    class CoroutineObject : asd.TextureObject2D
    {
        /// <summary>
        /// 消滅までの時間(フレーム)
        /// </summary>
        const int lifetime = 60;

        IEnumerator<int> coroutine;
        
        protected override void OnAdded()
        {
            base.OnAdded();

            coroutine = GetCoroutine();
        }

        protected override void OnUpdate()
        {
            base.OnUpdate();
            coroutine?.MoveNext();
        }

        IEnumerator<int> GetCoroutine()
        {
            for (int i = 0; i < lifetime; i++)
            {
                Position += new asd.Vector2DF(1, 0);
                yield return i;
            }
            Dispose();
        }
    }
}

生成されてから、毎フレームx座標を1ずつ動いて行って、60フレーム経ったら消滅するだけの、何の価値があるのかわからないオブジェクトです。
クラスのフィールド(メンバ変数)として、IEnumerator型の変数coroutineを定義しておき、OnAdded()での初期化時にその変数にGetCoroutine()の戻り値を代入。そしてOnUpdate()で更新されるたびに、coroutine?.MoveNext()*2としています。ざっくり言えば、このMoveNext()される度に、GetCoroutine()が呼ばれると思えばいいです。*3*4*5
どうでしょう?コルーチンの使い方が何となくわかりましたかね?後は、実際に自分で動かしてみたり、調べたりすれば、たぶん理解できると思います(それでも、わからなければ誰かに聞きましょう)。


さて、ですがまだ、「それ、わざわざコルーチンで書く意味ある?intでタイマー作れば十分じゃない?」と、こう思うかもしれません。
正直、上記の例程度なら、タイマーを使用する場合とコルーチンを使う場合で、フィールドの数も変わりませんし、それでも問題ありません。
コルーチンが真価を発揮するのは、より複雑なアニメーションをさせたいときなどです。

複数の動作を順番に実行!

例えば、60フレーム右移動してから、120フレーム下に移動するオブジェクトを作りたい。そんなことってありませんか?
メニューの選択肢が選ばれたら、最初に拡大してから、その後点滅させたい。そんなことってありませんか?ありますよね。
無いって人は、これを機に、こだわったアニメーションを作りましょう。

そしてこういった複数の動作を順に実行させたいようなときにコルーチンは便利なわけです。

        IEnumerator GetCoroutine()
        {
            for (int i = 0; i < 60; i++)
            {
                Position += new asd.Vector2DF(1, 0);
                yield return null;
            }
            for (int i = 0; i < 120; i++)
            {
                Position += new asd.Vector2DF(0, 1);
                yield return null;
            }
            Dispose();
        }

こうするだけです。さらにこの後処理を付けたすのも簡単ですし、処理の順番通りに書くだけなので、処理の流れが分かりやすいです。
また、アニメーションで使用するタイマーなどの変数は、メソッドの外から利用することは少ないので、それらをローカル変数として狭いスコープで扱え、コードがきれいになるのも利点です。
タイマーで実装しようと思ったら、OnUpdate内に、ifが増えたり、状態変数を作ってswitchで分岐したり、もう考えるだけで面倒くさいですね。

状態変数としてのコルーチン

coroutine変数自体を状態変数のように使うこともできますから、(わかりやすいかはさておき)こんなこともできます。

        IEnumerator Wait()
        {
            /*
            ここで待機アニメの初期化
            */
            while (true)
            {
                if(asd.Engine.Keyboard.GetKeyState(asd.Keys.Up == asd.KeyState.Push)){
                    coroutine = Jump();
                    break;
                }
                /*
                ここで待機中のアニメーションを動かす
                */
                yield return null;
            }
            /*
            ここで待機アニメの終了処理
            */
        }

        IEnumerator Jump()
        {
            isJumping = true;
            velocity = new asd.Vector2DF(0, 10);
            /*
            ここでジャンプアニメの初期化
            */
            while (isJumping)
            {
                velocity -= new asd.Vector2DF(0, -0.5);
                /*
                ここでジャンプ中のアニメーションを動かす
                */
                yield return null;
            }
            /*
            ここでジャンプアニメの終了処理
            */
        }

このように、状態ごとにIEnumeratorを返すメソッドを用意して、状態遷移の際にcoroutineに戻り値を代入してやることで、OnUpdate()では状態に関係なくMoveNext()するだけで状態ごとの処理を実行できます。
遷移時の状態ごとの初期化や終了処理も含めてまとめて一か所に記述することができ、アニメーションで使用するテクスチャの切り替えや、色の変更などをしやすいのは利点だと思います。

あとがき

基本的なコルーチンの使い方はこんなところです。
他にも、応用的にできることはいろいろあり、戻り値はジェネリックなので、もちろん好きな型を使うことができますし、yield breakを利用して中断することや、
IEnumeratorでなく、IEumerableを返すようにして、foreachと組み合わせて、コルーチンの中で呼ぶようなこともできます。
基本さえわかってしまえば、それらは調べれば充実した情報も出てくるので、ここでは割愛します。


コルーチンに関する説明は以上です。コルーチンを使うとアニメーションのコードを書くのは確実に楽になります。
そして、アニメーションを充実させると、見栄えが良くなって、自分で見た時も、誰かに見せた時もモチベが上がります。
モチベが上がるとゲームの進捗は無限に生まれます。
是非コルーチンを完全に理解して、進捗を生みましょう。
(え?もうコルーチンとか知ってるからモデルビューの話をしろ?誰か無限に時間をください)

*1:MoveNext()的なことを、Unityはエンジン側でたぶん勝手にやってくれています。Altseedなどでコルーチンを利用する場合は、自分でする必要があるから、そのままじゃ動くわけないですね。

*2:?.という記法はNull条件演算子と言って、coroutineがNullだった場合、MoveNextせずに終了します。知らなかった人はNull合体演算子などと合わせて調べてみると幸せになれるかもしれません。

*3:戻り値であるiの値を、呼び出し側で使いたい場合は、coroutine.Currentで取得できます。

*4:終わったことを判定したい場合、MoveNext()の戻り値がfalseになったかで判定できます。

*5:そもそも戻り値とか使わないから必要ない、という場合は、IEnumeratorをIEnumeratorとして、yield return null;とすることもできます。その場合、using System.Collections;を忘れずにどうぞ。