プログラミング
Game Boy Advance開発: コンソールへのロギング
Game Boy Advance Dev: Logging to the Console (mattgreer.dev)
要約
この記事では、mGBAエミュレータを使用してGame Boy Advance (GBA) ゲーム開発中にコンソールへログを出力する方法を解説します。mGBAは、開発者がデバッグに役立つprintf()のような機能を利用できるように、特別なメモリマップドレジスタを提供しています。これにより、ゲームの実行中にデバッグ情報を効率的に確認することが可能になります。
全文翻訳
mGBAPrintf()のようなC言語のprintf()やJavaScriptのconsole.log()を使ったコンソールへの出力は、開発中に役立つツールです。GBAゲームを作成していると、これを諦めなければならないか、GBAの非常に小さな画面にログを出すしかないと思うかもしれません。しかし、mGBAを使用すれば、ゲーム内でprintf()機能を利用できます。これは簡単に実現できます。
仕組み
GBA開発では、REG_DISPCNTのようなレジスタに書き込むことがよくあります。これはメモリマップドレジスタであり、ゲームがGBAハードウェアに何をさせたいかを伝える簡単な方法です。これはGBAのメモリマップ上の単なるアドレスであり、書き込みが行われると、GBAハードウェアは受信したデータを受け取ってそれに応じて動作します。
mGBAは、ロギング目的でいくつかのメモリマップドレジスタを追加しました。これらはREG_DISPCNTなどと同様に機能しますが、ゲームがmGBA内で実行されている場合にのみ機能します。
これらのmGBA固有のレジスタの定義は以下の通りです。
#define REG_LOG_ENABLE (vu32*)(0x4FFF780)
#define REG_LOG_BUFFER (vu32*)(0x4FFF600)
#define REG_LOG_SEND (vu32*)(0x4FFF700)
したがって、データをログに記録するには、まずREG_LOG_ENABLEに特別な値を送信してロギングをオンにします。次に、データをREG_LOG_BUFFERに書き込みます。完了したら、ログレベルをREG_LOG_SENDに書き込みます。この時点でmGBAは送信されたデータを受け取り、それをログに出力します。
これは少し複雑に聞こえるかもしれませんが、実際にはこれらの詳細をすべて関数内に隠して、二度と考える必要はありません。以下はその関数です。
#define REG_LOG_ENABLE (vu32*)(0x4FFF780)
#define REG_LOG_BUFFER (vu32*)(0x4FFF600)
#define REG_LOG_SEND (vu32*)(0x4FFF700)
#define MGBA_LOG_MAX_LINE 256
#define ERROR 0x101
#define WARNING 0x102
#define INFO 0x103
#define DEBUG 0x104
void mgbalog(u32 level, const char *msg) {
*REG_LOG_ENABLE = 0xC0DE;
tonccpy((void *)REG_LOG_BUFFER, msg, MGBA_LOG_MAX_LINE);
*REG_LOG_SEND = level;
}
その後、ログを記録する必要があるときにゲーム内で使用できます。
#include "mgbalog.h"
void myFunc() {
...
mgbalog(DEBUG, "hello mGBA log");
}
ゲームを実行したら、Tools > View logs... に移動してログウィンドウを開きます。
mGBAのログウィンドウにログメッセージが表示された様子。
そして、メッセージが表示されます。
ログを記録する際には、エラー、警告、情報、デバッグなど、さまざまなレベルでログを記録することを選択できます。それがmgbalog()のlevelのビジネスです。希望するログレベルをREG_LOG_SENDに書き込みます。これは、メッセージをどのレベルで保存するか、そしてメッセージの書き込みが完了したことをmGBAに伝えるという二重の役割を果たします。
ターミナルへのロギング
ログを確認するために毎回ログウィンドウを開くのは面倒です。mGBAをコマンドラインから起動した場合、ログをターミナルに書き出すように指示することもできます。これを行うには、ログウィンドウを開き、詳細設定をクリックし、下部にある「Log to console」にチェックを入れます。
ログの詳細設定ウィンドウで「Log to console」にチェックが入った状態。
mGBA自体も多くのログを出力します。このダイアログでそれらをすべてオン/オフできます。
これで、ゲームをコマンドライン経由で起動すると、ログ出力がそこに表示されます。--log-levelでログレベルを制御できます。例えば、DEBUGのみをコンソールにログするには、次のようにします。
mgba --log-level 16 yourRom.gba
ログレベルは以下の通りです。
1 - 致命的なエラー
2 - エラー
4 - 警告
8 - 情報
16 - デバッグ
32 - スタブ
64 - ゲーム内エラー
これらを組み合わせることもできます。例えば、デバッグとエラーの両方を表示したい場合は、--log-level 18とします。
フォーマットの追加
静的な文字列をログに記録することはめったにありません。幸いなことに、printfスタイルのフォーマットを追加するのは簡単です。
私のブログでは、<と>を使用するCインクルードは許可されていないため、回避策としてスペースを追加しました。これを修正する必要があります...
#include < stdarg.h >
#include < stdio.h >
static char logBuffer[MGBA_LOG_MAX_LINE];
void mgbalog(u32 level, const char *format, ...) {
*REG_LOG_ENABLE = 0xC0DE;
va_list formatArgs;
va_start(formatArgs, format);
vsnprintf(logBuffer, MGBA_LOG_MAX_LINE, format, formatArgs);
va_end(formatArgs);
tonccpy((void *)REG_LOG_BUFFER, logBuffer, MGBA_LOG_MAX_LINE);
*REG_LOG_SEND = level;
}
これで、mgbalog(DEBUG, "pos.x=%i", pos.x); のようなことができます。
開発中のみロギング
ロギングはバイナリに追加され、メモリとCPUを消費し、一般的にゲームの最終バージョンでは望ましくありません。特にフォーマットを追加する場合、標準ライブラリのvsnprintfのようなものは、貧弱なGBAにとってはかなりコストがかかります。
私はロギングの実装をMGBALOGという定義の後ろに隠しています。そのため、ロギングが必要な場合はMGBALOGを定義します。定義されていない場合、ロギングに関連するものは何もバイナリに追加されません。
#ifdef MGBALOG
void mgbalog(u32 level, const char *format, ...) {
...
}
#endif
これはすべて良いことですが、MGBALOGを定義しない場合、コードベース全体からすべてのログ呼び出しを削除する必要があることを意味します。そうしないと、コンパイルエラーが発生します。
これは、さらに多くのdefineのいたずらで簡単に修正できます。これが私のmgbalog.hです。
#pragma once
#include < tonc.h >
#define MGBA_LOG_MAX_LINE 256
#define ERROR 0x101
#define WARNING 0x102
#define INFO 0x103
#define DEBUG 0x104
#define REG_LOG_ENABLE (vu32*)(0x4FFF780)
#define REG_LOG_BUFFER (vu32*)(0x4FFF600)
#define REG_LOG_SEND (vu32*)(0x4FFF700)
#ifdef MGBALOG
void _mgbalog(u32 level, const char *format, ...);
#define mgbalog(...) _mgbalog(__VA_ARGS__)
#else
#define mgbalog(level, format, ...)
#endif
これにより、ゲーム全体でログを記録したいときに、これまで通りmgbalog(...)を呼び出します。これは、MGBALOGが定義されていれば_mgbalog()を呼び出し、定義されていなければ何も行わないマクロになります。これで、ログ呼び出しについて心配する必要がなくなり、ゲームは常にコンパイルできるようになります。
上記のヘッダーファイルを使用すると、実装は次のようになります。
#ifdef MGBALOG
#include "mgbalog.h"
#include < stdarg.h >
#include < stdio.h >
static char logBuffer[MGBA_LOG_MAX_LINE];
void _mgbalog(u32 level, const char *format, ...) {
*REG_LOG_ENABLE = 0xC0DE;
va_list formatArgs;
va_start(formatArgs, format);
vsnprintf(logBuffer, MGBA_LOG_MAX_LINE, format, formatArgs);
va_end(formatArgs);
tonccpy((void *)REG_LOG_BUFFER, logBuffer, MGBA_LOG_MAX_LINE);
*REG_LOG_SEND = level;
}
#endif
結論
これだけです。ちょっとしたコードで、開発体験に多くのユーティリティが追加されます。自分で実装したくない場合は、mGBAロギングライブラリがいくつかあります。あるいは、Butanoのようなエンジンを使用している場合、それはすでにmGBAロギングが組み込まれています。ロギングライブラリを使用する際には、その裏側で何が行われているかを知ることができます。これは常に良いことです。