尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Game Boy 自制游戏开发实战:用 GBDK 构建《黑城堡 2》

Game Boy 自制游戏开发实战:用 GBDK 构建《黑城堡 2》 Game Boy 已经停产二十多年但今天仍有大量开发者乐此不疲地为它写游戏。这个现象本身就值得聊一聊。如果你以为 Game Boy 游戏只是“画面劣化版的小游戏”那很容易低估它背后真实的开发难度。实际上Game Boy 的自制游戏Homebrew圈子一直很活跃从引擎、工具链、素材管线到社区分享已经形成了一套非常完整的开发生态。而这篇文章要讨论的《黑城堡 2》正好是一个典型的小型自制 Game Boy 项目它的体量不大但涉及了瓦片地图、精灵控制、碰撞检测、卡带 ROM 构建等整套基础开发流程。这篇文章不会停留在“Game Boy 很复古、很情怀”这种层面。我的核心判断是Game Boy 开发真正的价值在于用极其有限的硬件资源逼你把游戏逻辑和组织能力理清楚。CPU 只有约 4.19 MHz主内存只有 8KB屏幕分辨率只有 160x144调色板只有黑白灰四种颜色。这种环境下任何浪费都是不可接受的。所以Game Boy 自制游戏并不是一个过时的兴趣而是一个极好的“编程约束训练场”。读完这篇文章你可以学会这样几件事Game Boy 上做游戏为什么慎用高级抽象用 GBDK 建立一个小型项目需要哪些文件一个角色移动、碰撞检测和地图滚动的最小实现长什么样如何把 C 代码编译成真正的 .gb ROM并用模拟器或烧录卡跑起来以及开发过程中最常踩的坑是什么。如果你对《黑城堡 2》这个谜题、动作或冒险游戏的设计感兴趣这篇文章也会帮你理解为什么在 Game Boy 上做“减法设计”不是妥协而是一种主动选择。1. 为什么到今天还有人自制 Game Boy 游戏很多人会把“自制游戏”和“开发小游戏”画等号觉得 Game Boy 这种老平台无非是分辨率低、颜色少做起来应该很简单。事实恰好相反。Game Boy 开发难度并不低因为它要求开发者先理解硬件再写代码。你不能像 Web 开发一样十几层抽象叠上去也不能像现代游戏引擎那样直接拖一个 Sprite 到场景里。你得知道当前帧处于什么阶段VBlank 只有多少行扫描时间可以用来安全更新显存精灵属性表只有 40 个槽位超出就会闪烁。自制 Game Boy 游戏之所以持续有人做有几个现实原因。第一开发工具逐步成熟。你不需要懂汇编才能动手GBDK 提供了 C 语言接口RGBDS 也支持用汇编精细控制。第二模拟器和调试工具非常完备开发者不必买实体卡带也能完成大部分验证。第三Game Boy 的视觉风格非常稳定黑白灰的调色板和固定分辨率天然形成一种审美门槛很多人反而觉得这种限制下做出来的画面很有味道。所以《黑城堡 2》这类项目并非偶然。它代表了一类“小而完整”的游戏开发实践一个人或小团队不依赖商业引擎不受平台更新影响能把一套代码从写第一个主循环一直做到生成 ROM 文件。从学习角度来看这样的项目比一上来就做跨平台手游更能训练底层思维。2. Game Boy 硬件的边界决定所有设计在进入代码之前我们需要先清楚 Game Boy 的硬件底牌。这不是背景知识而是后续每个设计决策的依据。Game Boy 使用了类似 Z80 的 8 位 CPU主频大约 4.19 MHz。注意这里还不是“GHz 时代”的多核处理器而是单核、低频率、有限寻址的 8 位 CPU。内存方面工作 RAM 只有 8KB显存 VRAM 只有 8KB精灵属性表 OAM 只有 160 字节最多同时定义 40 个精灵但每行硬件上只能显示 10 个。屏幕分辨率是 160x144 像素刷新率约 59.7 帧每秒。调色板也不是 RGB 真彩色而是 4 级灰阶从最亮到最暗一共 4 个颜色。理解了这些参数就理解了 Game Boy 开发的本质一切资源都极其稀缺。一个背景 tile 是 8x8 像素调色板只能给 tile 提供 2bit 索引一个精灵可以是 8x8 或 8x16 像素但位置、优先级、翻转、调色板信息都必须塞进 OAM 表里地图背景没有免费的高速缓存想要滚动地图就需要靠更新 tile map 或切换视口来实现。这些限制对《黑城堡 2》这种游戏的影响是直接的角色动作帧不能多每个方向可能只有 2 到 4 帧否则玩家精灵会占用过多 OAM 槽位地图物件不能无限制摆因为每个 8x8 的 tile 都有唯一编号tile 数量多了会占据大量 VRAM关卡纵深和屏幕外逻辑要控制CPU 太弱不能每帧对全地图做复杂寻路美术风格必须简洁4 级灰度下过度追求细节没有意义反而会让画面糊成一团。用一句话总结Game Boy 开发的核心不是“做加法”而是“在卡片大小的内存里做取舍”。这份约束感是商业游戏引擎很难给你的。3. 开发方案对比GBDK、RGBDS 还是 ZGBGame Boy 自制游戏有几条主流技术路线。选型会影响你写代码的方式以及你能用多快的速度实现玩法。3.1 GBDK 系列GBDK 是 Game Boy Development Kit 的缩写它提供了 C 编译器工具链和运行时库。开发者可以用类似标准 C 的语法写游戏逻辑然后交给 lcc 编译器生成 ROM。GBDK 2020 是当前维护比较频繁的版本API 相比旧版更清晰。它的优点很明显上手快、可以复用 C 的工程经验、字符串和数组处理方便。缺点也很明显C 语言距离硬件还是有点远如果你对瓦片 bank 切换、精灵复用、时序控制理解不深很容易写出“看起来没问题但运行就会花屏”的代码。3.2 RGBDSRGBDS 是一套汇编工具链。它用 .asm 文件编写开发者可以精确控制每一条指令、每一个内存字节。优点是性能上限极高、代码体积可以做到很小。缺点是学习曲线陡峭你不仅要写游戏逻辑还要管理寄存器、地址空间和中断。大多数独立自制游戏不会全用汇编除非作者有很深的嵌入式经验。3.3 ZGB 引擎和其他框架ZGB 是基于 GBDK 的引擎层它提供了精灵管理、地图系统、场景切换等高级封装。优点是开发效率高适合做俯视角 ARPG。缺点是封装性更强出问题时你需要理解引擎内部才能排查。回到《黑城堡 2》这个案例。如果目标是做一款带有探索、战斗和解谜要素的动作游戏GBDK 直接裸写至少需要自己管理很多东西tile map 更新、OAM 排序、帧循环、输入消抖。而 ZGB 可以省去一部分样板代码。从学习角度看第一版建议先用 GBDK 裸写一版因为你一旦理解了底层之后用任何引擎都不慌。从效率角度看如果已经有明确玩法和美术素材ZGB 会让迭代更轻松。这篇文章的示例代码使用 GBDK 风格因为它的通用性最强能覆盖最多开发场景。4. 开发环境搭建把 GBDK 和模拟器跑起来无论你是 Windows、macOS 还是 Linux 用户搭建 Game Boy 开发环境都不复杂。核心工具只有两个GBDK 工具链和一个稳定的模拟器。4.1 安装 GBDKGBDK 2020 提供了各平台的预编译包。如果你使用 macOS可以通过 Homebrew 搜索相关 formula也可以直接从官方 Release 下载压缩包。Linux 用户通常下载 tar.gz 后设置 PATH 即可。Windows 用户可以解压后把 bin 目录加入系统环境变量。下面以 Linux/macOS 的命令行方式说明# 假设你已经下载并解压到 /usr/local/gbdk export GBDK/usr/local/gbdk export PATH$GBDK/bin:$PATH # 查看编译器是否可用 lcc --version如果你解压到其他目录请把 GBDK 变量改成你的实际路径。记得把这两行 export 写进 .bashrc 或 .zshrc否则每次开终端都要重新设置。4.2 安装模拟器推荐使用 BGB 或 mGBA。BGB 有强大的调试功能和精确的时序仿真适合排查渲染和中断问题mGBA 更通用跨平台且持续维护。如果你用的是 macOSmGBA 也可以通过 Homebrew 安装非常方便。# macOS 安装 mGBA 示例 brew install mgba模拟器不是“一个能跑的窗口”那么简单。对自制游戏来说调试器才是关键我们要看的是 VRAM 当前状态、OAM 表内容、中断触发时间、内存读写断点。BGB 在这方面的表现尤其突出。建议在开发初期就把断点和内存查看器用起来否则后期遇到花屏或精灵闪烁纯靠猜测会非常痛苦。4.3 建立一个空项目在写任何代码之前先建立清晰的项目目录。后面所有源码、素材和构建产物都不会散落得到处都是blackcastle2/ ├── build/ ├── res/ ├── src/ │ ├── main.c │ ├── player.c │ ├── player.h │ ├── map.c │ ├── map.h │ └── ... ├── Makefile └── README.mdbuild 目录放编译产物res 目录放 PNG 素材和由工具转换出来的 C 数组src 目录放源码。这样一个结构在自制游戏里已经足够不需要引入复杂的依赖管理。5. 游戏设计《黑城堡 2》在 160x144 里如何运转在为《黑城堡 2》写代码之前我们先把玩法落地到硬件限制上。5.1 选择视角与移动方式俯视角四方向移动是 Game Boy 上最常见的 ARPG 形态因为实现成本低、玩家理解成本低。画面中玩家角色占据一个 8x16 或 8x8 的精灵地图以 tile 为单位布局角色按方向键移动。因为屏幕只有 160x144 像素一次能显示的地图范围大约 20x18 个 tile。这个范围既适合做地牢探索也适合做解密关卡。5.2 动画帧与调色板因为只有 4 级灰阶美术上不能依赖过多颜色变化。角色动画需要突出轮廓和位置变化通常每个方向准备 2 到 3 帧。tile 数据由 png2asset 工具从 PNG 导出为 C 数组后续代码直接把这些数组加载到 VRAM。5.3 地图与碰撞地图可以划分成 tile map 和碰撞层。tile map 记录背景缩略索引碰撞层单独用一个字节数组记录哪个 tile 可走、哪个 tile 是墙。这种做法很符合 Game Boy 的硬件逻辑显示用一套数据逻辑碰撞用另一套数据。两者的索引对应关系由地图编辑工具统一生成。5.4 场景数量与 ROM 容量普通 Game Boy 卡带在没有 MBC 的时候只有 32KB ROM但这显然不够一个完整游戏使用。MBC1 之类的 Memory Bank Controller 可以把 ROM 分成多个 16KB bank程序运行时切换 bank 来读取不同关卡数据。《黑城堡 2》如果需要多个地图和大量对话文本必须考虑 bank 切换如果只是单关卡 Demo32KB 也能跑。从项目风险角度看我建议第一个版本不要做多关卡。先做一个 20x18 屏幕的封闭场景完成角色移动、碰撞、NPC 对话和简单的门开关再扩展到多地图。这样每一步都可运行、可回滚。6. 完整代码实现从第一个主循环到可玩版本下面进入核心部分。我会用 GBDK 2020 的 API 写一个简化版《黑城堡 2》框架。代码不是完整游戏但能体现一个可运行 Game Boy 程序的工程骨架。6.1 工程目录与构建脚本创建一个 Makefile 来管理构建流程。这是整个项目的基础因为手工敲 lcc 编译命令很容易出错。# 文件路径Makefile PROJECT blackcastle2 SRC_DIR src BIN_DIR build SOURCES $(SRC_DIR)/main.c \ $(SRC_DIR)/player.c \ $(SRC_DIR)/map.c CC lcc CFLAGS -Wall -Wextra -Werror TARGET $(BIN_DIR)/$(PROJECT).gb $(TARGET): $(SOURCES) mkdir -p $(BIN_DIR) $(CC) $(CFLAGS) -o $(TARGET) $(SOURCES) run: $(TARGET) mgba $(TARGET) clean: rm -rf $(BIN_DIR) .PHONY: run clean这个 Makefile 做的事情很简单把 src 下的三个 C 文件编译成一个 Game Boy ROM 文件。运行 make run 时会用 mGBA 直接打开刚生成的 .gb。如果你的模拟器不是 mGBA改成你的模拟器路径即可。6.2 主循环与帧同步所有 Game Boy 游戏的骨架都类似初始化硬件资源然后进入一个无限循环每一帧等待垂直同步更新游戏状态最后把显示对象的位置同步到 OAM。下面的 main.c 展示了这个流程。// 文件路径src/main.c #include gb/gb.h #include player.h #include map.h void init_game(void) { // 以实际项目资源为准这里假设已经从素材文件转换得到 // load_background_tiles(); // load_background_map(); player_init(80, 72); SHOW_BKG; SHOW_SPRITES; DISPLAY_ON; } void main(void) { init_game(); // 主循环Game Boy 程序不会“退出” while (1) { // 等待 VBlank确保在安全时间内更新显存 wait_vbl_done(); // 更新玩家逻辑 player_update(); // 每帧只做当前必须做的事情 // 不要把复杂的计算放在这里否则会掉帧 } }这个主循环看起来简单但它是整个执行模型的核心。wait_vbl_done() 的作用是等待屏幕扫描进入垂直消隐区间。只有在这个区间内更新精灵和背景数据才不会出现撕裂或花屏。如果循环内的工作量太大超过了 VBlank 窗口游戏就会降帧。很多时候 Game Boy 游戏的“卡顿”不是硬件问题而是主循环里塞了太多冗余计算。6.3 玩家移动与碰撞检测玩家移动是动作游戏的核心。我们用一个单独的 player.c 文件管理玩家坐标、精灵初始化和更新逻辑。// 文件路径src/player.h #ifndef PLAYER_H #define PLAYER_H #include gb/gb.h void player_init(uint8_t x, uint8_t y); void player_update(void); #endif// 文件路径src/player.c #include gb/gb.h #include player.h #include map.h // 使用 16 位坐标避免移动速度导致回绕 static uint16_t player_x; static uint16_t player_y; static uint8_t player_speed 1; // 玩家精灵占用的 tile 索引 #define PLAYER_SPRITE_TILE 0 void player_init(uint8_t x, uint8_t y) { player_x x; player_y y; // 假设已在 main 中加载过精灵图 set_sprite_tile(0, PLAYER_SPRITE_TILE); move_sprite(0, player_x, player_y); } void player_update(void) { uint8_t keys joypad(); uint8_t next_x player_x; uint8_t next_y player_y; if (keys J_LEFT) { if (player_x 0) { next_x player_x - player_speed; } } if (keys J_RIGHT) { next_x player_x player_speed; } if (keys J_UP) { if (player_y 0) { next_y player_y - player_speed; } } if (keys J_DOWN) { next_y player_y player_speed; } // 以 tile 为粒度做碰撞检测 if (is_walkable(next_x / 8, next_y / 8)) { player_x next_x; player_y next_y; } move_sprite(0, player_x, player_y); }这里的关键点在于坐标使用 uint16_t避免加到 256 时发生回绕先计算目标位置再统一做碰撞检测而不是每按一次键立刻移动这样能避免角色卡进墙里is_walkable 接收的是 tile 坐标而不是像素坐标所以需要把像素坐标除以 8move_sprite 的参数是像素坐标注意 Game Boy 硬件里精灵坐标有偏移量实际显示位置还需要根据需求调整。如果你觉得一行移动 1 像素太慢可以把 player_speed 改成 2但要注意碰撞时是否需要按 tile 对齐。多数 Game Boy ARPG 在移动时并不强制整格对齐因为硬件本身可以做到像素级位移。是否对齐取决于你的制作工具和关卡设计。6.4 地图碰撞与背景加载接下来是 map.c它负责向上层提供“某个 tile 是否可通行”的查询以及背景的加载。背景数据通常不是手写数组而是由 png2asset 等工具从 PNG 导出。为了保持示例完整这里用函数接口说明结构。// 文件路径src/map.h #ifndef MAP_H #define MAP_H #include gb/gb.h #define MAP_COLS 20 #define MAP_ROWS 18 void map_load(void); uint8_t is_walkable(uint8_t col, uint8_t row); #endif// 文件路径src/map.c #include gb/gb.h #include map.h // 假设这个数组由 png2asset 工具导出 // 示例中不展开全部数据实际项目请替换为真实资源 const uint8_t background_tiles[] { // 这里存放从 PNG 转换得到的 tile 数据 }; const uint8_t background_map[] { // 这里存放 tile map长度至少为 MAP_COLS * MAP_ROWS }; const uint8_t collision_map[] { // 0 表示可通行1 表示障碍物 // 长度与背景 tile map 一致每字节对应一个 tile }; void map_load(void) { // 第一个参数是 VRAM tile 起始索引 // 第二个参数是 tile 数量 // 第三个参数是 tile 数据数组 set_bkg_data(0, sizeof(background_tiles) / 16, background_tiles); // 将背景地图写入显存 // 20 x 18 对应 Game Boy 整个屏幕的 tile 数量 set_bkg_tiles(0, 0, MAP_COLS, MAP_ROWS, background_map); } uint8_t is_walkable(uint8_t col, uint8_t row) { uint16_t index row * MAP_COLS col; return collision_map[index] 0; }这段代码有两个需要特别说明的地方。第一set_bkg_data 的 tile 数量计算是字节数 / 16因为每个 tile 固定是 8x8 像素每像素 2bit合计 16 字节。很多新手会在这里算错后果是画面出现乱码。第二is_walkable 只是查询一个数组数组数据必须和视觉背景图严格对齐。如果你在游戏里改动了地图但忘了更新 collision_map就会出现“角色能穿墙”或“面前是空气却走不过去”的问题。实际开发中collision_map 一般由脚本从地图编辑器中导出而不是手写。比如你用 Tiled 编辑地图导出 CSV 后写一个小工具转换成 C 数组。人工维护碰撞层非常不可靠尤其是当关卡变大以后。6.5 扩展物体、敌人和对话主循环、移动和碰撞都跑通后就可以向《黑城堡 2》的完整玩法扩展了。敌人可以用同样的方式创建但要注意OAM 总共有 40 个精灵角色用掉 1 个如果角色是 2x2 个 8x8 精灵组合成一个 16x16 角色就会用掉 4 个一个敌人如果设计成 16x16同样占 4 个精灵槽位屏幕上的敌人数不能太多对话系统可以复用背景 tile map用半透明覆盖层弹出文字框但这个实现要谨慎因为会临时占用背景 tile 和调色板。推荐的做法是先把单场景的“移动 碰撞 敌我距离判定 交互按键”跑通再考虑多场景切换。因为多场景切换会引入 ROM bank 管理和地图复位的复杂度如果基础玩法没稳扩展很容易失控。7. 构建、运行与效果验证代码写完之后怎么验证它真的在模拟器上跑起来了7.1 构建命令在项目根目录执行make clean make如果一切正常你会在 build 目录下看到 blackcastle2.gb。这个文件就是标准的 Game Boy ROM 镜像可以被模拟器加载也可以烧录到实体卡带。7.2 在模拟器中运行make run或者手动打开mgba build/blackcastle2.gb预期结果是模拟器打开后背景显示了地图玩家精灵出现在屏幕中央。按方向键玩家可以在地图内移动遇到障碍物会自动停止。如果屏幕显示异常、角色消失或按键无反应先按下面的顺序排查。7.3 验证成功的标准画面没有花屏背景 tile 显示正确没有出现随机噪声角色稳定移动使用 4 个方向都能走走到地图边缘或撞到墙时不会越过障碍物帧率稳定画面没有明显闪烁或拖影ROM 大小正常在 build 目录执行ls -l build/blackcastle2.gb确认大小与你的卡带类型匹配。如果你用的是 BGB还可以打开 VRAM 查看器直接确认 tile 数据和 tile map 是否符合预期。这一步非常有用很多渲染问题在 VRAM 视图里一眼就能看出来。8. 常见问题与排查思路在自制 Game Boy 游戏过程中有几个问题几乎每个人都会遇到。这里列成表格方便你贴在项目笔记里。问题现象可能原因排查方式解决方案编译报错找不到 lccGBDK 不在 PATH 中在终端执行lcc --version检查 GBDK bin 目录是否已加入 PATHROM 生成后模拟器打不开ROM 文件损坏或工具链版本过旧检查 build 目录是否生成了有效文件、重新编译升级到 GBDK 2020 最新版本角色第一次移动就花屏tile 数据加载时机不对或 VRAM 写入越界检查 set_sprite_data 调用是否在 SHOW_SPRITES 之前统一初始化顺序先显存、再精灵、再显示角色碰撞不准碰撞 map 与视觉 tile 未对齐打印或查看碰撞数组的偏移度统一从工具链导出 collision map不要手写精灵闪烁或消失OAM 槽位不足或一帧内在不该更新的时候改了 OAM用 BGB 查看 OAM 视图限制同屏精灵数量排序后写入 OAM画面上下跳动或撕裂主循环内工作量过大超过 VBlank在循环内加帧计数观察实际帧率把重计算移到非渲染阶段减少每帧工作量按键响应延迟每帧调用 joypad 次数过多或初始化顺序错误在 player_update 里加调试代码每帧只调用一次 joypad状态缓存到局部变量这些问题的共同特征是表面现象在画面上真正原因在内存和时序里。所以养成使用模拟器调试器的习惯非常重要。不要只看窗口画面要多看 VRAM、OAM 和中断信息。9. 工程最佳实践让自制游戏项目能持续迭代写一个能跑的 Demo 和写一个能完成的游戏是完全两件事。下面这些工程建议是从《黑城堡 2》这类小型 Game Boy 项目里可以复用到的经验。9.1 素材进代码要自动化不要手敲 tile 数组。用 png2asset 或类似工具把 PNG 导出成 C 头文件再让代码 include。游戏的每个版本素材和代码应该能够从源文件重新生成中间不能有“手工粘贴”的步骤。只要有一次手工复制错后续排查都是灾难。9.2 用 bank 管理大关卡如果你的游戏超过 32KB必须引入 MBC1 或更高档次的卡带映射。这意味着代码里会出现 SWITCH_ROM_MBC1 之类的操作。建议封装成模块不要散落在各种业务代码里。比如void load_level_data(uint8_t level_id) { // 切换到对应 ROM bank SWITCH_ROM(level_bank); // 从该 bank 读取地图数据 }不同的 MBC 类型有细微差异最稳妥的路径是先查你的卡带选择对应的 GBDK 内存映射宏并在模拟器里验证切换是否成功。9.3 OAM 要集中管理不要让每个模块直接调用 move_sprite 和 set_sprite_tile。更稳妥的做法是建立一个简单的精灵管理器统一给角色、敌人、道具分配 OAM 槽位。这样你能清楚地知道哪里有槽位、哪里超限。尤其是敌人数变化时集中管理可以避免“一个模块把另一个模块的精灵覆盖掉”的隐性 Bug。9.4 保留版本历史和复盘记录自制游戏很容易陷入“改一个参数然后视觉看不出问题于是不提交版本”的陷阱。但内存和时序问题往往在改过很多次参数后突然爆发。建议从项目第一天就用 Git每个可运行版本都打 tag。Game Boy 开发的一个重要能力是复现问题如果你无法回到出问题的版本很多 Bug 会变成玄学。9.5 不要过早优化在 Demo 阶段你完全不需要满屏对精灵、复杂的录放、异步脚本系统。先把移动手感调到舒服再把碰撞边界调对。Game Boy 的性能确实有限但多数新手项目的卡顿问题不是硬件计算力不够而是把大量不必要的工作塞进了主循环。先精简算法再谈汇编优化。10. 总结与后续实践路线《黑城堡 2》这个项目技术上真正值得记住的不是某个华丽效果而是一套闭环理解硬件边界设计适合硬件的玩法用 GBDK 写主循环和碰撞用工具链生成 ROM再用模拟器和调试器验证。这个过程任何一步缺失项目都会卡住。如果你是从零开始建议不要一上来就瞄着几十关的目标。先把文中的最小框架跑通然后在单场景里加对话、敌人、道具、门锁和一个小谜题。跑通之后再考虑用 ROM bank 加载第二张地图。这套路径的可控性很强每一步都能产出可玩的成果也方便随时回滚。接下来的学习方向主要看你的目标。如果你对引擎层更感兴趣可以研究 ZGB 的源码看它如何封装精灵和地图如果你对极限性能感兴趣可以学习 RGBDS 汇编把手写精灵滚动和 DMA 传输做到极致如果你更关心游戏设计那就多玩几款经典 Game Boy ARPG分析它们在同样硬件限制下的通关路径设计。Game Boy 自制游戏这个领域不高产但每一部作品都在用实际上手的代码回答“老硬件能不能承载新创意”这个问题。慢慢来把主循环跑对你手里就有了一台属于自己的小小游戏机。
返回列表