用 F# 构建 Game Boy 模拟器:从学习原理到解决难题,体验模拟开发乐趣
Nick Kossolapov的开发背景截至2026年4月Nick Kossolapov做软件工程师已超8年但一直没搞懂计算机工作原理打算通过模拟计算机学习。因小时候花数百小时玩《宝可梦》选择Game Boy作为模拟对象。他先学习了《从与非门到俄罗斯方块》课程理解计算机基础知识还用F#构建了CHIP - 8模拟器Fip - 8。几个月后做出能正常运行、支持声音且可在桌面和网页运行的Game Boy模拟器Fame Boy。模拟器工作原理为让模拟器在桌面和网页运行设计了核心与前端简单接口包括两个数组160x144的灰度数组framebuffer、采样率为32768 Hz的环形音频缓冲区audiobuffer和两个函数stepEmulator()执行CPU指令并返回周期数、getJoypadState(state)提供游戏手柄状态。以类似Game Boy实际硬件方式构建Fame BoyCPU像Sharp LR35902大量运用函数式领域建模Memory.fs存储大部分RAM充当内存映射和总线IoController.fs处理硬件寄存器简化接口、提高安全性Emulator.fs中的stepper函数整合模拟器组件。模拟器单线程运行组件按顺序执行为流畅运行每秒需正确周期数前端开启声音用音频采样率驱动静音用帧率驱动。模拟CPU与F#的使用领域建模CHIP - 8模拟器是纯函数式Fame Boy大量使用可变性因Game Boy运行速度快复制内存不明智。选择F#是因其类型系统适合CPU指令建模且开发者喜欢F#。实现CPU参考Gekkio技术参考对指令分组将CPU指令从512个操作码减少到58条指令通过F#类型系统建模避免非法状态但操作码0x76是领域模型瑕疵。使用函数式语言有流畅感建议未使用者尝试。保持简单项目目标是学习硬件未深入研究其他模拟器代码。看到CAMLBOY代码中按任意顺序传递标志方式因F#类型系统无法创建相同方式采用类似方式但不满意。最终改进setFlags函数新函数性能更好使模拟器FPS提高约10%开发者认为16行的Flags模块是最喜欢的F#代码。测试一开始用简单函数处理CPU遇到异常实现对应指令但此方法有查找繁琐和不确定实现是否正确问题通过单元测试解决。AI帮助编写测试用例实现测试驱动开发还发现已实现指令的bug。CPU之外PPUGame Boy有PPU图像处理器开发者花更多时间关注像素。其他博客多关注CPUPPU需更长时间理解实现它是机械性工作。实现PPU从读取图块和背景地图开始此方法在各阶段有帮助。PPU实现虽满意但可能是硬件模拟最不准确部分Game Boy用FIFO队列显示像素模拟器每行绘制周期开始时渲染扫描线多数游戏能正常运行。游戏手柄游戏手柄最初实现轻松但重大重构后易出问题。早期CPU每个周期写游戏手柄状态效率低改成每帧更新后方向键失灵因游戏依赖寄存器变化读取按钮状态。最后让IoController仅在CPU读取时更新寄存器可能需编写集成测试。声音很难实现完成模拟器后添加APU音频处理器因认为用帧率驱动模拟器反直觉而做此决定。声音是最具挑战性部分AI帮助理解声音寄存器和声道工作原理实现声道有成就感但APU需选择和调整的地方多。