🕹️

M5Stack でゲームを作る 001

2022/05/05に公開

注意

開発環境整備やライブラリ導入方法などはここでは触れません。ご容赦ください。

記事概要

M5Stack で動作するゲームを goblib、goblib_m5s で構築していきます。
ある程度のプログラム知識、M5Stack に関しての知見が必要です。

M5Stack とは?

M5Stack は M5Stack社による小型マイコンモジュールで、様々なタイプのものが発売されています。
ここでは最も基本的な M5Stack BASIC/Gray + Faces Bottom + Gamepad Panel の構成下でゲームを作っていきます。

Gamepad Panel は十字キーとA/Bボタン、SELECT/STARTボタンがあり、ゲームボーイ相当の操作が可能です。

M5Stack 性能概略

16bitRGBカラーLCD(解像度 320 x 240)、スピーカ搭載、SDカードからの読み書きが可能という事で、ゲーム作成においては必要な機能を満たしています。
ただしハードウェアによるBGやスプライト等の描画や、3D関連機能等ゲーム専用機に搭載されているような機能は一切持っていません。サウンド系他も同様で基本的には自分で全てなんとかしなくてはいけない、そういうハードです。
古の8bitパソコンやPC-9801シリーズでのゲーム開発に似た感じと思ってよいでしょう。(なお筆者はその時代での開発経験がある年寄りでございます(´・ω・`) )

開発言語

開発環境て添付のコンパイラ類を使用し C++11 以降を前提とします。

採用ライブラリ、コード

ゲーム用フレームワークはたぶん存在しないと思われます。
のでいくつかのライブラリと自作ライブラリを組み合わせていきます。

基本

M5Stack の諸々を制御するための基本ライブラリ M5Stack があり、こちらを使用していきます。

描画

さて当初は昔のように基本的な描画ライブラリを作ろうと考えていたわけですが、M5Stack 界隈では様々な方が様々なライブラリを発表されています。その中でも白眉といえるのが LovyanGFX です。
私が欲しいと思っていた機能を満たし、性能的にも素晴らしいライブラリで、諸々実験してみた結果いけそうな手応えを得て、描画はこちらを採用としました。(なお LovyanGFX は 二条項BSDライセンスです)

サウンド

こちらも M5UnifiedSpeaker Class があり、サウンド再生時のノイズ軽減他、機能性能ともに十分なものです。
ただ M5Unified を採用すると先述の M5Stack との競合が発生するのと、SDカードからのストリーミング再生と絡ませる為、該当部分のコードを自作ライブラリに取り込んで改造して使用しています。(なお M5Unified は MITライセンスです)

SDカード

M5Stack のライブラリでもSDカードアクセスは可能ですが、いくつかのバージョンで不具合があった為、SdFat を採用しました。(なお SdFat は MITライセンスです)

自作ライブラリ

MITライセンスで公開しております。

goblib

古の時代の自作ライブラリとコード群を掘り出して、整備改良追加したものです。
こちらはハードウェア非依存。

goblib_m5s

M5Stack 依存部分の自作ライブラリです。

アプリケーションクラスによるメインループ

LovyanGFX のサンプルにある MovingIcons を移植してみます。

https://github.com/lovyan03/LovyanGFX/tree/master/examples/Sprite/MovingIcons

goblib にはいわゆるゲームループを内包した App class がありますので、こちらを継承して使用します。
なお M5Stack でゲームを動作させる上では 30FPS 固定辺りが適当でしょう。

M5.begin

M5.begin(false /* LCD */, false /* SD */ , true /* Serial */);

LCD 関連は LovyanGFX、SDカード関連は SdFat に任せるので M5.beginでは LCD と SD に関しての初期化などを行わないようにします。
(今回はSDカードは使用しません)

goblib::App

継承

アプリケーションクラスはシングルトンとしても定義します。いわゆる多重継承ですね。

#define MAX_FPS (30)
using AppClock = std::chrono::steady_clock;
class m5s_app : public goblib::App<AppClock, MAX_FPS, MAX_FPS>, goblib::Singleton<m5s_app>{...};

goblib::App::fixedUpdate()

テンプレート引数として指定した FPS に基づき、1秒間に指定回数実行されるように努めるメンバ関数です。
処理遅延で累積遅延時間が溜まると、解消するためにフレームの中で複数回呼ばれる場合があります。
これによって擬似的に一定間隔で呼ばれているかのような挙動となります。
Unity における FixedUpdate に近いものと考えると良いでしょう。

goblib::App::update(const float delta)

毎フレーム1回、fixedUpdate() の後に呼ばれるメンバ関数です。引数 delta を用いて諸々の補正することが可能です。

goblib::App::render()

毎フレームに1回、update() の後に呼ばれるメンバ関数です。
update() での処理に基づき描画処理を行う事を目的としていますが、任意の処理をしても構いません。

goblib::App::sleep_until(const std::chrono::time_point<AppClock, UpdateDuration>& abs_time)

fixedUpdate()、update()、render() の呼び出しの後、固定 FPS 維持のために指定した絶対時間まで待つメンバ関数です。
M5Stack では std::thread::sleep_until() / sleep_for() の実装が荒いので、オーバーライドしてより精度の高い待ち方をするようにします。

gob_app.hpp
GOBLIB_INLINE virtual void sleep_until(const std::chrono::time_point<Clock, UpdateDuration>& abs_time)
{
    std::this_thread::sleep_until(abs_time);
}
app.hpp
virtual void sleep_until(const std::chrono::time_point<AppClock, UpdateDuration>& abs_time) override
{
    auto us = std::chrono::duration_cast<std::chrono::microseconds>(abs_time - AppClock::now()).count();
    auto ms = us > 0 ? us / 1000 : 0;
    delay(ms);
    while(AppClock::now() < abs_time){ taskYIELD(); }
}

それぞれの比較は以下の画像を参照してください。 std::thread::sleep_until を使用すると FPS が落ちてしまいます。

std::thread::sleep_until
override

コード

MovingIcons のアイコンを動かす部分を update() に、描画部分を render() へ移動し、 goblib::App ベースで動作するようにしたものです。

https://github.com/GOB52/M5S_games/tree/master/m5s_app

謝辞

素晴らしいライブラリの作成者の皆さんと、諸々の助言をしてくださった方々に感謝申し上げます。

Have a happy coding :)

Discussion