こんにちは、PS事業部のSです。
最近、コードを書くという環境から離れており、ふとコードを書きたいという猛烈なプログラミング欲(?)に駆られていました。そんなとき、たまたま動画でFCエミュレータ(NESエミュレータ)の解説をみて、「自分にも作れるのかな?」という思いが湧いてきたのです。
そして、その勢いのまま、ゼロからFCエミュレータを自作する企画をスタートさせました。この記事は、半年間にわたる試行錯誤の記録です。
※この記事に登場する情報は、筆者の偏見や誤った解釈を含む場合があります。エミュレータ開発に詳しい方々はどうか温かい目で、これから開発を始めようとしている方は必ず疑いの目で、ご覧ください。
開発するにあたっての独自ルール
気ままなサンデープログラマーとして、完成までの時間や品質に縛られることなく、開発を楽しむために、いくつかの独自ルールを決めました。
1.Web上の情報は参考にするが、他人のコードは見ない
「自作」と謳う以上、他人のエミュレータのコードを写経するだけでは意味がありません。しかし、FCについて何の知識もない私がエミュレータを作るのは到底不可能。
そこで、「Web上の技術情報は参考にするが、コードそのものは見ない」というルールを設けました。
2.C#で実装し、便利なライブラリは使わない
特にこだわりがあったわけではありませんが、開発環境が簡単に整うC#を選択しました。C#製のエミュレータはGitHubでも数が少なそうだったことも理由の一つです。
また、今回は「自作」に徹底するため、画面表示やサウンド、エミュレータのコア部分を担う便利なライブラリは一切使用しないよう心掛けました。C#というオブジェクト指向言語を使い、CPUやPPUといったハードウェアの各パーツを個々のオブジェクトとして扱うことで、より本質的な実装を目指しました。
完成までの道のり:半年間の苦闘
「所詮は8bitなレトロゲームなのだから、2か月もあれば完成するっしょ!」という何の根拠もない自信を持って開発に臨んだのですが、結局完成といえるところまでに半年を要しました。 実装は大きく以下の4つのコンポーネントに分けて進めました。
- カセット: ROMファイルを解析し、本体と情報をやり取りする部分
- CPU: ROMのプログラムを読み取り、メモリの読み書き、PPUやAPUとの情報伝達を担う部分
- PPU: ゲーム画面の描画を担当する部分
- APU: ゲームのサウンドを担当する部分
手始めにカセット
カセットは、プログラムやキャラクターのデータを取り扱う部分です。マッパーと呼ばれるカセット内の回路の仕組みを理解するのが最初の関門でした。マッパーは限られたメモリ空間を拡張する役割を持っており、最初は最もシンプルな「マッパー0」を実装し、動作するようになった後に種類を増やしていきました。 「マッパー0」はCPUやPPUからメモリのアクセス要求に対して値を返却するというシンプルな動作であるため、躊躇なく実装ができました。
調子に乗ってCPU
ROMのプログラムを読み込む・メモリの読み書きを行うCPUは、Web上の情報をそのままコードに置き換える作業が中心でした。また、ゲームが動作するにつれて、割り込みやDMA(ダイレクトメモリアクセス)の処理に手直しが入るなど、調整が大変でした。
難関のPPU
画像の各ドットを描画すると同時に、裏で決められた処理を実行していくPPU。処理を省略する方法もあるようですが、正確なエミュレートを目指し、各ドット単位で処理を行う方針としました。
パフォーマンス面が気がかりでしたが、意外にも大きな影響はありませんでした。ちなみに、画面表示にはWindowsの標準ライブラリのみを使用しており、DirectXは使っていません。そのため、画面スクロール時にわずかなティアリングが発生してします。
最も苦労したAPU
開発の中でも最も難易度が高かったのは、リアルタイムに音を生成するAPUでした。矩形波から取り掛かったのですが矩形波が音として出力された瞬間の感動は忘れられません。
当初はSystem.Media.SoundPlayerで音を鳴らそうとしましたが、リアルタイムな音の生成には相性が悪く、音がブツブツと途切れてしまいます。結局、初志を貫徹できずXAudio2のラッパーライブラリに頼ってしまいました。 音を出す仕組みを理解できたことで、「チップチューン音源ならお任せ!」と言えるような根拠のない謎の自信に満ち溢れています。
そして完成! 史上初の独自機能搭載!(※自分調べ。すでに実装しているエミュレータがあったらごめんなさい)
なんだかんだあって完成です。(経緯をかなり端折っていますがかなり苦戦しました)
ここでは、世の中のエミュレータではあまり見かけない、独自に実装した機能をいくつかご紹介します。
機能1:カセット半挿し(半抜き)、強制取り出し+カセット入れ替え
カセット半挿し(半抜き)はカセットの端子がFC本体に正しく接触していない状態を再現します。これにより、背景の当たり判定が狂ったり、キャラが化けて隠れキャラを見つけ出したりすることができます。
強制取り出し+カセット入れ替えでは、FC本体の電源が入ったままカセットを入れ替えます。これにより、RAMの内容は入れ替え前のゲームのまま、ROMは入れ替え後のゲーム、という状態となり誤動作を起こします。
カセット半挿しの状態
エミュレータではPPU側の非接触となるピンの選択も可能。
カセット強制取り出しの状態
一見ランダムに見える模様だが、実は規則的なパターンとなっておりどのカセットもこの模様となる(配色は直前のゲームの色となる)
カセット入れ替えした状態
キャラクタ(8 x 8ドットの絵)やプログラムはカセットから読み取られるが、キャラクタの配置場所や色などのRAMは入れ替え前のゲームのままとなる。この状態では入れ替え後のゲームはほぼプレイできません。
リセットして入れ替え後のゲームをプレイした状態
リセット時にソフトがRAMの初期化を行わない場合は、入れ替え前のゲームのRAMの内容をひきついでゲームが始まります。そのため誤動作します。
この画像だけみて、入れ替え前・後のゲームが何なのかわかる人もいるはず。
機能2:背景入れ替え
ゲームの背景色に特定のパレットが使われている場合、代わりにエミュレータが用意した画像を背景に描画します。例えば、宇宙空間のゲームで背景をグラデーションにすると、とても映えます。
カセット半挿しの状態の背景をグラデーションに切り替えてみる
機能3:母の掃除機攻撃モード
FC本体に少し衝撃が加わるとゲームが暴走し、音が「ぷぉ~......」と鳴り続ける事故が多発しました。
これは、ゲーム中に母親が掃除機をかけ始め、本体に接触してしまうことで頻繁に起こった、当時のファミっ子たちのトラウマを呼び起こすモードです。ちなみにこの機能を使用することで何のメリットもありません。
「カセット強制取り出し」+「カセット半挿し」機能で実現しています。
※当ブログでは動画の投稿ができないためトラウマ動画は割愛しますが「ぷぉ~......」も再現できています。
最後に:企画を終えて
開発終盤には、「またゲームが動作しない」という悪夢にうなされることもありましたが、無事に完成して本当に良かったです。記事内ではまともなゲームプレイ画像もなく紹介もできずに心残りです。
この企画を通じて、コンピューターの仕組みを深く理解でき、技術的な課題を一つずつ解決していく楽しさを改めて感じました。そして今は、プログラミング欲もすっかり満たされ、「当分コード書くのはいいや......」という気持ちでいっぱいです(苦笑)。
今回開発したコードはプライベートリポジトリに置いているため公開予定はありませんが、この記事が誰かのエミュレータ開発のきっかけになれば嬉しいです。
おまけ
今回、エミュレータ開発で協力していただいたカセットたち。画像には映っていないカセットがまだ大量にあったりします。
Excelでカセット保有情報を管理しダブりがないようにしているものの、それでも同じゲームがダブっていたりします。


