ホーム < ゲームつくろー! < Lua組み込み編


その2 Luaスクリプト事始め


 Luaが出たての頃、とにかく情報が少なくてどうして良いやらさっぱりわからなかった時がありました。しかし昨今ではネット上に非常に沢山のLuaの情報が置かれるようになりました。なので、いわゆるLuaなプログラム(if文とかループ文とか)についてはそういうサイトを参考にして頂ければと思います。その気になれば基本文法は2時間もあれば終わります。

 Lua組み込み編で注目したいのは、LuaのC言語とのやりとりの部分です。ここが出来て初めてゲームプログラムとの連携が取れるようになります。とは言え、何にも知らない状態からだと大変なのも確かなので、この章では簡単なプログラムを色々と作ってLuaに慣れて見ることにしましょう。



@ lua_Stateがすべての始まり

 Luaはテキストでプログラムを作成します。テキストはそのままですと文字列ですから、誰かが解釈する必要がありますね。その解釈してくれる人、環境を「Luaステート」と呼ぶことにします。作ったコードをLuaステートに投げると、コードが走り出し、変数や関数などを保持してくれます。このLuaステートさんの存在をしっかり感じるのが、Luaスクリプトを体得する上で非常〜〜に大切だということを、私は使いながら学びました(^-^;。

 例えば、Luaには「lua.exe」という単独実行環境が提供されています。このアプリを実行するとコンソールウィンドウが表示され、1行ずつ実行される(インタプリタ)Luaプログラムを打つ事ができるようになります。例えば、そのコンソール上で、

val = 100;

と打ってEnterキーを押すと、実際にvalに100が代入されます。これを表示させるには、

print( val );

とします。では先程のvalは誰が覚えていてくれたのか?それがLuaステートさんです。lua.exeの中身ではLuaステートさんが一人作られて、打たれたプログラムを1行ずつ解釈してくれていたというわけです。逆に言えば、Luaステートさんはプログラムを行単位でいつでも解釈できる能力があります。

 さて、ではLuaステートさんの存在を意識した上で、話をC言語側にしてみます。C言語側でLuaコードを解釈するためには「lua_State」というLua環境の管理者を通す必要があります。これが正にLuaステートさんです。lua_StateはLuaのすべてを握っていて、LuaコードとC言語とでデータのやり取りの仲立ちをしたり、内部で使用している変数を覚えてくれてたりします。ですから、実は「Luaコードが知らない変数や関数をC言語側で設定」出来ます。C言語側からLuaステートさんに「val = 100;」という変数を設定しておけば、いきなり、

print( val );

というLuaコードをその後で読み込んでもプログラムは正常に動いてしまうんです。ですから例えば、

DrawLine(100, 50, 300, 220);

というのがLuaスクリプト内にいきなり出てきても、C言語側で予めLuaステートさんに「DrawLine関数」を教えておけばちゃんと動きます。つまり、C言語側でLuaステートに下準備をしてあげれば(=環境を作ってあげれば)、スクリプト側でいくらでも好きな関数や変数を使えるわけです。この感覚、とっても大切です。

 その大元となるlua_StateはluaL_newstate関数で作り、使い終わったらlua_close関数で削除します。削除を忘れるとメモリ内にいつまでも残るので注意が必要です。以下はその最小限プログラムです:

Lua最小限プログラム
#include "stdafx.h"
#include <lua.hpp>

int _tmain(int argc, _TCHAR* argv[])
{
    lua_State *L = luaL_newstate();
    lua_close(L);

    return 0;
}

 lua_Stateオブジェクトの実体は構造体なのですが、この中身を触ることはまずありません。以後、あらゆるluaの関数にこのオブジェクトを渡していくことになります。尚、Luaは内部でガベージコレクトを採用しています。そのため、lua_Stateオブジェクト意外でオブジェクトの寿命を管理する必要はありません。



A lua_Stateは沢山作れる

 lua_Stateは唯一というわけではありません。作ろうと思ったらメモリの許す限り作れます。これは実は割と重要な事だったりします。Luaスクリプトファイルを2つのlua_Stateで別々に読みこめば、完全に分離した環境で同じスクリプトを動かす事ができます。ただ、lua_Stateを作るのもスクリプトファイルを読み込むのも多少時間がかかる処理でメモリも食います。沢山作れますが、毎フレーム作るとかは暴挙ですから気を付けて下さい。一度作ったLuaステートは必要に応じてとっておいて、再利用するのが良さそうに思います。

lua_Stateを沢山作る
#include "stdafx.h"
#include <lua.hpp>

// lua_Stateを沢山作る
void test_lua_state_create() {
    lua_State *stateAry[10];
    for (unsigned i = 0; i < 10; i++)
        stateAry[i] = luaL_newstate();

    for (unsigned i = 0; i < 10; i++)
        lua_close(stateAry[i]);
}



B スタック事始め

 Luaはスタックに始まりスタックに終わるほど「スタック」が超重要な鍵を握ります。そのスタックとは何か?これは値を積み上げた箱のようなイメージです。LuaはスクリプトとC言語側のやりとりの「すべて」をこのスタックで行います。これを理解しない限りはLuaを理解できません!図示するなら以下のような感じでしょうか:

 スタックは箱をどんどん積みます(push)。そして箱は上から外されます(pop)。この非常に単純な仕組みでLuaは値をやりとりするんです。

 このスタックはプログラムをする上で非常に大切なのですが、如何せん目に見えないので何ともイメージし辛いんですよね。そこで、現在積まれているスタックを見えるようにするヘルパー関数を一番最初に作ってしまうのを強くお勧めします。

 まず、スタックに値を積むには主にlua_push****系の関数を使います。Luaはスタックに積める型が厳格に決まっています。積めるものは全部で9種類です:

説明 定義 関数
nil 「空っぽ」の意味。値が無いのであり0ではない。 LUA_TNIL lua_pushnil
ブーリアン 真偽値。int型(0, 0以外) LUA_TBOOLEAN lua_pushboolean
軽量ユーザデータ voidポインタ LUA_TLIGHTUSERDATA lua_pushlightuserdata
数字 デフォルトではdouble型 LUA_TNUMBER lua_pushnumber
文字列 const char*型。終端記号'\0'が必要。 LUA_TSTRING lua_pushstring
テーブル 連想配列。Luaの核心! LUA_TTABLE lua_settable
関数 C言語の指定関数。Luaの肝! LUA_TFUNCTION lua_pushcfunction
ユーザデータ メモリブロック LUA_TUSERDATA lua_newuserdata
スレッド Luaステート(コルーチン) LUA_TTHREAD lua_pushthread

聞きなれない型もありますが、大抵はnil、ブーリアン、数字、文字列、テーブル、関数です。nilはC言語で言うNULLと同じような感覚で「何も無い物」を表します。ブーリアンと数字と文字列はまぁそのままです。テーブルはLua最大の魅力の一つで、Luaが規定する型であれば何でも格納できるコレクション配列です。関数はC言語の関数ポインタで、これを通してLua側からC言語の関数を呼べたりします。軽量ユーザーデータはvoid*型で、これがLuaでのオブジェクト指向に一役買います。一方ユーザデータはメモリブロックになります。スレッドについては使う機会が出た時に説明します。

 各スタックの型を知るにはlua_type関数を使います:

int lua_type(
    lua_State *L,    // スタックを保持しているステート
     int idx         // スタック番号
);

 第1引数にはスタックを持っているLuaステートを渡します。Luaステートはスタックを1つ持っています。
 第2引数のスタック番号は、型を知りたいスタックのインデックス(番号)を直接指定します。この指定の仕方こそがLuaのスタックを理解するとっても大切なポイントになります。

 スタックは値が入った箱がどんどん積み上げられていくイメージです。この時、「箱の一番下から」と「一番上から」という2つの指定方法が考えられます。Luaはこのどちらからの指定もできるようになっています。下の図を御覧下さい:

箱の一番下からは「+1, +2, ...」と正の値で番号が振られています。一方箱の一番上からは「-1, -2, ...」と下向きに負の値で位置が示されています。上の図で言えば-1も+5も同じ「IKD」という文字列が入ったスタックを示す事になります。同じスタックに対して2つの示し方があるのは、上から辿る事が便利なときも下から登る時が便利な時もあるためです。最初は「プラスとマイナスとどっちがどっちだっけ?」と迷うのですが、「下向きが負」もしくは「上向きが正」とどちらかの方向だけ覚えておけばすぐにイメージが固まると思います。

 では、スタックに積まれている型を表示するprintStackヘルパー関数を作ってみます:

スタック表示関数
// スタックを見る関数
void printStack(lua_State *L) {
    // スタック数を取得
    const int num = lua_gettop(L);
    if (num == 0) {
        printf("No stack.\n");
        return;
    }
    for (int i = num; i >= 1; i--) {
        printf("%03d(%04d): ", i, -num + i - 1);
        int type = lua_type(L, i);
        switch(type) {
        case LUA_TNIL:
            printf("NIL\n");
            break;
        case LUA_TBOOLEAN:
            printf("BOOLEAN %s\n", lua_toboolean(L, i) ? "true" : "false");
            break;
        case LUA_TLIGHTUSERDATA:
            printf("LIGHTUSERDATA\n");
            break;
        case LUA_TNUMBER:
            printf("NUMBER %f\n", lua_tonumber(L, i));
            break;
        case LUA_TSTRING:
            printf("STRING %s\n", lua_tostring(L, i));
            break;
        case LUA_TTABLE:
            printf("TABLE\n");
            break;
        case LUA_TFUNCTION:
            printf("FUNCTION\n");
            break;
        case LUA_TUSERDATA:
            printf("USERDATA\n");
            break;
        case LUA_TTHREAD:
            printf("THREAD\n");
            break;
        }
    }
    printf("-----------------------------\n\n");
}

 現在のスタック数を取得する直接的な関数はありませんが、lua_gettop関数がスタックの一番上のプラス番号を返してくれるので、これがそのままスタック数になります。Luaのスタック番号は1基底なので、列挙も1番から始まります。lua_typeで指定の番号のスタックの型(type)を取得し、対応する表示を行っています。型が数字だった場合はlua_tonumber関数でそのスタックに格納されている数字を取得して表示しています。同様に文字列だった場合はlua_tostring関数で文字列を取得しています。ブーリアンはint型として値を返してきますのでそれを"true"か"false"に変換しています。他の型は型名だけです。

 この関数を呼び出すテストプログラムは例えばこんな感じです:

スタック表示
// スタック表示
void test_stack_print() {
    lua_State *L = luaL_newstate();

    lua_pushboolean(L, 1);
    lua_pushnumber(L, 100.0);
    lua_pushstring(L, "Marupeke");

    printStack(L);

    lua_close(L);
}

これを実行すると次のようにスタックの中身が表示されます:

これであらゆるスタック操作を視覚的に見る事ができるようになりました。私の経験上ですが、スタックが視認できるようになるだけでLuaは相当にわかりやすくなります(^-^)

 Win32アプリケーションの場合はprintf関数が使えませんので、替わりにOutputDebugStringA関数を使うのが近道です。



C Luaにあるグローバル値をC言語に

 スクリプトとしての基本機能が「値の初期化」です。Luaにある数値をC言語に持ってくる方法は色々とありますが、一番簡単な方法はスクリプト内で「グローバル」として定義された値をC言語側で取得する方法です。

 まず、Luaスクリプトを書いてみます:

変数定義スクリプト
windowWidth = 640;
windowHeight = 480;
windowName = "Marupeke";

非常に簡単です。変数名に値を入れる、これだけ。LuaのスクリプトにはC言語のような「型定義」がありません。いきなり変数名を書いて値を代入できるんです。もし代入がなければ、その変数には「nil」が入っていると認識されます。

 この初期化変数を読み込んでみましょう:

スクリプトを読み込んで値を初期化
// 初期化その1
void test_init_01() {
    lua_State *L = luaL_newstate();

    // Luaファイルを開いて読み込み
    if (luaL_dofile(L, "InitParam01.lua")) {
        printf("%s\n", lua_tostring(L, lua_gettop(L));
        lua_close(L);
        return;
    }

    // Luaステートに読み込まれたグローバル変数をスタックに積む
    lua_getglobal(L, "windowWidth");
    lua_getglobal(L, "windowHeight");
    lua_getglobal(L, "windowName");

    printStack(L);

    // スタックを削除
    int num = lua_gettop(L);
    lua_pop(L, num);
}

 Luaファイルを読み込んで中身を解析するのがluaL_dofile関数です。第1引数には値を保持する管理人となるLuaステートを渡します。第2引数にLuaファイル名を渡します。拡張子が.luaとなっていますが、Luaファイルであれば名前は別に何でもかまいません。拡張子が無くてももちろんOKです。関数が成功すると0、失敗すると0以外の値が返ってきますので、エラー処理を行います。

 luaL_dofile関数が失敗するのは「ファイルがない」「Luaコードに間違いがある」場合です。この場合、スタックにエラー文字列が積まれます。それを文字列として表示すれば、具体的に何が起こったかを知る事ができます。Luaは突然終了する事が多いので、このエラー表示は大切です。

 読み込みと解析に成功したら、渡したLuaステートの中に「グローバル変数として」値が登録されます。これにアクセスするにはlua_getglobal関数を使います。第2引数に変数名を渡すとスタックにそれに該当するグローバル変数が積まれます。変数名は大文字小文字の区別があります。

 Luaはどのような値でもを一端スタックに積みます。lua_getglobal関数を呼ぶとスタックに値が乗せらます。指定の変数名がなければnilが乗ります。ですから先ほど作ったprintStack関数を呼べば、スタックに値が積まれたかを確認できます:

 例えばゲームの初期値などはこの方法で設定する事が可能です。Luaからパラメータを読む方法は他にも色々ありますが、この章は事始めという事でここまでにしておきます。後章でより詳しく見ていくことにします。



D Luaの関数をC言語から呼ぶ

 Luaはスクリプト上で独自の関数を作ることができます。これは例えば次のような感じです:

Lua内定義関数
function add(val1, val2)
    return val1 + val2;
end

Luaはfunction〜endで関数を定義するスタイルをとっています。これはVBなどと同じ方式です。C言語に慣れている人は上の関数定義に違和感を覚えてしまいます。上と全く同じ働きをするC言語版関数は次のようですよね:

C言語内定義関数
int add(int val1, int val2) {
    return val1 + val2;
}

 見比べると、まず戻り値の型がLua内関数にはありません。また引数の変数の型もありません。Luaは「型宣言無し言語」でして、代入された値によってその型をコロコロと変えることができるんです。そのため、戻り値や引数の型宣言が必要ないんです。「じゃ、じゃぁ、引数がval1=文字列、val2=数字も受け付けるの?」と疑問に思うわけですが、これ、ちゃんと受付けます。受け付けるのですが、実際に引き渡すと足し算の所でエラーになります。文字列 + 数字という演算が認められていないためです。Luaはインタプリタ型言語なので、構文が正しくてもプログラムが正しく動くかどうかはそこを通ってみないとわかりません。

 Lua関数には「複数の値を戻り値にできる」というC言語の関数を越える驚愕的な機能があります。例えば引数の値の加減乗除を返す関数を次のように定義できます:

Lua内加減乗除を返す関数
function calc(val1, val2)
    return val1 + val2, val1 - val2, val1 * val2, val1 . val2;
end

戻り値をコンマで区切ると、いっぺんに複数の値を返せるんです。

 さて、ここにC言語との親和性をゆるがす問題が出てきます。C言語の関数は複数の引数を取れますが、複数の戻り値を受け取れません。つまり、Luaで定義された関数を「C言語のスタイル」では呼べないんです。この違いを吸収するのがスタックです。Luaの関数から戻される値をスタックに積んでもらえばいいんです。C言語側ではそのスタックから値を貰うようにすれば、相互のやり取りが成立します。

 Luaの関数をC言語から呼ぶには、lua_getglobal関数で関数名を指定します。この段階でスタックにその関数を表す値が積まれます。次にLua側に渡す引数の値をC言語側で第1引数から積んでいきます。後はlua_pcall関数で関数名を指定してあげればLua側の関数が実行されます。関数が実行される時、スタックに積んだ物はlua_pcall関数を呼んだ時にスタックから取り出され、替わりに戻り値がスタック積まれます。

 この辺りの挙動をサンプルで確かめてみましょう:

CalcFunc.lua
function calc(val1, val2)
    return val1 + val2, val1 - val2, val1 * val2, val1 / val2;
end
C言語内からLuaのcalc関数を呼ぶ
void test_callLuaFunc() {
    lua_State *L = luaL_newstate();

    // Luaファイルを開いて読み込み
    // これでLuaステートに関数が登録されます
    if (luaL_dofile(L, "CalcFunc.lua")) {
        printf("%s\n", lua_tostring(L, lua_gettop(L));
        lua_close(L);
        return;
    }

    // Luaステート内にある"calc関数"を指定
    lua_getglobal(L, "calc");

    // 引数を設定
    lua_pushnumber(L, 100);
    lua_pushnumber(L, 200);

    printStack(L);

    // Lua関数を実行
    if (lua_pcall(L, 2, 4, 0)) {
        printf("%s\n", lua_tostring(L, lua_gettop(L));
        lua_close(L);
        return;
    }

    printStack(L);

    // 複数ある戻り値を取得
    float add_Res = (float)lua_tonumber(L, 1);
    float sub_Res = (float)lua_tonumber(L, 2);
    float mult_Res = (float)lua_tonumber(L, 3);
    float dev_Res = (float)lua_tonumber(L, 4);

    // スタックを削除
    int num = lua_gettop(L);
    lua_pop(L, num);

    lua_close(L);
}

 Luaファイル側のcalc関数は先に説明した通りで、引数を2つ取り、戻り値が4つあります。
 C言語側がメインです。まず関数が定義されているLuaファイルを読み込んで解析し、Luaステート内にcalc関数の情報を登録します。これはluaL_dofile関数が担ってくれます。登録に成功したら、C言語側でLuaの関数をlua_getglobal関数で直接指定します。これで指定された関数がスタックに積まれます。関数もLuaにとっては値なんです。

 次に関数に渡す引数を積んでいきます。これは第1引数から順番に積みます。ここまでのスタック具合を表示してみるとこんな感じです:

ちゃんと「関数、引数1、引数2」の順番に下から積まれていますよね。この状態でlua_pcall関数を呼びます:

int lua_pcall (
    lua_State *L,    // Luaステート
    int nargs,       // 引数の数
    int nresults,    // 戻り値の数
    int errfunc      // エラーハンドラ関数のスタックID
);

第1引数は関数が登録されているLuaステートです。
第2引数は先に指定した関数の引数の数を渡します。
第3引数は関数から戻される戻り値の数を指定します。
第4引数は独自のエラーメッセージを取り扱う時に使いますが、デフォルトで良い場合は0を渡します。

この関数が成功するとスタックに積まれていた「関数、引数1、引数2」はスタックから取り除かれます。正しくはlua_pcallで指定した引数の数+1だけ無くなります。関数から戻ってきた時には、lua_pcallで指定した戻り値の数分だけスタックに戻り値が積まれます。後は、スタックの値をC言語側で取り出すだけです。

 上のサンプルを実行すると次のような結果が得られます:

加減乗除の結果がきちっとスタックに積まれているのがわかりますね。

 Lua側で定義した関数をC言語側で呼べると言う事は、C言語の仕事の一部をLua側に委託できるという事です。スクリプトっぽくなってきました。

 ざざっとLuaとC言語の連携を見てきました。次章はLuaのもう一つの素晴らしい機能「コルーチン」のお話をします。