fc-simulator
fc-simulator copied to clipboard
【分享文档-去掉图片版】写个模拟器,在浏览器上玩赤色要塞
我做了什么
- 历时两个月,基于go语言实现了一个桌面端FC模拟器,模拟器支持大部分常见FC游戏,支持音效(音效是灵魂!)。
- 基于已实现的模拟器,编译生成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模拟器本身其实相当复杂,原理学习和实现过程需要大量参考文档,这里参考了一系列博客,推荐:https://github.com/dustpg/BlogFM/issues/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游戏的一些有趣的黑科技: **
从博客了解到,像《忍者龙剑传》中的过场动画通过应用一种屏幕分割技术打造出了大片级的效果:
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文件:
- 安装 portaudio mac下安装方式:brew install portaudio
- 给二进制文件授权 chmod +x ./fc-simulator 打开失败后,需在系统偏好设置 -> 安全与隐私 点击允许执行fc-simulator应用
- 桌面运行二进制文件 ./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 特别说明:
- web版实现并不完善,建议使用safari浏览器/FireFox浏览器打开,chrome浏览器更容易出现卡顿。
- 已知问题:部分低版本系统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