在1980年代中期,任天堂娱乐系统(NES)是必不可少的控制台。 当时所有控制台中最好的声音,最好的图形和最好的游戏-控制台扩大了可能的界限。 到目前为止,
Super Mario Bros等项目
。 ,
《塞尔达传说》和《
银河战士》被认为是
有史以来最好的游戏。
NES发布30多年后,经典游戏的感觉很棒,这不能说它们使用的硬件。 由于只有256x240的分辨率,NES控制台无法为游戏提供足够的空间。 尽管如此,无所畏惧的开发人员还是成功地融入了NES游戏中,令人难以忘怀的世界:《
塞尔达传说 》迷宫般的地牢,
《 银河战士 》中星球的广阔空间,《
超级马里奥兄弟》的明亮水准。 。 但是,由于NES硬件限制,播放器永远不能超过256x240 ...
直到最近。
我向您介绍了
wideNES项目-一种播放NES经典音乐的新方法!
wideNES是一项新技术,
可以 实时 自动地 交互式标记NES游戏。
当玩家在关卡中移动时,wideNES会记录屏幕,并逐步构建世界探索区域的地图。 在随后的级别中,wideNES将屏幕上的游戏玩法与生成的地图同步,从本质上允许玩家通过“超越” NES屏幕的边界来查看更多内容! 最重要的是,您对wideNES游戏进行标记的方式是
完全通用的 ,这使各种NES游戏无需任何配置即可与wideNES一起使用!
但是,这一切如何运作?
如果您想在阅读本文之前检查widthNES的工作原理,请!
ANESE是我编写的NES模拟器,目前是唯一实现wideNES的模拟器。 但是,值得警告的是,就UI和仿真准确性而言,ANESE
并不是世界上最好的NES模拟器。 大多数功能(包括包含wideNES)只能通过命令行使用,尽管许多受欢迎的游戏都能正常工作,但其他一些游戏的行为却可能出乎意料。
NES的工作原理
在深入研究细节之前,重要的是简要解释NES如何渲染图形。
使用PPU进行像素传输
NES的核心是古老的MOS 6502处理器,在70年代末和80年代初,6502
随处可见 ,并在Commodore 64,Apple II等传奇机器中工作。 它便宜,易于编程且功能强大
到足以引起危险。
在NES控制台中补充6502是一个功能强大的图形协处理器,称为
图像处理单元 (PPU)。 与旧系统上使用的简单视频协处理器相比,PPU在可用性方面有了巨大的改进。 例如,在发布NES的五年之前,Atari 2600处理器6502被用于将图形指令传输到
每个光栅行的协处理器,这使处理器几乎没有时间执行游戏逻辑。 为了进行比较:PPU
每帧只需要几个命令,这给6502提供了足够的时间来创建有趣和创新的游戏玩法。
PPU是一个了不起的芯片,其渲染图形的方式几乎与现代GPU的工作不同,并且
将需要一系列完整
的文章来全面说明其功能。 由于wideNES仅使用PPU功能的一小部分,因此只需简要考虑一下即可:
- 分辨率:256x240像素,60 Hz
- 它独立于CPU工作
- 使用具有内存映射的I / O与 CPU进行通信(地址范围0x2000-0x2007)
- 2个渲染层: 精灵层和背景层
- 精灵层
- 每个精灵可以放置在屏幕上的任何位置。
- 非常适合移动物体:玩家,敌人,炮弹
- 多达64个8x8像素精灵
- 背景层
- 绑在网格上
- 非常适合静态元素:平台,大障碍物,装饰品
- 视频内存足以存储大小为8x8像素的64x30瓦片
- 真正的内部分辨率512x240,视口为256x240
- 支持硬件滚动以更改256x240视口
- PPUSCROLL寄存器(地址0x2005)控制X / Y中视口的移位
在处理了这个
非常简短的概述之后,让我们继续进行最有趣的事情:wideNES是如何工作的?
主要思想
在每个帧的末尾,CPU将更改信息发送到PPU。 其中包括新的精灵位置,新的关卡数据,以及对于newNES至关重要的
新视口偏移 。 由于wideNES可以在仿真器中运行,因此我们很容易跟踪写入PPUSCROLL寄存器的值,这意味着计算屏幕在任意两帧之间移动了多少非常容易!
嗯,如果不是将每个新帧
直接绘制
在旧帧上,而是将新帧
叠加在前一帧上绘制,而是移动到当前滚动值,将会发生什么情况? 然后,随着时间的流逝,关卡中越来越大的部分将保留在屏幕上,逐渐构建关卡的完整图片!
为了检查这个想法是否有价值,我快速草绘了第一个实现。
编译...
发射中...
下载
超级马里奥兄弟。 ...
瞧!
奏效了!
好像是...
另一种方法:为什么不直接从ROM文件中提取级别?
甚至无需考虑实现细节,很明显,该技术存在严重的局限性:只有当玩家独立探索整个游戏时,才能收集完整的游戏地图。
如果有某种方法可以从
原始 NES ROM中提取电平,该怎么办?
这样的技术还能存在吗?
好吧,很可能不会。
如果您为NES玩任何两款游戏,则可以保证它们只有一个共同点-它们都适用于NES。 其他一切都可以完全不同! 这样的不匹配是一场真正的灾难,因为NES游戏本质上具有用于存储关卡数据的无限多个选项!
有些人通过逆向工程以存储
几个游戏的关卡数据的方式提取了完整关卡(有时会创建全功能的
地图编辑器 !),但这是一项艰巨的任务,需要大量的工作,毅力和智慧。
为了从ROM中提取级别数据,必须确定ROM的哪些部分是代码(不是数据),这很难做到,因为
在二进制文件中查找所有代码等同于停止问题 !
WideNES使用更简单的方法:wideNES无需猜测游戏如何将关卡数据打包到ROM中,而是直接启动游戏并跟踪输出!
滚动到255以上
NES是一个8位系统,即PPUSCROLL寄存器只能接收8位值。 这将最大滚动偏移量限制为255个像素,即最大8位数字。 NES屏幕分辨率为240x256像素,这不是巧合,即255像素的偏移
量足以滚动整个屏幕。
但是,滚动
超过 255会发生什么?
首先,游戏将PPUSCROLL寄存器重置为0。这解释了当Mario向右移得太远时,为什么将
SMB传送到开头。
然后,为了补偿8位的PPUSCROLL限制,游戏将更新另一个PPU寄存器:PPUCTRL(地址0x2000)。 PPUCTRL的后2位以全屏增量设置当前场景的“起点”。 例如,写入值1将视口向右移动256像素;值2将视口向下移动240像素。 使用PPUSCROLL寄存器将PPUCTRL偏移量压入
堆栈 ,这使您可以在512像素内水平滚动屏幕或在480像素内垂直滚动屏幕。
但是构建时,是否只有足够的视频内存用于两级屏幕? 当视口向右滚动太远并“超出” VRAM时会发生什么? 为了处理这种情况,PPU进行了卷积:将所选视频存储器之外的所有视口部分简单地折叠到视频存储器的相对边缘。
这种折叠结合智能的PPUSCROLL和PPUCTRL寄存器操作,使NES游戏能够创造出无限高大世界的错觉! 由于延迟将部分关卡延迟加载到查看窗口之外并逐步滚动到其中,因此玩家从未意识到在VRAM内部他们实际上是“绕圈运行”!
nesdev Wiki上的一个极好的插图显示了《
超级马里奥兄弟》的制作过程。 使用这些属性创建的级别超过两个屏幕:
让我们回到我们正在讨论的问题:wideNES如何处理超过256的滚动?
好吧,坦率地说,wideNES会
完全忽略 PPUCTRL寄存器,而只是跟踪帧之间的PPUSCROLL差异!
如果PPUSCROLL意外地跳到256,这通常意味着玩家的角色在屏幕上向左/向上移动,并且如果他意外地跳到了大约0,则通常意味着玩家在屏幕上向右/向下移动。
尽管这种启发式方法看起来很简单-的确如此-实际上,它很好用!
实施了这种启发式后,
超级马里奥兄弟。 ,《
银河战士》和许多其他游戏几乎完美运行!
我很激动,所以我继续上传了另一个NES经典作品-
超级马里奥兄弟。 3 ...
嗯...不是很漂亮。
忽略静态屏幕元素
许多游戏在屏幕边缘都有静态UI元素。 对于
SMB3,这是左侧的一列,状态的底部是状态栏。
默认情况下,从屏幕边缘以16像素为增量递增的widthNES采样,即,对边缘的所有静态元素进行采样! 不好!
为了解决这个问题,wideNES实施了规则和试探法,试图自动识别和掩盖静态屏幕元素。
通常,NES游戏使用三种不同类型的静态屏幕元素:HUD,蒙版和状态栏。
HUD-没问题
如果游戏在某个关卡之上强加了HUD,则HUD可能包含多个子画面。 示例:
Metroid中的 HUD。
幸运的是,这样的HUD不会引起问题,因为wideNES当前只是忽略了子画面层。 太好了!
口罩-轻松无比
PPU具有允许游戏掩盖背景层最左边8个像素的功能。 通过设置寄存器的第二位(地址0x2001)将其激活。 许多游戏都使用此功能,但是解释
为什么这样做超出了本文的范围。
识别包含的掩码非常简单:当寄存器中的第二个位置1时,wideNES只会跟踪PPUMASK值,而忽略最左边的8个像素!
似乎实现此简单规则可以解决
SMB3的问题:
...很好,或
几乎被淘汰。
状态栏最难
由于PPU在屏幕上任何给定时间的限制,最多只能有64个精灵。 而且,任何时候,
每条栅格线中最多只能包含8个精灵。 此限制阻止开发人员根据子画面创建复杂的HUD,并迫使他们使用背景层的一部分来显示信息。
除了遮罩外,PPU中没有简单的方法将背景层分为游戏区域和状态区域。 因此,开发人员开始花样,导致产生了一系列
非常规的方式来创建状态面板...
WideNES使用各种启发式方法来识别不同类型的状态面板,但是为了节省时间,我将仅考虑最有趣的一种:中帧IRQ跟踪。
中帧IRQ跟踪
与具有大型内部帧缓冲区的现代GPU不同,PPU
通常没有帧缓冲区! 为了节省空间,PPU将场景存储为8x8像素的64x32瓦片网格。 无需预先计算像素数据,而是将切片存储为
指向 CHR存储器(字符存储器)的
指针 ,该寄存器包含所有像素数据。
由于NES是在80年代开发的,因此PPU的创建没有考虑现代显示技术。 PPU而不是同时渲染整个帧,而是输出NTSC视频信号,该信号应显示在CRT屏幕上,该CRT屏幕
逐像素 ,
逐行 ,从上至下,从上至下,从左至右显示视频。
为什么这一切都很重要?
由于PPU逐行从上到下渲染帧,因此您可以将PPU指令发送到
中间帧以创建其他任何方法都无法实现的视频效果! 这些效果可以很简单(例如,更改调色板),也可以很复杂(例如,您猜到了,创建状态栏!)。
为了解释中帧PPU写入如何创建状态栏,我记录了单个
SMB3帧的原始PPU和CHR内存视频切片转储:
一切看起来都很好,没什么特别的……只是看看状态栏! 她完全扭曲了!
现在看相同的原始转储,但在第196行之后制作的...
是的,等级看起来很糟糕,但是状态栏看起来很棒!
这是怎么回事
SMB3设置了一个计时器,恰好在渲染195栅格线之后触发IRQ(中断),并将以下指令传递给IRQ处理程序:
- 将PPUSCROLL设置为(0,0)(以便状态栏保持原位)
- 我们将磁贴卡替换为CHR内存(按顺序排列了状态栏的图形)
由于层的其余部分已经渲染,因此PPU不会“重新更新”帧。 取而代之的是,它将继续使用这些选项进行渲染,显示出美丽的未变形状态栏!
让我们回到wideNES:通过观察帧中间的所有IRQ并记住发生它们的栅格线,wideNES可以忽略记录中的所有后续栅格线! 如果IRQ出现在240/2以上的光栅行中,则所有
先前的行都将被忽略,因为光栅线的早期中断意味着状态栏可能
位于屏幕
顶部 。
实施了这种启发式后,
超级马里奥兄弟。 3分完美!
我简要考虑了使用计算机视觉库(例如OpenCV)来识别状态面板(或屏幕的其他大部分静态区域)的可能性,但结果我决定放弃它。 使用庞大,复杂且不透明的计算机视觉库违反了wideNES的理想,在这种情况下,我尝试使用紧凑,简单且透明的规则和启发式方法来获得结果。
场景识别
除了一些著名的示例(例如
Metroid )以外,NES游戏通常
不会在一个巨大的,密不可分的层次上通过。 相反,大多数NES游戏被分为许多小的独立“场景”,它们之间带有门或过渡屏幕。
由于wideNES没有“场景”的概念,因此在更改场景时会发生坏事...
例如,这是
恶魔城场景的第一个过渡,西蒙·贝尔蒙特进入了德古拉的城堡:
哇,一切都不好! wideNES用新关卡的第一个屏幕完全重写了关卡的最后部分!
显然,wideNES需要某种方式来识别场景变化。 但是哪一个呢?
感知哈希!不同于通常在输出信息空间中均匀分布相似输入数据的
密码散列函数,
感知散列函数试图使相似的输入数据在输出数据空间中彼此保持“接近”。 因此,感知哈希是识别相似图像的理想选择!
感知哈希函数可能非常复杂,如果其中之一被旋转,缩放,拉伸以及其中的颜色发生了变化,它们中的一些就能识别相似的图像。 幸运的是,wideNES不需要复杂的哈希函数,因为可以保证每个帧都具有相同的大小。 因此,wideNES使用现有感知哈希中最简单的哈希:
将屏幕上的所有像素相加!很简单,但是效果很好!
例如,在
《塞尔达传说》中 ,查看绘制随时间变化的感
观哈希值,看看场景之间的过渡效果如何:
当前,wideNES使用感知哈希值之间的固定阈值来完成场景之间的转换,但是结果远非理想。 不同的游戏使用不同的调色板,并且在很多情况下,wideNES认为过渡已经发生,但实际上并非如此。 理想情况下,wideNES应该使用动态阈值,但到目前为止,固定阈值仍可以使用。
实施了这种新的启发式方法之后,wideNES成功地识别了西蒙从
恶魔城到城堡的入口,并因此创建了新的画布。
有了这个决定,我们就完成了WideNES难题的最后一个主要部分。
实现了最简单的序列化之后,我终于能够为NES运行游戏,可以在多个级别玩,并自动生成级别图!
将来有什么等待广阔?
wideNES由两个独立的部分组成:wideNES
内核 (这是该技术的基本规则/启发法),以及ANESE仿真器内部的wideNES的特定实现。
WideNES 核心增强
首先,wideNES倾向于过于激进地识别场景之间的过渡。 可以通过使用更合适的感知哈希算法或在感知哈希之间切换到动态阈值来最大程度地减少误报的数量。
识别静态屏幕元素还需要进行其他工作。
例如,《Megaman IV》在帧的中间有一个IRQ,但是没有状态栏,这就是为什么wideNES会错误地忽略比赛场地的坚实部分的原因。尽管可以通过手动调整来纠正这种特殊情况,但是最好使用更智能的启发式方法。某些NES游戏以“独特”方式滚动屏幕。最著名的例子之一是《塞尔达传说》,它使用PPUSCROLL进行水平滚动,但使用完全不同的寄存器进行垂直滚动-PPUADDR。塞尔达传说(Zelda)是一款相当受欢迎的游戏,因此WideNES专门针对塞尔达传说实施启发式。还有其他具有类似“独特”滚动模式的游戏,它们也需要单独的试探法。找到某种方式来“缝合”相同的场景将很有用。例如,如果用户玩《超级马里奥兄弟》。级别1,但爬入管道中并带着硬币进入地下洞穴,wideNES将为级别1创建两个单独的场景:场景A,级别直到马里奥进入装有硬币的区域,场景B,即刻开始当马里奥(Mario)离开水管到达旗杆时。如果游戏随后重新启动并且在不进入管道的情况下重新播放了1级,则wideNES将仅更新场景A,其中将包含完整级别的地图,而场景B将“中断”。最后,wideNES必须跟踪场景之间的过渡。没有这些数据,就不可能构造场景之间的过渡图来生成不包含单个大世界的游戏世界地图。改善ANESE中wideNES的实施
目前,wideNES仅在我以ANESE名义编写的NES模拟器中实现。 ANESE是一款非常出色的Spartan模拟器:大多数选项都隐藏在CLI标志后面,并且唯一实现的UI是最简单的文件选择覆盖!他仍然是非常从“生产”的水平为止。除了缺少UI,ANESE和wideNES外,兼容性和速度方面的改进也不会受到损害。 ANESE是我编写的第一个仿真器,非常引人注目!其中存在很多兼容性问题-许多游戏无法正常运行或根本无法启动。幸运的是,ANESE的不完善并不意味着WideNES是一项不良技术。 wideNES是基于久经考验的原理构建的,在其他仿真器中将易于实现!在速度方面,ANESE和wideNES并不完美,即使在功能相对强大的PC上,性能有时也可能低于60fps! ANESE和wideNES需要实施许多优化。除了对ANESE内核的总体改进之外,还需要改进WideNES帧记录,地图渲染和哈希采样。结论
在本文中,我讨论了wideNES的主要方面,但是无法描述许多小的功能。例如,wideNES存储每个帧的真实哈希值和滚动值的映射,用于启用重复场景。此功能和许多其他功能在WideNES项目页面上发布的广泛评论的wideNES源代码中进行了描述。在wideNES上工作确实是一次了不起的经历,但是随着沃特洛大学新学期的到来,我怀疑在不久的将来我能否继续开发wideNES。目前,wideNES的主要功能正在运行,我很高兴能够撰写这篇介绍其某些技术的文章!尝试使用wideNES并分享您的感受!下载ANESE,启动超级马里奥兄弟。,《塞尔达传说》或《银河战士》,然后以新方式玩!