目标
目前macOS本地端有一个能运行俄罗斯方块的GameBoy模拟器,希望移植到浏览器端来玩
成果
操作方法:
- 回车键(Enter/return)开始或暂停游戏
- 方向键(←↓→)控制方块位置
- Z键和X键转动方块
按键映射关系:
| GameBoy按钮 | 键盘按键 |
|---|---|
| UP | ↑ |
| DOWN | ↓ |
| LEFT | ← |
| RIGHT | → |
| A | X |
| B | Z |
| START | Enter/return |
| SELECT | Tab |
方案
我在macOS下实现了一个基于C语言的gameboy模拟器,它基于SDL2来实现游戏的UI和画面。其CPU已经通过了Blargg‘s test,并能够在本地运行经典的俄罗斯方块游戏。现在我希望将这个模拟器移植到web端,这样就可以在浏览器中玩了。
为了实现这个想法,我的直觉是重新写一套完整的前后端服务。但是考虑具体的前后端时,发现完全没有头绪。在后端方面,有哪些数据需要进行前后端交互,如何获取,如何构造传输形式……这些问题令人头大。另一方面,前端也很复杂,对于我这个前端菜鸟,没有能力靠自己把读来的数据重新绘制成游戏。我问了很久的ChatGPT,对于全栈服务这套方案,它也只能给出一些大致的步骤和思路。
因此,我不断尝试询问,并把询问的关键词从“实现一套前后端系统”换成了“移植”“输出到浏览器”这一类提问,最终我找到了突破口:webAssembly和emscripten。
webAssembly(wasm)很通俗易懂,就是web端运行的assembly(汇编语言)。熟悉虚拟机和解释型语言的人一看便能心领神会,它的思想就是把诸如C, Java, Go, Rust这样的语言编译为一种浏览器的“汇编语言”。
选择编译器
emscripten就是这样一种webAssembly的编译器,它能够把C/C++编译为webAssembly,并且支持SDL2库,完美符合我的需求
嵌入游戏Rom
由于wasm的特性,所有运行时的数据在编译完成时已经确定,不能像本地模拟器那样在运行时去访问磁盘文件中的游戏ROM。因此我使用了xxd工具,将俄罗斯方块的ROM文件转换为C语言的数组形式,然后在C代码中引用这个数组,这样相当于把俄罗斯方块直接嵌入了这个模拟器。
Tetris.h
|
|
折腾Cmake
添加emscripten编译时所需的链接项,使用修改cmake源的方式来切换编译方式(clang和emcc)。在无止境的修复编译bug的过程中,我发现原始的项目过于复杂,导致emscripten编译起来非常麻烦。
不断做减法
原项目的以下特点在本地运行时没问题,但对于emscripten而言,要么编译难产,要么编译通过但运行效果差,我需要将他们逐一地做减法
去除冗余
原项目不知为何引入了SDL_TTF依赖,不仅没有用而且会导致emscripten编译失败(和-s USE_PTHREAD选项同时使用会报错,浪费了我许多时间),删掉。
多线程与本地调试
原项目为了保证ui和cpu独立执行,引入了pthread库来将cpu单独一个线程执行。尽管emscripten官网声称目前他们已经支持了pthread,使用-s USE_PTHREAD选项进行编译即可。但在我实际编译出来的web程序中,线程无法被成功创建。因此我需要把原来的多线程模拟器修改为单线程的,减小这个问题的复杂度。
这个减法需要我对模拟器的组件配合有更深的认识,因此我需要对它进行本地调试。最终选择了macOS的lldb编译器进行调试,比我想象的要简单,安装vscode的CodeLLdb插件,编写一下项目的调试配置文档就能顺利运行了:
.vscode/launch.json
|
|
.vscode/tasks.json
|
|
调试窗口
web端无法像本地一样同时渲染两个窗口,因此在emscripten编译时,取消tile的debug窗口输出,发现游戏窗口可以初步展示,游戏可以初步玩,但是画面大小和帧率还不满意
ui.c
|
|
调试输出
在emscripten编译时删除printf输出,大幅提高了游戏帧率(猜想是浏览器运行机制限制了内存量,堆积的printf输出占用内存,严重影响了模拟器的运行)
io.c
|
|
改写主循环
为了适应emscripten的调用形式,将ui.c的内容移到gbemu/main.c中,并改写主循环。原理有待研究,好像与js的回调机制有关或类似
main.c
|
|
未来计划
- 优化游戏页面
- 编写一个index.html长期用,替换目前每次编译输出的gbemu.html,使得index.html可以调用编译输出的的wasm和js文件。在index.html中提示操作方法等
- 优化游戏体验
- 目前web端按键有时候需要按两次才有反应,帧率目测也没有达到60FPS,需要进一步研究单线程版的模拟器
- 实现MBC
- 支持塞尔达传说这种ROM容量超过32KB的游戏
- 支持原项目的两个输出窗口
- 研究一下emscripten改写主循环的原理
- 比较wasm版和原版的性能