原文在:NES 模拟器实现指南(零) - Name1e5s的文章 - 知乎 https://zhuanlan.zhihu.com/p/34636695
Name1e5s
伟大的理想是你上吊的绳
66 人赞了该文章
Family Computer (ファミリーコンピュータ),缩写为 Famicom (ファミコン),是日本任天堂公司推出的一种第一代家用游戏主机,在国内常被称为红白机。红白机有两种,一种是日本版,体积较小,机身以红色和白色为主,俗称“红白机”;另一种是欧美版,体积较大,机身以灰色为主,称为 Nintendo Entertainment System,简称NES。两套机器的主要差别是支持的视频制式不一致,以及卡带的形状不同。在上世纪八十年代,红白机曾是世界上使用最广泛的游戏终端。自其从 1983 年发布至 1993 年停止维护,红白机将电子游戏带入各家各户,并推动了电子游戏最初的发展。
尽管自红白机以来,科技已经进步了不少,我们能使用最新的技术制作出足以以假乱真的游戏画面,能够利用相当于当初 FC 卡带几百万倍的存储空间来存储游戏内容。但是,那个时代的 FC 游戏依然以其卓越的可玩性吸引着各个年龄的玩家。超级马里奥兄弟,洛克人,魂斗罗仍然是难以逾越的经典之作。
当然,现代的操作系统以及硬件已经无法直接运行 FC 游戏。不过好在我们可以通过使用软件模拟 NES 主机的硬件来让游戏运行在现在的电脑上。这类软件便被称为模拟器。现有的 NES 模拟器中较为著名的有全平台的 FCEUX,Android 上的 Nesoid,以及 Windows 专供的 VirtuaNES。笔者一直使用的便是 VirtualNES。本文的目的便是实现一个简单的 NES 模拟器。笔者在演示时使用的语言为 Go 语言,当然读者若是想自行实现的话可以使用其擅长的任意语言进行。
iNES 文件(拓展名 .nes,大小写均可)是 NES 游戏分发的事实标准。该文件标准的最初是由 Marat Fayzullin 为其模拟器 iNES 而开发的文件格式。要实现一个 NES 模拟器,我们要做的第一步就是读取 iNES 文件,并将之映射到内存中以备使用。
我们首先要做的是创建 NES 文件的文件头结构体。NES 文件的前 16 个字节是文件头。其中:
0 = 0x4E (N)
1 = 0x45 (E)
2 = 0x53 (S)
3 = 0x1A (^Z)
模拟器依靠这个确定文件的格式。
0 -> Mirror Type ( 1 为水平, 0 为垂直)
1 -> 是否存在 battery-backed RAM ( 1 则为存在,映射到 $6000-$7FFF)
2 -> 是否存在 trainer (同上,映射到 $7000-$71FF)
3 -> 是否存在 VRAM
4-7 -> Mapper Type 的低四位
*0 -> 卡带是否含有 VS-System
*1-3 -> 保留,但必须全为 0
4-7 -> Mapper Type 的高四位
在上文中,暂不需要读取的区段笔者已经使用星号(*)标出。出现的词汇的含义会在以后的文章中逐步介绍。根据上文信息,现在我们即可写出文件头的结构体。如下:
const NESMagicMumber = 0x1a53454e //"NES^Z"
type NESFileHeader struct {
MagicNumber uint32 // NES Magic Number,must be 0x1a53454e
PRGNum byte // PRG-ROM banks number
CHRNum byte // CHR-ROM banks number
Ctrl1 byte // Control
Ctrl2 byte // Control too
RAMNum byte // RAM number (8KB each)
_ [7]byte // Empty bytes. Not used at this tume but MUST BE ALL ZEROS or games will not work.
}
并写出相应的读取文件头片段:
file,err := os.Open(path)
if err != nil {
return nil,err
}
defer file.Close()
header := NESFileHeader{}
// Read header
if err := binary.Read(file,binary.LittleEndian,&header) ; err != nil {
return nil, err
}
if header.MagicNumber != NESMagicMumber {
return nil , errors.New("Magic Number is Wrong.Invilid iNES file.")
}
处理完文件之后我们需要一个暂时的 NES 卡带结构来将我们读取到的内容存储到内存中,我们只需要写出来目前需要读取的部分即可:
type Cartridge struct {
PRG []byte
CHR []byte
Mapper int
Mirror int
Battery bool
}
之后便是按照上面的说明读取各个变量
// mapper type
mapper1 := header.Control1 >> 4
mapper2 := header.Control2 >> 4
mapper := mapper1 | mapper2<<4
// mirroring type
mirror1 := header.Control1 & 1
mirror2 := (header.Control1 >> 3) & 1
mirror := mirror1 | mirror2<<1
// battery-backed RAM
battery := (header.Control1 >> 1) & 1
以及计算各个 ROM 块的个数,分配空间
// read trainer if present (unused)
if header.Control1&4 == 4 {
trainer := make([]byte, 512)
if _, err := io.ReadFull(file, trainer); err != nil {
return nil, err
}
}
// read prg-rom bank(s)
prg := make([]byte, int(header.NumPRG)*16384)
if _, err := io.ReadFull(file, prg); err != nil {
return nil, err
}
// read chr-rom bank(s)
chr := make([]byte, int(header.NumCHR)*8192)
if _, err := io.ReadFull(file, chr); err != nil {
return nil, err
}
// provide chr-rom/ram if not in file
if header.NumCHR == 0 {
chr = make([]byte, 8192)
}
最后将这些代码片段一封装为函数,即可完成读取 iNES 文件这一操作。十分简单。
本段的全部代码可以在这里找到。
Reference: