Game Boy Advance 开发:控制台日志记录
摘要
本文介绍如何在 Game Boy Advance 开发中实现控制台日志记录,通过使用 mGBA 自定义的内存映射寄存器,在模拟器的日志窗口或终端中实现类似 printf 的调试功能。
<p><a href="https://lobste.rs/s/ujjm68/game_boy_advance_dev_logging_console">评论</a></p>
查看缓存全文
缓存时间: 2026/07/04 18:44
# Game Boy Advance 开发:控制台日志记录
来源:https://www.mattgreer.dev/blog/gba-dev-logging/
在开发过程中,使用 `printf()`(C 语言)或 `console.log()`(JavaScript)之类的方式向控制台输出信息是非常有用的工具。在制作 GBA 游戏时,你可能会认为只能放弃这种做法,或者只能将日志输出到 GBA 那个小小的屏幕上。如果你使用 mGBA,就可以在你的游戏中获得 `printf()` 的功能。而且实现起来很简单。
## 工作原理
在 GBA 开发中,你经常需要向 `REG_DISPCNT` 这样的寄存器写入数据。这是一个内存映射寄存器,是让你的游戏告诉 GBA 硬件要做些什么的简单方式。它只是 GBA 内存映射中的一个地址,当写入数据时,GBA 硬件会接收数据并做出相应处理。
mGBA 为了日志记录的目的,额外添加了几个内存映射寄存器。它们的工作方式与 `REG_DISPCNT` 等类似,唯一的区别是它们只在你的游戏运行在 mGBA 内部时才生效。
以下是这些 mGBA 特定寄存器的定义:
```c
#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 会接收你发送的数据并输出到它的日志中。
这听起来有点复杂,实际上也确实如此。但你完全可以把这些细节封装到一个函数中,之后就不再需要考虑它们。
下面是这个函数:
```c
#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;
}
```
然后,你可以在游戏中任何需要记录日志的地方使用它:
```c
#include "mgbalog.h"
void myFunc() {
...
mgbalog(DEBUG, "hello mGBA log");
}
```
运行游戏时,进入 `Tools > View logs...` 打开日志窗口。
mGBA 日志窗口中显示了我们记录的日志消息。
这就是你的消息。记录日志时,你可以选择不同的日志级别:错误(error)、警告(warning)、信息(info)或调试(debug)。这就是 `mgbalog()` 中 `level` 参数的用途。你将所需的日志级别写入 `REG_LOG_SEND`,这一方面告诉 mGBA 按什么级别存储消息,另一方面也表示消息写入完成,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 中使用 `<` 和 `>`,所以我加了空格来绕过这个问题——我真的需要修复这个问题……
```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`;如果没有定义,与日志记录相关的任何内容都不会被添加到二进制文件中。
```c
#ifdef MGBALOG
void mgbalog(u32 level, const char *format, ...) {
...
}
#endif
```
这样很好,但这也意味着如果你没有定义 `MGBALOG`,就必须移除代码中所有的日志调用,否则会出现编译错误。这个问题可以通过更多宏技巧轻松解决。
下面是我的 `mgbalog.h`:
```c
#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()`;如果没有定义,就什么都不做。这样你就不再需要担心日志调用的问题,无论是否定义宏,游戏都能正常编译。
配合上面的头文件,这是我的实现:
```c
#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 (https://github.com/gvaliente/butano) 这样的引擎,它已经内置了 mGBA 日志功能。
现在,当你使用一个日志库时,你就知道它在后台做了什么——这总是件好事。
相似文章
模拟器调试:Area 5150 的 Lake Effect
本文详细介绍了在MartyPC模拟器上调试Area5150演示中“Lake”效应的过程,解释了需要特定标题hack的原因,以及通过总线嗅探和动态时钟实现周期精确CGA模拟的后续修复方法。
在iPhone上编程GBA游戏
一位作者记录了如何完全在iPhone上编程Game Boy Advance游戏,使用了iSH、Textastic、Delta和gba bootstrap等工具,最终制作了一款名为TO THE TOWER的短小游戏。
Rust 与 GBA:环境搭建与像素渲染
一份教程指南,讲解如何搭建 Rust 项目以构建能在 Game Boy Advance 上运行的 ROM,涵盖项目设置和像素渲染。
为什么我在2024年用Zig编写了一个Game Boy Advance游戏
一位开发者解释了为什么他们选择Zig编程语言来创建Game Boy Advance游戏,强调了Zig的交叉编译能力及其对嵌入式编程的适用性。
打造一个串行和VGA的“万能控制台”
一个DIY项目,利用二手IBM 1U机架式控制台和终端模拟器构建便携式串行和VGA控制台,面向复古电脑爱好者。