その3 コルーチンで状態遷移をLuaで制御
Luaが持つ素晴らしい機能の一つに「コルーチン(coroutine)」があります。コルーチンは「関数の途中で戻ってきて、続きからまた実行出来る」という仕組みです。
こう聞いても「ん?」とハテナが付くと思います。私もそうでした。C言語にはコルーチンの機能がありません。なので、C言語に慣れている人だとピンと来ないんですよね。
例えば、キャラクタの位置を連続的に動かす事を考えてみます。最初10フレームは右へ1ステップずつ動き、次の5フレームで上へ、さらに次の7フレームで左下に移動するとしましょう。これをC言語でベタに書くとこんな感じになります:
キャラクタ移動プログラム(C言語) int state = STEP_1;
int curFrame = 0;
int step() {
switch(state) {
// 右へ10フレーム1ステップ
case STEP_1:
stepRight();
if (curFrame == 10) {
curFrame = 0;
state = STEP_2;
}
break;
// 上へ5フレームステップ
case STEP_2:
stepUp();
if (curFrame == 5) {
curFrame = 0;
state = STEP_3;
}
break;
// 左下へ7フレームステップ
case STEP_3:
stepLeft();
stepDown();
if (curFrame == 7) {
curFrame = 0;
state = STEP_END;
}
break;
}
}
このstep関数を毎フレーム呼ぶと、内部で現在の状態(state)に対応した移動関数が呼ばれキャラクタを動かします。関数ポインタやクラスを用いればもう少しスマートに書くことができますが、何と言いますか直感的ではない感じがします。それはC言語が「呼び出した関数は必ず頭から始まる」という性質を持っている事にあります。つまり、step関数を呼び出すと必ず最初から始まってしまうため、今の状態にあった箇所へswitch等の条件構文を使ってジャンプしないといけないわけです。
もし、上のコードを次のように書けたら幸せに感じませんでしょうか?
キャラクタ移動プログラム(幸せな書き方) int step() {
right(10); // 右へ10フレーム1ステップ
up(5); // 上へ5フレームステップ
leftdown(7); // 左下へ7フレームステップ
}
void right(int frm) {
for (int i = 0; i < frm; i++)
go_right();
}
一番最初にstep関数を呼び出すとright関数が呼ばれます。この関数は引数のフレーム数だけgo_right関数(右へ移動させる関数)を呼び出します。go_right関数は1回呼び出される度に「一時停止して制御を呼び出しの大元に戻す」という素晴らしい機能を保持しているとします。そのため、go_right関数が処理された後、forループに続かずにstep関数がいきなり終了します。
もう一度step関数が呼ばれた時、今度は関数の先頭ではなく、先ほど一時停止した続きからプログラムが動き出します。つまりforループの続きから始まるわけです。2回目のgo_right関数が呼ばれると、同じように一時停止して制御がstep関数の呼び出し元にすぐに戻ります。ですから、step関数を10回呼ぶまではgo_right関数が1回ずつ処理されるんです。up関数やleftdown関数も同じように振舞えば、キャラクタを1ステップずつ思った通りに動かせます。
幸せな書き方の方は、元のC言語側に比べ素直に状態遷移が書けていますよね。「右行け、上行け、左下行け〜」です。こういう書き方をできるようになるには、言語仕様として「関数の途中で一時停止して制御を呼び出しの大元に戻し、次は続きからできる」という機能が必要なんです。そう、これこそ正に「コルーチン」なんです。
この章では、こんなご機嫌な機能であるコルーチンをLua側で設定し、それをC言語側で呼び出し制御する方法を見ていく事にします。
@ Luaプログラムにコルーチンを
コルーチン処理はLua側のプログラムに仕込みます。非常に簡単なサンプルを作ってしまいましょう:
文字列を次々に返すLuaコード function step()
coroutine.yield("そこは広場だった。");
coroutine.yield("小さな滑り台があった。");
coroutine.yield("昔ここで良く遊んだ事を思い出した。");
end
step関数を呼ぶたびに上の3行の文字列を一つずつ取り出すのが目的です。coroutine.yield関数に注目です。この関数はLuaに用意されているライブラリ関数の一つで、そこで一時停止して状態を保存し、処理をすぐに返してくれます。関数の引数には処理を返す時の戻り値を渡します。もちろん戻り値が無くてもいいですし、複数の戻り値があっても大丈夫です。
このコルーチン用の関数を呼び出すC言語のサンプルがこちら:
コルーチン処理(C言語側) // コルーチンテスト
void test_coroutine() {
lua_State *L = luaL_newstate();
// コルーチンを使えるようにライブラリをLuaステートに設定する
luaopen_base(L);
// Luaファイルを開いて読み込み
// これでLuaステートに関数が登録されます
if (luaL_dofile(L, "Coroutine.lua")) {
printf("%s\n", lua_tostring(L, lua_gettop(L));
lua_close(L);
return;
}
// コルーチン(スレッド)を作成
lua_State *co = lua_newthread(L);
// コルーチンステート内にある"step関数"を指定
lua_getglobal(co, "step");
// ステップ実行
while(lua_resume(co, 0)) {
printStack(co);
_getch();
}
lua_close(L);
}
幾つかポイントがあります。まず、コルーチンを使えるようにするにはluaopen_base関数を呼び出す必要があります。この関数は基本的なライブラリをLuaステートにセットする関数です。デフォルトではLuaステートは最低限のライブラリしか使えない状態になっています。コルーチンはその範囲外であるためluaopen_base関数の呼び出しが必要になります。次にいつものようにLuaファイルを開き解析してもらいます。
次からがコルーチンの肝。まず、Luaファイルを読み込んだLuaステートからlua_newthread関数を通してコルーチンステートを貰います。これは言わば最初のLuaステートの分身。lua_newthread関数を呼ぶ度に分身が作れます。でも、お互いのスタック状態は独立しています(ただしグローバル変数(関数)は共有)。
コルーチンステートには最初のLuaステートに登録されているLuaファイル内のstep関数が共有化されているため、これをlua_getglobal関数で呼び出して自分のスタックに積むことができます。その関数を実行するにはlua_resume関数を用います。第1引数はコルーチンステート、第2引数はLuaに渡す引数の数です。lua_resume関数を呼び出すと先に設定したstep関数が実行されるのですが、coroutine.yieldで止めた次から再生され、次のcoroutine.yield関数もしくは関数の最後まで実行されます。これにより、lua_resume関数をどんどん呼ぶと、一つの関数内がちょっとずつ実行されるわけです。
先に登録したstep関数には3行のcoroutine.yield関数があり、その戻り値が表示させたい文字列になっています。それはスタックに積まれますので、printStack関数(※注:独自の関数です。実装はこちら)で表示させるとその文字列が1文字ずつお目見えします。スタックに積まれた変数は、次にlua_resume関数が呼ばれた時に戻り値としてスタックされた数だけ取り除かれます。
コルーチンの基本はこれくらいのものでして、扱うのも割と簡単です。…あれ?これで終わりかもです(^-^;。また何か思いついたら続きを書きます、はい(^-^;;;