Luaで超簡単にゲームプログラミングができて、Web-APIで実機転送ができちゃうゲーム機の裏側


ゲーム機についての表側についてはこちら
https://qiita.com/pluswing/items/021b8ddc0e88ad6f36ac

どーもこんにちは。
今回は、前回の続きで、そんなゲーム機のファームウェア作るにはどーしたらいいの?
ということろを解説していこうと思います。

思ったより簡単(ESP32の機能がスゴイ)なので、これを気に電子工作の世界に足を踏み入れる人が増えると良いなーと思ったりしています。
かくいう私もかれこれ電子工作は暇があればやっていますが、いまだに電子回路についてはよくわからないことが多く、ほぼ行き当たりばったりでやっています。
意外となんとかなるので(電子部品をぶち壊すことはよくある。たまに白煙が出て超焦るw)まぁとりあえずやってみるのが一番かなと思います。

さて、今回作成したゲーム機。「ESP Console」ですが、
Arduino で開発しているので、
組み込みだから、開発環境用意するの面倒なんでしょ?
WIndowsじゃ無いと開発環境ないんでしょ?
といったことは一切ありません。
Arduino IDEをインストールしたら、環境構築はほぼ完了です。
私はMacで開発しています。

技術的トピックとしては以下になるかなと思います。(恣意的抜粋ですが。。)

  • WiFi接続ができる
  • Web API 経由でスクリプトをアップロードできる
  • 組み込みなのにファイルシステムがある
  • Luaを組み込んだ

WiFi接続

ESP32にはWiFiモジュールが内臓されています。
ついでに言うと今回は使っていませんが、Bluetoothモジュールも入っています。

WiFiに接続するには、C言語で何百行と書かないといけないんでしょ?
いいえ、4行です。


WiFi.begin("<SSID>", "<PASSWORD>");
while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
}

はい。これだけ。ぶっちゃけ重要なコードは最初の1行で、あとの3行は接続されるのを待ってるだけです。
ね。簡単でしょ?

multicast DNSにも対応していて、(IPの代わりに、hoge.local とかでその端末にアクセスできるようにする機能)
それを有効にするのも超簡単です。

if (!MDNS.begin("<HOST NAME>")) {
    Serial.println("Error setup MDNS.");
}
MDNS.addService("http", "tcp", 80);

<HOST NAME>espconsoleを指定してあげると、
http://espconsole.local でこの端末にアクセスができるようになります。

Web API 経由でスクリプトをアップロードできる

Webサーバになるのは流石に面倒でしょう!?

AsyncWebServer webServer(80);
webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "running");
});
webServer.begin();

これはC++なのか・・・と疑いたくなるくらい簡単です。
ちなみに、これは外部ライブラリを使用しているのですが、
セットアップは規定のフォルダgit cloneするだけです。
https://github.com/me-no-dev/ESPAsyncWebServer

組み込みなのにファイルシステムがある

はい。ファイルシステム。
組み込みの世界では、ファイルシステムなんて贅沢なものは無いのが当然なのですが、
(普通はFlashにアドレス指定で読み書きする)
なんと、ESP32には SPIFFSというFlashの領域をファイルストレージとして利用できる機能があります。

SPIFFS.begin();
File f = SPIFFS.open("/.setting.json", "r");
char content[SETTING_BUFFER_SIZE];
f.readBytes(content, SETTING_BUFFER_SIZE);
f.close();

ちょっとCらしさが垣間見えましたが、普通にファイル名でアクセスができちゃいます。
これは便利すぎます。

ちなみにですが、SDカードのファイルも同じような感じで読み書きができてしまいます。

Luaを組み込んだ

はい。本命です。

Luaを組み込むってどういうこと?
という人も多いような気がするので、そこから説明していきます。

ちょっとコードを見た方が理解ができると思うので、こちらをご覧ください。

lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_dostring(L, "<Lua script code>");
lua_close(L);

1行目で、VMを作ります。
2行目で、Luaの基本ライブラリを読み込みます。
3行目で、文字列をLuaスクリプトとして評価・実行します。
4行目で、後始末します。

