Code Monkey home page Code Monkey logo

fc-simulator's Introduction

FC-simulator

用go实现一个小霸王/NES/FC/红白机模拟器

支持情况

已支持mapper0/1/2/3/4的游戏,如冒险岛/沙曼陀蛇/魂斗罗/超级马里奥等大部分常见游戏

音效

支持音效

GUI

选择了fyne.io

桌面版使用方式

注意要先安装 portaudio; 在mac环境下安装方式:brew install portaudio

源码 go run main.go /User/xxx/xxx.nes 二进制文件 ./main /User/xxx/xxx.nes

web版本

除桌面版外,还完成了可立即体验的web版本:

桌面版操作

系统按键:
Q   重置游戏
-   缩小画面
=   放大画面

手柄1:
W/S/A/D  上下左右
F/G   游戏A/B键
R/T  选择/暂停

手柄2:
方向键  上下左右
J/K  游戏A/B键
U/I  选择/暂停

效果展示

fc-simulator's People

Contributors

55utah avatar

Stargazers

 avatar  avatar  avatar Mark Ma avatar  avatar fly9i avatar  avatar  avatar  avatar  avatar KunYi Chen avatar treker avatar mantou avatar 无枝 avatar vhxn avatar  avatar  avatar  avatar  avatar  avatar Luz avatar Yain avatar kong avatar Longyong Wu avatar DaBaiCai avatar  avatar Mickey avatar DeepKolos avatar Yiming Pan avatar Bradley Xu avatar

Watchers

James Cloos avatar  avatar

fc-simulator's Issues

国庆进展

说明

国庆3天时间尝试实现,参考其他同学的ts实现,慢慢发现nes模拟器实现相当复杂,超出预期难度。

问题:

  • 只有权威的英文文档,文档很难读懂,需要攻克,而且教程文字量极大;
  • 实现非常复杂,每个小部分都需要很大时间去完成;
  • 难以测试;

计划

后续需要慢慢重新实现下,时间大概为1个月,路线:
cpu -> ppu -> 整体运行 -> 控制器 -> apu

【分享文档-去掉图片版】写个模拟器,在浏览器上玩赤色要塞

我做了什么

  1. 历时两个月,基于go语言实现了一个桌面端FC模拟器,模拟器支持大部分常见FC游戏,支持音效(音效是灵魂!)。
  2. 基于已实现的模拟器,编译生成wasm,并成功将模拟器运行在浏览器上,实现在浏览器玩FC游戏的目标。
    以下主要介绍FC模拟器历史、原理和实现的一些细节,希望可以解答大家的疑惑,两个开源项目和对应的体验方式将在最后放出。

历史篇

起源

红白机是任天堂公司在1983年于日本推出的家用游戏机系统,官方名称Family Computer,史称FC。后续发布美版(英文版名:Nintendo Entertainment System,即NES),红白机对电子游戏产生了深远影响,累计销售了6700万台,也奠定任天堂在游戏机史上的地位。红白机被称为“电子游戏历史上影响力最大的一台游戏主机”。
以下分别是日版(俗称红白机)和美版(俗称灰机):
(图略)

什么,你小时候玩的小霸王是山寨机

红白机在国际市场上获得巨大成功,港台和内地开始出现山寨版,因为价格便宜,需求强烈,迅速占领市场,最出名的就是我们熟知的“小霸王”。
我们常说的FC/NES/红白机/小霸王其实算是同一个游戏平台。
[图片]
[图片]
小霸王公司创办人是步步高电子创始人段永平(说吧,你圈了8090后多少钱),小霸王依靠低廉的价格和学习机的噱头迅速成长,在1991年把广告打到了央视黄金时段(成龙大哥也有赚钱的广告),到1999年小霸王累积销量已达2000万,开机音乐“噢~ 小霸王,其乐无穷”已经响遍大江南北,至今还会在这一代人脑海中挥之不去。“小霸王”也成为我们对那一代山寨红白机的统称。虽然游戏机是山寨的,但童年的快乐是真的。红白机及其山寨机给全世界的无数孩子们带来了快乐,为我们留下了宝贵的童年记忆。
红白机作为时代的产物,依靠出色的设计,在非常有限的性能下却支持了那么多生动有趣的游戏,可谓是榨干了机能,收获了一大批玩家喜爱。不过在后期,红白机逐渐式微了,逐渐被街机、电脑游戏取代了,但它留给我们的游戏回忆依然难忘,启蒙了一大批热爱游戏的玩家。

FC游戏

下面FC游戏中哪一个是你的童年最爱呢😏 ?
超级马里奥兄弟、赤色要塞、魂斗罗、超级魂斗罗、双截龙、冒险岛、沙罗曼蛇、洛克人、炸弹人、热血系列、忍者神龟、街霸、快打旋风、坦克大战、打气球、敲冰块...

还记得“上上下下左左右右BA”吗
魂斗罗初代是KONAMI公司发布的游戏,在游戏内的秘籍就是“上上下下左左右右BA”,可以让游戏人物获得30条命,这条秘籍流传甚广,影响之深远,甚至比KONAMI公司本身还要有名。《英雄联盟》中的“男枪”法外狂徒·格雷福斯也有一句“上上下下左右左右BABA,哈哈!我有三十条命了!”的台词。说起魂斗罗,它的续作就是超级魂斗罗,魂斗罗系列两个游戏角色原型是施瓦辛格和史泰龙。

《魂斗罗》《赤色要塞》《绿色兵团》《沙罗曼蛇》四款游戏作为流传很广,被后来的人称为“FC老四强”。
FC游戏史上诞生了无数优秀游戏,承载了太多人的童年回忆,记忆中和小伙伴一起通关魂斗罗,一起玩热血格斗,那大概就是最快乐的回忆了。FC游戏的输出其实是256*240像素大小的,就是宽256像素、高240像素,经过电视机缩放,就产生了像素感,这样的像素游戏就算是如今也层出不穷,极富魅力。以前玩的游戏一般都是英文的,小部分是日文的,这就是因为这些游戏主要山寨于美版游戏,小部分是来自于日版,记得小时候玩日版的热血格斗,看不懂日文尝试了很久才正式进入游戏。其中很多游戏就算放到现在来玩,都会惊艳于其丰富的细节和优良的游戏性。

游戏厂商

提及一些FC游戏知名的游戏厂商和他们的代表作:KONAMI科乐美公司,制作了魂斗罗、沙罗曼蛇等游戏;Hudson公司,制作了冒险岛、炸弹人、忍者龙剑传等;最后介绍下CAPCOM卡普空公司,开发过魔界村、洛克人、吞食天地、松鼠大作战等,直到现在还在游戏界发光发热。
关于卡普空推荐阅读:https://zhuanlan.zhihu.com/p/138010842

游戏卡带

游戏卡带也称游戏ROM,基本就是一块只读存储卡,插卡后,红白机从卡带中读取程序执行。但是你知道超级马里奥兄弟只有64kb,魂斗罗只有128kb吗,这么小的游戏体积却能承载如此庞大的游戏内容,这其中蕴含了红白机前辈们对性能的极致优化,我们后面慢慢解释。
小时候见的最多的是黄色的FC卡带,其实这是山寨卡带,其他的浅蓝色、灰色等等其实绝大部分都是山寨厂商做的山寨卡带,大部分做成黄色据说是因为黄色塑料便宜,还有说是因为塑料老化后会变黄,所以干脆用黄色的。山寨厂商还会用“N合1”这种方式吸引购买,实际往往只有几个小游戏重复。

模拟器

将卡带上的游戏内容读取出来,保存为.nes格式的文件,这个就是游戏本体的数字版了。看红白机的原名就知道,是有computer的野心的,实际也确实在原理上类似计算机,有专门处理计算的cpu芯片,也有处理视频和声音的芯片。既然都是计算,那么可以用软件模拟硬件实现,将红白机跑在如今的操作系统上面吗,当然可以,小时候就接触过windows下的VirtualNes,很受震撼。模拟器的本质,就是从游戏rom文件读取指令放到cpu内执行,然后接收键盘信号输入以及输出信号到屏幕和耳机等设备,这就是模拟器。模拟器这个东西搞懂了原理,剩下就是要处理巨大细节量的代码实现了。

