文書の過去の版を表示しています。
フルーツフィールド for M5 Stack / M5 Core2
概要
フルーツフィールドの勝手移植第二段。 M5 Stack に BLEキーボードをつないだら、何か、アクション性のあるものも作ってみたくなりました。 でないと、キーボードにオートリピートをつける動機づけができないので。
そんなわけで、アクション性、というほどのものではないが、パズルは程よい題材だということで、フルーツフィールドを移植することに。
移植の方針
PC-6001mkII版に使った、PC-8001版のキャラクターをベースにしたものを今回も流用することに。 ただし、PC-6001mkII版は、PC-6001mkIIのVRAMに合わせたデータ構造にしてしまったので、これを逆に汎用の形に戻して、改めてRGB332 1)にすることに。
ハイハイスクールアドベンチャー経験から、スプライトが使えるといってばかばか使っているとあっという間にメモリを使い切ってしまうので最初から8bppで、しかも、8×8のキャラクターを16×16に引き伸ばして表示する方向でさらにメモリをケチることに。
音は、本格的なサウンドドライバーを作ったりしないで、単純に割り込みの中で、M5.Speaker.tone() を使って適当に鳴らすだけにします。 この割り込みを使うのも今回のテーマの一つです。
あとは、BLEキーボードハンドラに、オートリピートを持たせること。
とりあえず、コンストラクションモードは省いて、したがって、コンストラクションデータの保存と読込もとりあえずは省いていくことにします。
以下、実装にあたってのあれこれを書き綴ります。
画面
M5 Stack/M5 Core2 は 320×240 の 16bppの画面を持っているので、160×100 の PC-8001 由来のゲームを移植するのには不足はありません。
方針のところにも書いたように、キャラクターは8×8でM5GFX のスプライトとして保持します。 描画するときに、いずれかの段階で16×16にストレッチすることにします。 ストレッチのコストはありますが、メモリをけちるのはこの環境では大切です。
キャラクタを16×16で表示することにすると、ゲームマップは 16×10キャラクター構成なので、256×160 pixels の画面領域を占有します。
一応、M5 ATOM + 外部液晶(240×240)も視野に入れているので、拡大縮小は必須でもあります。
なお、タイトルロゴは、PC-8001版からの流用ではなく、Google Geminiに作らせたものを使っています。
割込
音
キーボード回り
デモ
スクリーンショット
M5Stackにせよ、なんにせよ、スクリーンショットを撮る機能は搭載されていないので、スクリーンショットが欲しかったら、それをプログラムに組み込むしかない。
めんどくさいファイル形式はコードがデカくなるだけなので、BMP形式で出力するものとする。 めんどくさいからパレットも省略して2)、M5.Displayの表示している16bppの画像をがさっと書き出す。 大したことないだろうと思ったら、ちょっとだけめんどくさかった。
ヘッダを作れ
BMP形式のファイルは、ファイルヘッダ+情報ヘッダ+パレット情報+画像データという構成になっている。 なので、ヘッダをつけてやればBMP形式ファイルの出来上がりである。 そう。キモはヘッダなのだ。
ヘッダの構成は次のようになっているらしい。
ファイルヘッダ
| オフセット | サイズ | 名称 | 内容 | 
|---|---|---|---|
| +0x0000 | 2 | bfType | ファイルタイプ 'BM' (0x4d42) | 
| +0x0002 | 4 | bfSize | 画像サイズ in bytes | 
| +0x0006 | 2 | bfReserved1 | 予約領域(0) | 
| +0x0008 | 2 | bfReserved2 | 予約領域(0) | 
| +0x000a | 4 | bfOffBits | ファイルの先頭から画像データまでのオフセット in bytes パレットがなければ 14 + 40) | 
情報ヘッダ
| オフセット | サイズ | 名称 | 内容 | 
|---|---|---|---|
| +0x000e | 4 | biSize | 情報ヘッダサイズ 40 (OS/2だと違うらしい) | 
| +0x0012 | 4 | biWidth | 画像幅 in pixels | 
| +0x0016 | 4 | bcHeight | 画像高 in pixels … 正数の場合画像データは下から上に向かって、負数なら上から下向かって並ぶ | 
| +0x001a | 2 | bcPlanes | プレーン数 (1) | 
| +0x001c | 2 | bcBitCount | 画素当たりのデータサイズ in bits | 
| +0x001e | 4 | biCompression | 圧縮形式 無圧縮なら 0 | 
| +0x0022 | 4 | biSizeImage | 画像サイズ in bytes / bfSizeと同じ | 
| +0x0026 | 4 | biXPixPerMeter | X方向の画像密度 2インチモニタのM5Stackでは200dpiで7874になる(はず) | 
| +0x002a | 4 | biYPixPerMeter | Y方向の画像密度 上に同じ | 
| +0x002e | 4 | biClrUsed | 格納されているパレット数(0) | 
| +0x0032 | 4 | biClrImportant | 重要なパレットのインデックス(0) | 
定義はシンプルだし、大してめんどくさくなさそうだけれど、いくつか注意しないといけないポイントがある。
エンディアン
ESP32は Little Endianである。 なんで、'BM'という文字コードを格納しようとして、bfType = ('B' « 8) | 'M'; とかすると、'MB'の順に格納されてしまう。 0x424d としても同じこと。
定義にそって、uint16_t bfType とするより、uint8_t bfType[2] として、'B', 'M'の順に格納する方がいいかもしれない。
アラインメント
昔からそうだけれど、CPUがメモリーにアクセスするとき、アラインメントに沿っていないとアクセスが遅くなったり、場合によっては、取り出しが分割されたりするので、気の利いたコンパイラだと、構造体の定義は、いい感じにアラインメントされるように処理される。
すると、ヘッダのデータが狙った場所に出てこなくなる。
なので、構造体を使ってヘッダーを作成するなら、ヘッダーを詰めて配置するようにコンパイラに指示しなければならない。 大体が、組込みプロセッサであるESP32の場合、デフォルトがこっちでいいんじゃないかという気もするんだが。
    struct __attribute__((__packed__)) BMP_File_Header {
        uint16_t bfType;
        uint32_t bfSize;
        uint16_t bfReserved1, bfReserved2;
        uint32_t bfOffBits;
    };
    struct __attribute__((__packed__)) BMP_Info_Header {
        uint32_t biSize;
        uint32_t biWidth;
        uint32_t biHeight;
        uint16_t biPlanes;
        uint16_t biBitCount;
        uint32_t biCompression;
        uint32_t biSizeImage;
        uint32_t biXPixPerMeter;
        uint32_t biYPixPerMeter;
        uint32_t biClrUsed;
        uint32_t biClrImportant;
    };