基本はこれだけです。
これを実現するには、
https://www.lua.org/download.html
ここから、一式ダウンロードして、
.inoファイルのあるフォルダに丸っと解凍して

extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

これだけです。

・・・とは行かないのが組み込みの世界。
ESP32の話になりますが、
C言語のsystem()関数がないので、loslib.cの該当箇所でエラーが出ます。

-static int os_execute (lua_State *L) {
- lua_pushinteger(L, system(luaL_optstring(L, 1, NULL)));
- return 1;
-}

・・・

static const luaL_Reg syslib[] = {
  {"clock",     os_clock},
  {"date",      os_date},
  {"difftime",  os_difftime},
- {"execute",   os_execute},
  {"exit",      os_exit},
  {"getenv",    os_getenv},
  {"remove",    os_remove},
  {"rename",    os_rename},
  {"setlocale", os_setlocale},
  {"time",      os_time},
  {"tmpname",   os_tmpname},
  {NULL, NULL}
};

こんな感じで、該当処理をまるっと消してしまえば良いです。
他の環境では、別のエラーが出ることがありますが、その環境に合わせて同等の処理に書き直すか、必要ないなら消してしまえば良いです。

LuaからCを呼び出す

ゲーム機なので、描画命令などC側の機能をLuaから呼び出す必要があります。
普通は、Cの関数を呼び出すために、↑で消したsystem()を呼び出すようにグルーコードを書いてあげないといけないんですが、
七面倒くさい。
ESP Consoleでは、tolua++というモノを使って、このグルーコードを自動生成しています。

C++のクラスをLuaで使う手順をざっくり。

まず、ヘッダファイルを用意して、公開したい宣言を
// TOLUA_BEGIN~//TOLUA_ENDで囲みます。
今回の例だと、コンストラクタ、デストラクタは公開したくないので、それ以外を囲っています。

game_api.h
// TOLUA_BEGIN
class GameApi
{
// TOLUA_END
public:
    GameApi();
    virtual ~GameApi();

// TOLUA_BEGIN

    int width();
    int height();
};
// TOLUA_END

次に、pkgファイルというものをを用意します。
これは$hfileに続いてヘッダファイル名を書けばOKです。

game_api.pkg
$hfile "game_api.h"

ここまで準備ができれば、あとは tolua++ コマンドを実行するだけです。

bin/tolua++ -o tolua_game_api.cpp game_api.pkg
sed -i -e s/tolua++.h/toluapp.h/ tolua_game_api.cpp

これで、tolua_game_api.cppというファイルが出来上がるので、
(sedしてるのは、Arduinoの制限でファイル名にが使用できないのでその対応のため。)

そしたら

lua_State *L = luaL_newstate();
luaL_openlibs(L);
tolua_game_api_open(L); // 追加!
luaL_dostring(L, "<Lua script code>");
lua_close(L);

とVMに追加でロードしてあげるだけです。
これで、クラスが使用可能になります。

今回の例だと、コンストラクタを非公開にしているので、このままではGameApiクラスをインスタンス化することができません。

ESP ConsoleではGetAPI()という関数も公開していて、シングルトンなGameApiのインスタンスを取得できるようにしています。↓
https://github.com/pluswing/espconsole/blob/master/game_api.h#L119

CからLuaのfunctionを呼び出す

単純な実行なら、luaL_dostring()を使えば良いのですが、
ESP Consoleは Lua側で定義されている update(), draw() functionを呼び出す必要があります。

この呼び出しは簡単で2行で実現できます。

lua_getglobal(L, "update");
lua_pcall(L, 0, 0, 0);

引数があるfunctionを呼び出す場合、若干コードが増えるのですが、引数なしの場合はこれだけで呼び出せてしまいます。

・・・ということで、ゲーム機のファームウェアをつくるに当たっての秘術的な話はこの辺で。
知ってしまえば、なんだそう言うことか。俺でもできそうだ!と思ったんじゃないでしょうか?

ぶっちゃけ、SSID選択画面つくったり、WiFiパスワード入力画面つくったりする方がよほど面倒なのですが。。

まだ夏休み真っ只中!電子工作に勤しんでみてはいかがでしょうか?