实践篇

综述

FC模拟器本身其实相当复杂,原理学习和实现过程需要大量参考文档,这里参考了一系列博客,推荐:dustpg/BlogFM#5 和nesdev官方网站:https://wiki.nesdev.org/。
目前已经有不少优秀的开源FC模拟器了,它们的代码也可以作为我们的参考,有go/rust/js/ts等各种语言版本,不愁没有参考,(实际上,很多细节连官方网站都没有说清楚或者干脆就没有提到,写完跑不起来的时候还是得借鉴优秀开源项目的源码)。实际在开发中,会有大量的时间都在debug,FC模拟器debug是比较困难的。下面的原理介绍,我会略去一些非核心流程的部分。

FC模拟器原理

概览

节选自wiki:

FC使用一颗理光制造的8位2A03 NMOS处理器(基于6502**处理器),PAL制式机型运行频率为1.773447MHz,NTSC制式机型运行频率为1.7897725MHz,主内存和显示内存为2KB。FC使用理光开发的图像控制器(PPU),有 2KB 的视频内存,调色盘可显示 48 色及 5 个灰阶。一个画面可显示 64 个角色(sprites) ,角色格式为 8x8 或 8x16 个像素,一条扫描线最多显示 8 个角色,虽然可以超过此限制,但是会造成角色闪烁。背景仅能显示一个卷轴,画面分辨率为 256x240 ,但因为 NTSC 系统的限制,不能显示顶部及底部的 8 条扫描线,所以分辨率剩下 256x224。从体系结构上来说,FC有一个伪声音处理器 (pseudo-Audiom Processing Unit,pAPU),在实际硬件中,这个处理器是集成在2A03 NMOS处理器中的。pAPU内置了2个几乎一样(nearly-identical)的矩形波通道、1个三角波通道、1个噪声通道和1个音频采样回放通道(DCM,增量调制方式。其中3个模拟声道用于演奏乐音,1个杂音声道表现特殊声效(爆炸声、枪炮声等),音频采样回放通道则可以用来表现连续的背景音。

也就是说,FC只有2kb内存、2kb显存,是的,你没听错,总共4kb就能演绎童年的色彩。再看如今的PC设备随便都是16G内存,8G显存,赫然存在六七个数量级的差别。wiki提到的运行频率1.78MHZ,其实是指的CPU时钟频率,即每秒走1.78e6个时钟周期,而intel i7的CPU时钟频率是4GHZ左右。
在实现模拟器的过程中,会使用大量的位运算,是bit级别的数据处理,这也是模拟器难调试的一部分原因。

** 基本名词解析:**
CPU:**处理器模块,即2A03,基于6502处理器
PPU:图形处理器模块,主要用于图形控制显示等
APU:音效处理模块
Mapper:用来切换虚拟数据bank,最终达到扩展空间地址的效果
PRG-ROM: 程序只读储存器: 存储程序代码的存储器. 放入CPU地址空间.
CHR-ROM: 角色只读储存器, 基本是用来显示图像, 放入PPU地址空间
实现一个模拟器,其实就是实现CPU/PPU/APU这三个核心模块,再加上Mapper、控制器。最后再通过GUI实现掉画面展示,再实现声音播放即可。
就实现难度来说 CPU < APU < PPU。

ROM读取

模拟器要运行,就需要加载游戏rom文件,也就是.nes后缀的游戏文件。

文件头:
 0-3: string    "NES"<EOF>
   4: byte      以16384(0x4000)字节作为单位的PRG-ROM大小数量
   5: byte      以 8192(0x2000)字节作为单位的CHR-ROM大小数量
   6: bitfield  Flags1
   7: bitfield  Flags2
  flag1/2内包含了mapper编号、镜像信息等等内容,暂时不做展开
  后面的部分就是上面说的PRG数据块和CHR数据块了,分别存放游戏逻辑代码和角色图块像素信息。

CPU实现

** 地址空间模型 **
地址空间逻辑很重要,我们先来搞明白为什么说FC只有2kb内存,我直接拿博客的图来说明:
[图片]
上面就是CPU的地址分布,CPU地址是16bit的,也就是从0x0000-0xffff的地址范围,即64kb。但是实际CPU在计算中使用的内存却只有2kb。上面的图意思是CPU从0x0000~0x0800才是真正使用的内存区域,16进制的0x800大小就是十进制的2048,也就是我们说的2kb内存,剩下的0x800-0x1800全是镜像,再往上是寄存器、特殊用途的ROM、还有一大块就是PRG了,这里的SRAM是用来储存进度的(可以参考:https://www.zhihu.com/question/57147508/answer/151795411)。

CPU指令

CPU使用6502指令集,用一个时钟做输入源,时钟频率是1.79MHz,即每秒1.79 * 10^6个时钟单位,不同的指令消耗不同单位的时钟长度,CPU会在一个或多个时钟期间执行一条指令,然后去执行下一条。
cpu有6个内部寄存器:A,X,Y,PC,SP,P,A是累加器、XY是地址相关寄存器、PC是程序计数器(16bit)、SP是堆栈寄存器,P标志寄存器比较复杂,包含了进位、中断、溢出、负数等标志信息(除PC是16位其他都是8位)。
指令集:指令共256个,有少部分是非官方的,可不实现;
寻址模式:共13种寻址模式,比如绝对寻址就是指读取当前PC寄存器数据作为目标地址的寻址方式;
指令的来源就是上面的PRG块,这个数据块内存放着一条条6502指令集对应的汇编指令和操作数据,形式是1字节操作码 + 0~3字节的数据。
** 如何从PRG内读取指令和操作数呢?**

opcode = READ(cpu.PC) // 从当前程序计数器代表地址出读1byte信息,称为操作码,因为是1字节,所以最多有256种操作码。
cpu.PC++
size = instructionSizes[opcode]
cpu.PC += uint16(size)  
// 每种操作码有对应的指令长度,大小0~3,并且有对应的寻址模式,大小0~12。每种指令还有固定的时钟周期数(还有要计算得到的额外时钟数据)
下面以一个opcode和数据为例:
读取opcode -> 0x01 即1号指令,名称是"ORA",查表可知指令字节大小是2,寻址模式是7,所以再读取两个字节的操作数,最终得到三个字节:0x01,0xad,0x9a;
虽然指令集里指令有256个,但是很多是重复的指令,只是由于指令长度,寻址不同所以opcode对应为了不同的指令,实现的时候只需要实现一次即可。每个指令执行上下文都可以得到三个信息:寻址得到的address、寻址模式、下一个指令的地址。

每种指令内部在做什么呢?

以一个指令为例:

func (cpu *CPU) bit(info *stepInfo) {
  value := cpu.Read(info.address)
  cpu.setZ(cpu.A & value)
  cpu.V = (value >> 6) & 1
  cpu.N = (value >> 7) & 1
}

该指令读取寻址得到的地址里的内容,将内容与累加器计算修改上面提到的标志寄存器P的某个bit位,并修改溢出标志位V和负标志位N。

程序执行的过程就是不断取指令、分析指令、执行指令这个过程,比如程序的循环就可以控制下一条指令跳转到之前经过的地址,这样不断重复而实现。实际上,我们就连FC当年存在的bug都要实现掉,因为没有这个bug,游戏可能就跑不起来了(离谱)。

PPU实现

终于到了噩梦难度的PPU了,CPU在它面前还是太简单了。
PPU的时钟是CPU三倍,可以看到PPU计算量是更大的,要做更多事情。
我们重点解释为什么FC可以展现这么丰富的色彩图像,却占用了非常小的空间。了解PPU之前,我们来计算下,图像分辨率是256240每个像素如果用rgb储存那就是,每帧需要256240*3 = 180kb,远大于CPU/PPU寻址空间64kb,但是实际上FC把一张图像的编码压缩到了1kb大小。

扫描线原理

小时候的电视机都是基于“阴极射线显像管”,进行隔行扫描,现在视频网站可能有720p, 1080p的视频,p就是表示逐行(progressive)扫描。红白机PPU也是从上到下一行行计算像素,每行从左到右计算,最终得到一帧265*240 rgb像素的图像。每帧有262个扫描线,超出了屏幕高度240,每条扫描线有340个时钟周期,超出宽度256,实际上就是每个像素对应一个时钟周期,水平垂直方向都有时间不需要绘制内容,这些时钟周期在做什么呢?主要用于计算下次绘制的内容,中断下来让其他模块有时间完成自己的任务。

地址空间

地址0~0x2000是Pattern Table(图样表)有8kb图像信息,位于卡带,由mapper映射。0x2000~0x2fff共4kb数据,其中2kb就是显存VRAM,剩下的2kb是镜像(显存只有2kb的原因)。这里还有Name Table(名称表),Attribute Table(属性表)的概念。

背景渲染

调色板

FC理论上可显示64种颜色,颜色依靠索引获取,索引大小是32字节,前面16字节背景使用,后面16字节精灵使用,也就是说用16字节对应32个颜色,即一个颜色占4bit,换句话说一个像素仅仅需要4bit来描述,以下说明基于这个结论。
名称表Name Table
这个表就是用来显示背景的,有四个名称表,每个大小0x400即1kb,使用1byte表示一个88图块(称作tile),将屏幕划分成3230的地盘,也就是占用3230 = 960byte,具体使用哪个名称表由cpu通过PPU中大量的寄存器来控制。每个名称表还剩下64字节呢,也被利用起来,称为属性表。属性表的64字节再划分为88,每个小块分得1字节但又又被划分为2*2的区域,其中的每个最小区域只剩下了2bit(简直空间利用到极致),这里计算下,屏幕共960个tile,属性表划分成64块,每块要负责16个tile,因为又划分了4部分,所以每个最小的图块就要负责4个tile,即16 * 16像素,也就是每四个tile分得2bit。
图样表Pattern Table
图样表一般来映射自ROM中的CHR-ROM部分,每个'图样'使用16字节, 描述了一个8x8的图块。

 VRAM    Contents of                     Colour 
       Addr   Pattern Table                    Result
      ------ ---------------                  --------
      $0000: %00010000 = $10 --+              ...1.... Periods are used to
        ..   %00000000 = $00   |              ..2.2... represent colour 0.
        ..   %01000100 = $44   |              .3...3.. Numbers represent
        ..   %00000000 = $00   +-- Bit 0      2.....2. the actual palette
        ..   %11111110 = $FE   |              1111111. colour #.
        ..   %00000000 = $00   |              2.....2.
        ..   %10000010 = $82   |              3.....3.
      $0007: %00000000 = $00 --+              ........
      
       $0008: %00000000 = $00 --+
        ..   %00101000 = $28   |
        ..   %01000100 = $44   |
        ..   %10000010 = $82   +-- Bit 1
        ..   %00000000 = $00   |
        ..   %10000010 = $82   |
        ..   %10000010 = $82   |
      $000F: %00000000 = $00 --+

两个8字节的数据对应到bit位按上面的规则是bit0在上,bit1在下,得到一个两位的结果,范围0~3。
一个tile的8*8区域,每个像素的两个比特不是一起存放的,而是先把每个像素的低位保存一遍,之后保存高位的。我们以下面的这个“心形”为例:

将前面字节每个像素的bit作为低位,后面每个像素的bit作为高位,得到的两位bit,就是0~3范围了。
换句话说就是88像素的区域(1个tile)每个像素单独有2bit的信息,这2bit就是上面映射调色板所需要的4bit中的低两位,而高两位呢,也就是上面属性表最终计算得到的4个tile共用的那两个bit,总共4bit就这样凑齐了。这里可以看到由于这4个tile共用高两位,那它的颜色就完全由低两位决定,也就是说这1616像素的区域最多只能显示4种颜色。
名称表一个字节对应一个tile,值用来索引到图样表中,图样表大小0x1000即4kb,这样每16byte表示一个tile,则 0x1000/16 = 256 刚好是名称表一个字节可以表示的范围。

最后总结一下:图像分成了 32 x 30 = 960 个 tile,每个 tile 在 Name Table 名称表占前 960 字节。同时 tile的值表示 Pattern Table图样表 0 - 255 的偏移量,Pattern Table 又以 16 bytes 为一个单位,那么总共需要 256 * 16 = 4KB 大小的 Pattern Table。Pattern Table 一共 8KB,可分为两个 4KB 分别给 background 或者 sprite 使用,另外 16 个 tile 组成的大块中,每个由属性表的一个字节表示(即4个tile由2bit表示),一共需要 8 x 8 = 64 bytes。加上 name table 的 960,刚好 64 + 960 = 1024 字节,即 1KB VRAM
通过如此巧妙的设计,硬生生的将一个 320 x 240 的画面压缩到了 1KB,不得不服!

上面的描述更倾向于细节了,如果想要快速全面了解FC是如何处理图像的,包括精灵、场景滚动这些,可以看这个文章:https://zhuanlan.zhihu.com/p/34144965

背景滚动

参考:https://wiki.nesdev.org/w/index.php/PPU_scrolling
之前介绍的都是静态的情况,实际上游戏过程中画面都是运动的,这就靠 PPU 滚动来完成。
之前说过,PPU 一共 4 个 1KB 的 VRAM,他们组成田字布局,把屏幕想像成窗口,PPU 滚动的时候就相当于窗口在田字格上滑动,类似于这种效果:
这样就不要每一帧所有像素都重新计算了,可以充分利用已绘制出来的背景。也就是说FC将上面的两个名称表拼接起来,使用一个偏移量实现滚动,这个技巧也是FC实现画面的又一个核心技巧。FC游戏有一个“横屏卷轴游戏”的概念,很契合原理了呢。

精灵

画面的主角还是精灵,游戏角色和小怪的丰富动作都得靠精灵实现。这里推荐这个文章:https://zhuanlan.zhihu.com/p/419540831
一个精灵是一个88像素的tile(也有816的情况),精灵可以在屏幕中移动,每个精灵有自己的位置信息,但是我们看到很多游戏角色比较大,8*8像素不够,那么就只能用多个精灵拼接了:

可以看到小的马里奥是四个精灵,大的是8个精灵,精灵大小限制了FC游戏性能,所以一般FC游戏角色不能太大。
还记得上面说的每个tile其实最多就四种颜色吗,其中第一种必须是保留的透明色,所以实际只能有三种,我们来看下马里奥的颜色:

马里奥是红、橙、绿三色的,蘑菇是白、橙、红三色的。再多一种颜色都不行。另一个角色路易吉是马里奥的换色,在换色时,必须整体更换调色板:
所以路易吉的帽子和衣服必须同色,因为马里奥就是同色的,衣服帽子的颜色不可能不相同。更换调色板这个其实指的是更换模拟器中的调色板索引信息,这个索引列表在游戏开始后由CPU控制填充,并在需要的时候进行更改。
将精灵和背景一起绘制出来,游戏就可以跑起来了,如果不想实现音效,就可以简单实现下Controller控制器,马里奥就可以玩起来了,要支持更多游戏,就要实现更多Mapper才行。

** FC游戏的一些有趣的黑科技: **
从博客了解到,像《忍者龙剑传》中的过场动画通过应用一种屏幕分割技术打造出了大片级的效果:
1a788b35-3695-4d38-a2b1-b44065abc2b3

APU实现

声音才是FC的灵魂~
APU有五个声道,两个方波声道、一个三角波声道、一个利用线性反馈移位寄存器的噪声声道、一个DMC声道,APU时钟周期是CPU的一半,这些声道大量运用计时器,定时器与寄存器配合接收CPU发送过来的信息,然后按照固定的规则播放出去,也是属于输入量尽可能少,输出更丰富的设计方式。由于篇幅有限,不做展开了,只说下最后如何输出。
这些声道通过寄存器控制开启和关闭,最终的输出就是这些声道输出的混频,混频算法如下:
[略]

这里除了可以在每次输出声音时才计算之外,还可以使用查表法,预先计算完所有的可能,然后查表返回数据,用来提升性能,最终输出的信号是0~1之间的浮点数。

Mapper

上面说到,PRG 的寻址范围为 0x8000 - 0xFFFF,CHR 寻址范围为 0x0000 - 0x2000,他们大小分别为 32K 和 8K,对于大型游戏这么小的空间是远远不够的,任天堂在设计FC的时候就考虑到了这一点,设计了Mapper机制来支持拓展(有点像我们常见的插件**,或者说更像是适配器**),Mapper又被称为映射器。
Mapper的类型在卡带上,每个ROM文件都有固定的一种Mapper,而FC支持的Mapper有256种之多,如果你的模拟器不支持这个游戏的Mapper,那游戏就无法运行,难道必须要实现256个Mapper吗,那倒也不必,一般只要实现几个常用的Mapper,就可以玩大部分常见游戏了,至于编号很大的Mapper,一般是支持特殊游戏或者是小时候常见的“N合1”类型的卡带。推荐实现Mapper0~4五个即可。
Mapper的原理实际就是映射篡改,比如将CPU读取的0 - 0x0800这块区域在某个时机映射到卡带上另外的区域,通过这种映射更改,实际可以向FC内读取的程序和图形信息就增大了很大。

综述

以上,模拟器的核心代码就介绍完毕了。
至于将PPU中的画面显示出来、声音进行播放、键盘控制等就不是模拟器的内容了,需要由统一的GUI控制,这就看你选择的语言和平台了。最终选择好GUI库和音效实现库实现之后,才能真正的玩起来。由于go语言很差的GUI环境,也是踩坑无数,最终使用了一个叫做fyne.io的GUI,声音处理使用了使用广泛的portaudio。

将模拟器跑在浏览器上

我们知道 go/Rust/c 等语言都是可以编译出wasm的,从而有运行在浏览器上,基于这个初衷,历经艰难,最终把上面go实现的模拟器跑在了浏览器上,不过这条路也不是很顺利,总有些奇怪的问题,测试发现safari下体验会好很多,但是chrome下容易卡。

介绍

go语言有一个syscall/js的库,通过这个库我们可以在编译出wasm之后,在浏览器里面像js一样操作window/document这些全局对象,甚至修改dom;同时也能暴露给js接口,让js可以调用wasm里面的方法。所以我们在调用上面模拟器的同时,需要一些桥接的代码,主要负责将调用入口挂载到window上,同时控制输出输入。

在浏览器内运行wasm,不使用web-worker的情况下,wasm和js代码是跑在一个线程内的,wasm方法阻塞的话,js代码不会执行,屏幕就不会刷新,页面内容也就不会更改,所以wasm执行要有间隔。
方案将wasm放在js线程内跑,使用requestAnimationFrame来控制帧率,调用wasm暴露给js的模拟器运行方法,每次执行17ms对应的时钟周期,这样就可以在理论上保证页面渲染与模拟器时钟一致。

更新画面

经过测试,在wasm内调用canvas的API更新画面效率更高,所以这个更新方式完全由go实现。

document = js.Global().Get("document")
canvas = document.Call("querySelector", "canvas")
ctx = canvas.Call("getContext", "2d")
imageData := ctx.Call("getImageData", 0, 0, width, height)
buf := js.Global().Get("Uint8ClampedArray").New(width * height * 4)
dst := js.Global().Get("Uint8Array").New(len(value))
js.CopyBytesToJS(dst, value)
buf.Call("set", dst)
imageData.Get("data").Call("set", buf)
ctx.Call("putImageData", imageData, 0, 0)

键盘控制

键盘控制也由go控制,控制的方式就是监听document的keyDown和keyup事件。

声音播放

要在浏览器上播放模拟器输出的声音信号,经过调研,我们使用原生的AudioContext API就可实现,比较难解决的是缓冲区的问题,Audio的相关API有一个播放缓冲区,播放完之后出发一个audioprocess事件重新构建缓冲区内容,我们为了性能,尽量减少wasm与js之间互相的调用,所以在go内维护一个缓冲区,缓冲区满了之后再更新到js对象上,由Audio进行消费用这个数据重新建立缓冲区。

体验篇

分别提供了mac桌面版和web版体验方式,就体验效果和完成度来说,桌面版好于web版。
桌面版
项目开源地址:
https://github.com/55utah/fc-simulator
体验方式:
解压roms文件包,然后调用应用文件执行喜欢的游戏rom文件:

  1. 安装 portaudio
    mac下安装方式:brew install portaudio
  2. 给二进制文件授权
    chmod +x ./fc-simulator
    打开失败后,需在系统偏好设置 -> 安全与隐私 点击允许执行fc-simulator应用
  3. 桌面运行二进制文件
    ./fc-simulator ./nes-roms/魂斗罗.nes
    音效支持:
    支持良好
    推荐指数:
    🌟🌟🌟🌟🌟
    操作
系统按键:
Q   重置游戏
-   缩小画面
=   放大画面

手柄1:
W/S/A/D  上下左右
F/G   游戏A/B键
R/T  选择/确定

手柄2:
方向键  上下左右
J/K  游戏A/B键
U/I  选择/确定

web版
项目开源地址:
https://github.com/55utah/wasm-nes-web
在线体验地址:
https://55utah.github.io/wasm-nes/index.html
特别说明:

  1. web版实现并不完善,建议使用safari浏览器/FireFox浏览器打开,chrome浏览器更容易出现卡顿。
  2. 已知问题:部分低版本系统safari浏览器不支持WebAssembly.instantiateStreaming API导致无法运行。
    音效支持:
    支持得不太好,有明显的杂音,可手动关闭
    推荐指数:
    🌟🌟🌟
    操作方式:
    点击想玩的游戏开玩,按键同桌面端。

架构问题:

最近发现的博客:https://djharper.dev/post/2018/09/21/i-ported-my-gameboy-color-emulator-to-webassembly/ ,博主也将go编译的wasm跑在浏览器上,运行GameBoy模拟器,探索了使用webwoker运行模拟器,将等数据传输到主线程,在主线程监听键盘事件传递到worker,后续考虑尝试这个方案,提升性能。

BUG说明

在目前的测试中,发现还是有一些bug存在:
1.《沙罗曼蛇》底部状态栏横向滚动有点问题,但是不影响游玩,懒得定位了;
2. 部分游戏精灵在某些情况会出现不同程度闪动;

类型mac下CPU占用mac下总CPU占用(6核)windows下总CPU占用桌面版100%左右17%--web版110%左右18.5%24%

拓展篇

xBRZ插值

参考:https://www.luogu.com.cn/blog/sjx233/xbrz-interpolation-explained
是一种图像整数倍放大的插值算法,大家体验过程可以发现放大窗口之后,人物颗粒化严重,这个算法就是结局这种情况的,据说,很火的《动森》游戏就使用这种算法处理像素画(参考:如何把马赛克变高清?扒一扒《集合啦!动物森友会》中使用的图像放大算法)。

FC游戏3D化

相关网站: https://geod.itch.io/3dnes
https://www.destructoid.com/turn-your-nes-games-3d-with-this-free-emulator/

手柄支持

浏览器是支持手柄API的,完全可以在浏览器里面用手柄玩游戏。
https://developer.mozilla.org/zh-CN/docs/Web/API/Gamepad_API

附录

https://zh.wikipedia.org/wiki/%E7%BA%A2%E7%99%BD%E6%9C%BA#%E5%8E%86%E5%8F%B2
https://zh.wikipedia.org/wiki/%E7%BA%A2%E7%99%BD%E6%9C%BA%E6%B8%B8%E6%88%8F%E5%88%97%E8%A1%A8
https://xw.qq.com/cmsid/20201228A0NXEC00
https://www.ifanr.com/1326469
https://zhuanlan.zhihu.com/p/419540831
https://zhuanlan.zhihu.com/p/34144965
https://zhuanlan.zhihu.com/p/43999178
https://djharper.dev/post/2018/09/21/i-ported-my-gameboy-color-emulator-to-webassembly/
https://www.ifanr.com/1326469

【初步完成】

我的模拟器终于可以跑起来了 !!!
历时一个月,完成了CPU/PPU的核心能力,目前可以运行mapper0的FC游戏。

mario马里奥游戏测试:

20211115-194558

待办列表:

问题1: 屏幕缩放支持 + 暂停 + 加载需要的游戏
问题2: 更多游戏mapper支持和测试【需要支持mapper1\2\3\4即可】
问题3: 声音支持
问题4: 集成到web
问题5: 开源和分享

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.