なんで、こんなことを書いているかというと、まさにこれがゆえに、作成されたBMPファイルは「サポートされない形式」っていわれてしまったのだから。
ファイル名
これはスクリーンショットと直接関係はないのだけれど、ファイル名を自動的に生成できるようにしておいた方が、多分便利。 RTCが入っていれば、yyyymmdd_hhmmss とかを入れておけば、一秒以内に立て続けにスクショされない限りはユニークなファイル名が補償できるが、M5StackにRTCがつながっているかどうかわからない。 というか、多分使われていないだろう。
スクショを撮る関数はファイル名を受け付けるようにしておくが、なかったら、screenshot_0000.bmp から始めて、9999まで10000枚の画像を保存できるようにしておこう.
4桁にしたことに深い意味はない。 5桁や6桁が良ければそのようにプログラムを改変すればいいだろう。
char ScreenShot::_filename[PATH_MAX] = "/screenshot_0000.bmp";
const char *
ScreenShot::get_next_filename()
{
    char num[5];
    strncpy(num, strchr(_filename, '_') + 1, 4);
    int n = strtol(num, nullptr, 0);
    while (SD.exists(_filename))
    {
      if (++n > 9999) n = 0;
      snprintf(num, 5, "%04d", n);
      strncpy(strchr(_filename, '_') + 1, num, 4);
    }
    return _filename;
}
スクリーンショット本体
あとはヘッダを整えて、画像データをくっつけるだけである。
void
ScreenShot::take(const char *filename)
{
    if (filename == nullptr)
    {
        filename = get_next_filename();
    }
   
    _bf = {
        ('M'<<8)|'B',
        (uint32_t)(M5.Displays(0).width() * M5.Displays(0).height() * (M5.Displays(0).getColorDepth() / 8)),
        0,
        0,
        14 + 40,
    };
    _bi = {
        40,
        (uint32_t)M5.Displays(0).width(),
        (uint32_t)M5.Displays(0).height(),
        1,
        M5.Displays(0).getColorDepth(),
        0,
        (uint32_t)(M5.Displays(0).width() * M5.Displays(0).height() * (M5.Displays(0).getColorDepth() / 8)),
        7874,
        7874,
        0,
        0,
    };
    uint16_t lineBuf[M5.Displays(0).width()];
    uint8_t b = M5.Displays(0).getBrightness();
    M5.Displays(0).setBrightness(20);
    File fp = SD.open(filename, FILE_WRITE);
    if (fp)
    {
        fp.write((uint8_t*)&_bf, sizeof(_bf));
        fp.write((uint8_t*)&_bi, sizeof(_bi));
        for (int y = M5.Displays(0).height() - 1 ; y >= 0 ; y--)
        {
            for (int x = 0 ; x < M5.Displays(0).width() ; x++)
            {
                lineBuf[x] = M5.Displays(0).readPixel(x, y);
            }
            fp.write((uint8_t*)lineBuf, sizeof(lineBuf));
        }
        fp.close();
    }
    M5.Displays(0).setBrightness(b);
}



