プログラミングや低レイヤで遊ぶ人

基本的には遊んだことを記事にしていく

ファミコンエミュレータ作り その4

FC emulator作り with golang

完成までのマラソン記事。がんばってやり切るぞい。

また、注意点としてこの記事はコードを更新しながら作成していっていくので記事で書いてある内容と実際のコードに差が生まれる可能性はあります。

コード本体         

反省の儀


これまでの記事があまりに雑すぎたのでここで懺悔します。

時間がかかってもちゃんとやった方がいいよね!

ファミコンの画面を作る その一

ebiten による画面の実装


ファミコンの画面を作っていくわけですが、まずファミコンの画面についてです。
f:id:jumdtw:20200521031001j:plain ファミコンは横256×縦240のピクセルで構成されています。(実際にコードを組む時は縦横256ピクセルとして処理をしていくのですが、その理由はまた別のタイミングで)

今回はEbitenという2Dのゲームエンジンを使用して実装していきます。
Ebiten website


sampleにあるNoiseをいじって画面を作っていきます。
とその前に、tourにある基本部分のコードの解説だけしていきます。
興味のない方は飛ばしてしまってオッケーです。
tourには詳しい解説が乗っています。

ebiten 基本コード


以下がebitenの基本コードの全体です

package main

import (
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

type Game struct{}

func (g *Game) Update(screen *ebiten.Image) error {
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    ebitenutil.DebugPrint(screen, "Hello, World!")
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 320, 240
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Hello, World!")
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}

上から一つづづ見ていきます。

type Game struct{}


golangではクラスなどはない変わりに構造体があります。
Gameの構造体を定義します。
ebiten の run.goには以下の定義があります。

// Game defines necessary functions for a game.
type Game interface {
    // Update updates a game by one tick. The given argument represents a screen image.
    //
    // Basically Update updates the game logic. Whether Update also draws the screen or not depends on the
    // existence of Draw implementation.
    //
    // The Draw function's definition is:
    //
    //     Draw(screen *Image)
    //
    // With Draw (the recommended way), Update updates only the game logic and Draw draws the screen.
    // In this case, the argumen
                        .
                        .
                        .


このGame interfaceを使用するためにGame structを定義しているらしいです。
次は描画部分です。

func (g *Game) Update(screen *ebiten.Image) error {
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    ebitenutil.DebugPrint(screen, "Hello, World!")
}


処理の順番的にはUpdate -> Drawのようです。
Updateでゲームロジックの更新。Drawで画面への描画をするらしいです。
頻度的には両方とも60fpsだそうです。

次で最後です。

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 320, 240
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Hello, World!")
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}


Layoutは今回のエミュレータでは使わないのであまり説明しませんが、公式いわくゲームの論理画面サイズを返す関数らしいです。

main関数ではwindowの生成とRunGame(&Game{})でゲームの開始をしています。
裏ではRunGame(&Game{})からrungameを呼び出していました。
rungameが再帰関数となっていてそこでゲームをループさせていたました。

これがebitenの基本コードです。
大した内容ではないのでgolangになれている人はすぐ理解できると思います。

Noise sample の編集


Noise sample の Update と Draw は以下の通りです。

func (g *Game) Update(screen *ebiten.Image) error {
    // Generate the noise with random RGB values.
    const l = screenWidth * screenHeight
    for i := 0; i < l; i++ {
        x := theRand.next()
        g.noiseImage.Pix[4*i] = uint8(x >> 24)
        g.noiseImage.Pix[4*i+1] = uint8(x >> 16)
        g.noiseImage.Pix[4*i+2] = uint8(x >> 8)
        g.noiseImage.Pix[4*i+3] = 0xff
    }
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    screen.ReplacePixels(g.noiseImage.Pix)
    ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS()))
}

                            .
                            .
                            .
func main() {
    ebiten.SetWindowSize(screenWidth*2, screenHeight*2)
    ebiten.SetWindowTitle("Noise (Ebiten Demo)")
    g := &Game{
        noiseImage: image.NewRGBA(image.Rect(0, 0, screenWidth, screenHeight)),
    }
    if err := ebiten.RunGame(g); err != nil {
        log.Fatal(err)
    }
}


golang の "image" モジュールをGame構造体に使用しています。

func NewRGBA(r Rectangle) *RGBA {
    return &RGBA{
        Pix:    make([]uint8, pixelBufferLength(4, r, "RGBA")),
        Stride: 4 * r.Dx(),
        Rect:   r,
    }
}


NewRGBAで新しいRGBA構造体を定義しています。UpdateではRGBAのPixを利用して画面への描画を行っているようです
。 pixelBufferLength(4, r, "RGBA")ではimage.Rect(0, 0, screenWidth, screenHeight)で生成したウィンドウの大きさを4倍した数を返しています。これは一ピクセルにRGBの色と輝度を設定するためのようです。

このサンプルを実行すると以下のようになります。
f:id:jumdtw:20200521031400j:plain 画像では止まって見えますが、実際にはノイズ画面のように動いています。CPU使用率もそんなに上がっていないですね。なんならchromeの方が高いです。

このサンプルを少し変えて行きます。

var rc, gc, bc uint8 = 1,0,0

const (
    screenWidth  = 256
    screenHeight = 240
)

                    .
                    .
                    .
                    .

func (g *Game) Update(screen *ebiten.Image) error {
    // Generate the noise with random RGB values.
    const l = screenWidth * screenHeight
    for i := 0; i < l; i++ {
        //x := theRand.next()
        g.noiseImage.Pix[4*i] = rc
        g.noiseImage.Pix[4*i+1] = gc
        g.noiseImage.Pix[4*i+2] = bc
        g.noiseImage.Pix[4*i+3] = 0xff

        if rc==0xff{
            rc = 0
            gc = 0xff
        }else if gc ==0xff{
            gc = 0
            bc = 0xff
        }else{
            bc =0 
            rc = 0xff
        }

    }

    return nil
}


ウィンドウのサイズをファミコン画面のピクセル数に合わせ、ノイズのようにランダムに動かすのではなく固定値を代入しています。
実行すると以下のようになります。
f:id:jumdtw:20200521031008j:plain これをCPUのメモリと連携させることでファミコンの画面を作っていきます。まともなFPSもでていますしね。(裏での話をするとまともなFPSを出してくれるエンジンがこれくらいしかなかったです。今時ドットを描画してくれるものは珍しいようです。)

今回ではここまでにしますが、次回ではPPU構造体を作成してCPUのメモリとPPUのメモリを連携させていきたいと思います。

参考

Ebiten website tour hello world golang image