一个用Go实现的NES模拟器 // NES emulator written in Go.

名称 nes
地址 Github
作者 fogleman
Brief Intro NES emulator written in Go.
LICENSE MIT
starts 2,816

介绍

这是一个使用Go实现的NES模拟器。

NES

依赖

1
2
3
github.com/go-gl/gl/v2.1/gl
github.com/go-gl/glfw/v3.1/glfw
github.com/gordonklaus/portaudio

portaudio-go依赖需要在系统中安装PortAudio

在ubuntu上,需要执行apt-get install portaudio19-0dev即可,在Mac系统,需要执行brew install portaudio

安装

使用go get指令

1
go get github.com/fogleman/nes

用法

1
nes [rom_file|rom_directory]

评价

NES是童年时很多人的挚爱,它的全称是Nintendo Entertainment System,也就是俗称的红白机。当年国内的小霸王就是对NES的盗版。
NES上有众多让人印象深刻的游戏,譬如马里奥系列、魂斗罗、松鼠大战、双截龙、泡泡龙等等。那是一个经典游戏的辉煌与井喷的年代。

实现细节

本nes工程,是对NES白皮书的一种go的实现。
代码中涉及了很多相对底层和硬件的内容,乍一看可能灰色难懂。这里可以循一条线来带你看懂nes的代码。

首先关注主目录下的文件结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
├── LICENSE.md
├── README.md
├── main.go
├── nes
│   ├── apu.go
│   ├── cartridge.go
│   ├── console.go
│   ├── controller.go
│   ├── cpu.go
│   ├── filter.go
│   ├── ines.go
│   ├── mapper.go
│   ├── mapper1.go
│   ├── mapper2.go
│   ├── mapper3.go
│   ├── mapper4.go
│   ├── mapper7.go
│   ├── memory.go
│   ├── palette.go
│   └── ppu.go
├── ui
│   ├── audio.go
│   ├── director.go
│   ├── font.go
│   ├── gameview.go
│   ├── menuview.go
│   ├── run.go
│   ├── texture.go
│   └── util.go
└── util
└── roms.go

直接放在root下的代码文件只有main.go,直接决定了nes这个可执行文件运行之后运行的代码。main.go在整个工程里是最易懂的代码了,简单来说就是判断一下参数,然后调用ui.Run(nes文件路径)。这条线索待会继续跟踪…

再来看主目录下面的文件夹们。nes文件夹主要负责nes文件的格式解析支持,ui负责界面与交互,util主要用来测试rom。(个人认为把util放在这里,并且起这个名字,从项目结构上不妥)

下面从nes/ui/run.go入手, 毕竟main函数调用了ui的Run函数,而Run函数可以看做是ui这个包的入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// run.go
package ui

import (
"log"
"runtime"

"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.1/glfw"
"github.com/gordonklaus/portaudio"
)

const (
width = 256
height = 240
scale = 3
title = "NES"
)

func init() {
// we need a parallel OS thread to avoid audio stuttering
runtime.GOMAXPROCS(2)

// we need to keep OpenGL calls on a single thread
runtime.LockOSThread()
}

func Run(paths []string) {
// initialize audio
portaudio.Initialize()
defer portaudio.Terminate()

audio := NewAudio()
if err := audio.Start(); err != nil {
log.Fatalln(err)
}
defer audio.Stop()

// initialize glfw
if err := glfw.Init(); err != nil {
log.Fatalln(err)
}
defer glfw.Terminate()

// create window
glfw.WindowHint(glfw.ContextVersionMajor, 2)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
window, err := glfw.CreateWindow(width*scale, height*scale, title, nil, nil)
if err != nil {
log.Fatalln(err)
}
window.MakeContextCurrent()

// initialize gl
if err := gl.Init(); err != nil {
log.Fatalln(err)
}
gl.Enable(gl.TEXTURE_2D)

// run director
director := NewDirector(window, audio)
director.Start(paths)
}

它所依赖的包就不多说了,与上文所述的依赖一致。

第一个函数是init()函数。看起来这个函数在整个工程中并没有被调用,其实不然。Go里面有两个保留函数,分别是init函数和main函数,其中init函数能够应用于所有的package,而main函数只能应用于main package。当某一个包被引入的时候,首先会引入自身的其他依赖,然后初始化常量,初始化全局变量,接下来就会自动调用这个package的init()函数。

可以在一个package下的多个文件中都定义init()函数,然而这样不是很便于管理,建议每个package最多写一个init()函数。
nes的作者就写了很多个init()函数。

init的内容是把runtime.GOMAXPROCS设置为2。runtime.GOMAXPROCS可以认为是Go语言最多使用的核心数目,在不设置的时候默认是1。
较大的GOMAXPROCS适合于CPU密集型,且并发度较高的情形。如果是IO密集型,CPU之间的切换反而会带来较大的性能损失。
nes中的GOMAXPROCS设置,是为了在执行任务的时候,音效不要卡顿。
接下来作者调用了runtime.LockOSThread(),这保证了调用OpenGL的时候,go只有一个线程去访问OpenGL的接口。

在执行Run的时候,NES首先初始化音频部件,然后初始化glfw,接下来使用glfw创建一个窗口,

glfw是一个C的OpenGL库,而go glfw则是一个典型的C与GO混合开发的一个库。

下面,NES初始化了gl,使用TEXTURE_2D模式。

最终,新建了一个Director,并执行这个Director,至此Run函数结束。

Director

Director导演的作用主要是对操作进行一个分发。如果当前有游戏的话,那么就加载游戏的GameView;如果是一个大列表,就把列表展示出来,让用户可以选择一个nes游戏执行。

按键

令人伤感的是,作者把按键适配写死在代码里,而且如果只有键盘的话,只能单人玩,简直是不能忍呀。具体的按键写死的代码在util.go中,有兴趣的小朋友可以给他改了。

1
2
3
4
5
6
7
8
9
10
11
12
func readKeys(window *glfw.Window, turbo bool) [8]bool {
var result [8]bool
result[nes.ButtonA] = readKey(window, glfw.KeyZ) || (turbo && readKey(window, glfw.KeyA))
result[nes.ButtonB] = readKey(window, glfw.KeyX) || (turbo && readKey(window, glfw.KeyS))
result[nes.ButtonSelect] = readKey(window, glfw.KeyRightShift)
result[nes.ButtonStart] = readKey(window, glfw.KeyEnter)
result[nes.ButtonUp] = readKey(window, glfw.KeyUp)
result[nes.ButtonDown] = readKey(window, glfw.KeyDown)
result[nes.ButtonLeft] = readKey(window, glfw.KeyLeft)
result[nes.ButtonRight] = readKey(window, glfw.KeyRight)
return result
}

上述代码规定Z和X对应红白机的A和B键。

虽然按键不尽如人意,但是fogleman的令人拍案称奇的作品确实还是太多了,估计没时间做nes的按键适配了吧。况且毕竟glfw并不特别方便进行窗口编程。

参考文献

  1. Benefits of runtime.LockOSThread in Golang - Stackoverflow
  2. LockOSThread