JavaScript
game
emulator

ファミコンエミュレータに挑戦してみた


  • 背景がスクロールできるように改良

  • スプライトが透過され背景が表示されるように改良



はじめに


  • ファミコンエミュレーターの創り方 - Hello, World!編 -」の続きを作ろうとしたら思いの外苦戦した


  • なのでHello, World!以降の第1歩となる資料の作成を試みた


  • まだリアルなファミコンのソフトを実行するところまでできていない(間に合わなかった)


  • 手厚く開発のサポートしてくれたbokuweb氏にこの場を借りて感謝したい




何故ファミコンエミュレーター

CPUなど処理装置の学習がしたかったから


  • CPUの創りかたという本を読んだ


  • 試しにCPUのエミュレーターをJavaScriptで作成してみた


  • 勉強にはなったがやれることが少なくて物足りない(タイマーくらいしか使えない)


  • サンプルの多い練習台はないか


  • ファミコンエミュレーター(NES)があるじゃないか




内容

落とし穴になりそうなあたりを説明



nesファイルの作成

思ったより簡単



colorの定義はどこにある?


  • 背景のパレットやスプライトのパレットで指定されるやつ

  • ファミコンの基盤によって微妙に違うらしい

  • とりあえず筆者はNES HACKERに乗っているcolorの情報を使っている



paletteのミラー


  • 色情報を指定するpaletteは背景用(16byte)とスプライト用(16byte)の2種類が存在する

  • スプライト用のアドレスの一部が背景のアドレスのミラーになっている(同一のデータになっている)

  • 背景のアドレスの一部もまた別の背景のアドレスのミラーになっている

  • 筆者はNES Palette Tricksを参照した



paletteのミラー(例)

スプライトの画像情報を作成する処理

  createSpliteDrawInfo(spriteIndex, upperColorBits, attribute) {

const sprite = createSpriteInputs(this.characteSpriteData[spriteIndex])
const retSprites = createBaseArrays()
const { startTop, startLeft, useBackground } = this.culculateAttribute(attribute)

for (let row = 0; row < 8; row++) {
for (let column = 0; column < 8; column++) {
const actualRow = startTop ? row : 7 - row
const actualColumn = startLeft ? 7 - column : column

const paletteIndex = (upperColorBits << 2) | sprite[row][column]
// 特定のアドレスを参照する際、スプライトのパレットではなく背景のパレットを参照する
const shouldUseBackground = (useBackground || paletteIndex === 0x00 || paletteIndex === 0x04 || paletteIndex === 0x08 || paletteIndex === 0x0c);

retSprites[actualRow][actualColumn] = shouldUseBackground
? this.backgroundPalette[paletteIndex]
: this.spritePalette[paletteIndex]
}
}
return retSprites
}



paletteのミラー(例)

背景の画像情報を作成する処理

  createBackgroundSprite(characterIndex, nameIndex) {

const { row, column } = convertIndexToRowColumn(nameIndex)
const sprite = createSpriteInputs(this.characteSpriteData[characterIndex])

return sprite.map((elem) => {
return elem
.map((num) => {
const base = (this.colorTileBuffer[row][column] << 2) | num

// アドレスが0x04 or 0x08 or 0x0cの時、 0x00の値を返す
const paletteIndex = base === 0x04 || base === 0x08 || base === 0x0c ? 0x00 : base
return this.backgroundPalette[paletteIndex]
})
.reverse()
})
}



適当に作ると思ったより遅い


  • putImageDataなどを使いcanvasで描画する回数を減らす

  • 可読性は落ちるが可能な限り論理演算で対処する(特にPPUのレジスタが顕著)

  • debug用のflagを用意して、trueの場合、どのbitがtrueになっているとか表示するのが妥当そう

60fpsは適当に作ると出ない



レジスタだけ管理すれば動くという訳ではない


  • 当たり前と言えば当たり前の話

  • nmiの割り込みなどregisterだけで全ての状態が管理されている訳ではない(1fpsに1回)

  • 筆者はこれに気づかずcpuの1cycleごとに1回nmi割り込みを実行するように1回実装した

実機だとどんな処理をしているかイメージしながら実装すると捗ると思う(多分)



実際のとこどうなの?


  • 思いの外、雑でも動く

  • どんな処理を実際にはやっているのか想像しながら実装できるので学習は進む

  • 動くサンプル(flownes)があるので亀の歩みでも進捗はできる

  • 日本語の資料だけではやっぱり不足する(NesDevを読もう)

  • 想像以上に時間がかかる(完全に注力できたわけではないとは言え、現状まで2ヶ月ほどかかっている)

CPUなどの処理装置の勉強の第2歩目くらいに良いと思う



レポジトリ

working2.gif



参考リンク