一个用Go实现的NES模拟器 // NES emulator written in Go.
名称 |
nes |
地址 |
Github |
作者 |
fogleman |
Brief Intro |
NES emulator written in Go. |
LICENSE |
MIT |
starts |
2,816 |
介绍
这是一个使用Go实现的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
| 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() { runtime.GOMAXPROCS(2)
runtime.LockOSThread() }
func Run(paths []string) { portaudio.Initialize() defer portaudio.Terminate()
audio := NewAudio() if err := audio.Start(); err != nil { log.Fatalln(err) } defer audio.Stop()
if err := glfw.Init(); err != nil { log.Fatalln(err) } defer glfw.Terminate()
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()
if err := gl.Init(); err != nil { log.Fatalln(err) } gl.Enable(gl.TEXTURE_2D)
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并不特别方便进行窗口编程。
参考文献
- Benefits of runtime.LockOSThread in Golang - Stackoverflow
- LockOSThread