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

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

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

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

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

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

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

## WiFi接続

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

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

```c

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

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

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

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

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

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

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

```cpp
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の領域をファイルストレージとして利用できる機能があります。

```c
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を組み込むってどういうこと？
という人も多いような気がするので、そこから説明していきます。

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

```c
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ファイルのあるフォルダに丸っと解凍して

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

これだけです。

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

```diff
-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`で囲みます。
今回の例だと、コンストラクタ、デストラクタは公開したくないので、それ以外を囲っています。

```cpp: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++` コマンドを実行するだけです。

```bash
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の制限でファイル名に`＋`が使用できないのでその対応のため。）

そしたら

```cpp
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行**で実現できます。

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

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


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

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

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