10

每周一个Github项目【第六期】nes

 3 years ago
source link: http://zablog.me/2017/09/12/github_nes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

每周一个Github项目【第六期】nes

2017年9月12日

一个用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

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指令

go get github.com/fogleman/nes
nes [rom_file|rom_directory]

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

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

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

├── 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这个包的入口。

// 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中,有兴趣的小朋友可以给他改了。

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并不特别方便进行窗口编程。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK