2023年から始めるNINTENDO64ゲーム開発

2023年から始めるNINTENDO64ゲーム開発
~64-bitセンセーション~

この記事はKMCアドベントカレンダー15日目の記事として執筆いたしました。

NINTENDO64について

みなさん御存知の通り、N64は20世紀で最も素晴らしいゲームハードウェアの一つです。発売されたソフトの数自体は少ないとはいえ、時のオカリナF-ZERO Xマリオ64といった世に影響をもたらした名作が満ち溢れています。

OOT

令和におけるN64について

発売から約28年の時を経たとはいえ色褪せることなくN64タイトルは現在でも多くの人に遊ばれていますが、技術系のN64コミュニティやイベントも数多く存在します。

以下にいくつかご紹介します。

N64ゲーム開発チュートリアル (2023年)

N64の主要なソフトウェア開発キットには、当時任天堂が実際に開発に使用した libultra と2023年現在においてOSSとして開発が続けられているlibdragon が存在します。

ただlibultraは機能豊富ですが、Windows Vista以前の32-bit環境でのみ動作し、さらには使用に関して法的にもグレーであるため、ここではlibdragonを使用する方法をご紹介します。

以下では、次のリンク先に従って説明します。

https://github.com/DragonMinded/libdragon/wiki/Installing-libdragon

いくつか方法が記載されていますが、Dockerとnpm経由で利用するのが簡単だと思います。

Dockerとnpmがインストールされていない場合は、以下の記事などを参考にしてください。

Dockerとnpmをインストールし、起動した状態で、以下のように実行します。

mkdir hello64
cd hello64

npm init
npm install -g libdragon
npx libdragon init

これでlibdragonがインストールできました。しかし、2023/12/08執筆現時点においてlibdragonのデフォルトブランチでは基本的に2D描画のみをサポートしており、高度な機能を利用できません。

そこで、予めunstableブランチのlibdragonを使用することのがおすすめです。unstableブランチでは画像などのアセットを簡単にROMに組み込んだり、いにしえのOpenGL 1.1 APIを利用した快適で高度なコーディングが可能です。

以下のコマンドでunstableに切り替えます。

git submodule add -b unstable https://github.com/DragonMinded/libdragon.git
git submodule update --init --recursive
cd libdragon
npx libdragon install

これで、libdragonをunstableブランチとして利用することができるようになりました。

では、実際にN64 ROMを作成していきましょう。

src/main.c ファイルを開けて、以下のコードに書き換えて保存してください。

#include <libdragon.h>

#define TEXT_POS(x, y) ((x)*8), ((y)*8)
#define LX_8 8
#define CY_14 14

static display_context_t s_display = NULL;
static char s_sb_64[64];

int main(void)
{
    display_init(RESOLUTION_320x240, DEPTH_32_BPP, 2, GAMMA_NONE, ANTIALIAS_RESAMPLE);
    controller_init();

    int count = 0;
    while (1)
    {
        while (!(s_display = display_lock()))
            ;

        graphics_fill_screen(s_display, 0x1F1F1FFF);
        graphics_set_color(0xFFFF00FF, 0);
        graphics_draw_text(s_display, TEXT_POS(LX_8, CY_14 - 2), "Hello N64!\n");

        snprintf(s_sb_64, sizeof(s_sb_64), "Count: %2d\n", count);
        graphics_draw_text(s_display, TEXT_POS(LX_8, CY_14 + 2), s_sb_64);
        display_show(s_display);

        count++;
    }
}

以下のコマンドでビルドを行います。

(プロジェクトのルートディレクトリにMakefileが存在するので、ルートディレクトリで行ってください)

npx libdragon make

おめでとうございます。これで hello.z64 というN64で動作可能なROMファイルが生成されました。

では実際に動作確認をしてみましょう。

私が開発中のN64エミュレーターをぜひご使用ください、と言いたいところですがまだまだ実用段階に到達していないので、aresやdgb-n64、simple64といった新世代のエミュレーターで動作させるといいと思います (paraLLEl-RDPを利用したこれらのエミュレーターは「Bandicam」というソフトウェアとなぜか競合するのでご注意ください)

実行画面

実行して以下のように「Hello 64!」とカウントアップと表示されれば成功です (エミュレータの種類によってはうまくいかない可能性があるので改めてご注意ください)

画像を表示したり、ポリゴン描画や音声再生などについては libdragon/examples が参考になると思います。

次に、これらの前にあらかじめ開発において知っておきたい前提知識を説明いたします。

開発に必要なN64のハードウェア基本知識

私自身、20世紀にはこの世に生を受けておらず、未熟者として学習の途上にございます。以下に述べる内容には、不備や誤りが含まれる可能性がございますが、何卒ご了承いただけますと幸いです。

グラフィックについて

ファミコンゲームボーイなどを少しかじったことがある方はPPUによるSpriteやBGの画面構成に馴染みがあるかもしれませんが、N64では完全に廃止されRDP (Reality Display Processor) によるポリゴン描画がメインになり、現代コンピュータのGPU描画に近い形態となりました。RDPへの描画コマンドの送信はVR4300i(MIPSアーキテクチャ64-bit CPU) からも可能ですが、基本的にはRSP (Reality Signal Processor) にマイクロコードを乗せてコマンド送信します。

従来の古典的ハードウェアと違い、描画領域VRAMは予め確保されておらず、主記憶RDRAMのうち描画領域をソフトウェア側でMMIOビデオインターフェースを通じて指定します。画面の幅や高さも独自で指定可能ですが、N64の多くの商業用ゲームは320x240のSD画質で表現されています (ただし、WiiのVCの一部タイトルでは640x480の2倍の解像度に修正されていたりします)

現代のビデオゲームと同様に、libultraやlibdragonではダブルバッファリングしています。

サウンドについて

8, 16-bit音、ピコピコ音が特徴的なAPUを搭載した古典的ハードウェアから脱却し、N64MIDI再生が可能となりました。N64はオーディオ専用のプロセッサを搭載していないので、基本的に上記でも登場したRSPを利用して音を流し込みます。実はRSPはたったの4KiBのマイクロコードしか搭載できないので、1フレームの間だけでかなりRSPのマイクロコードが書き換わります。

余談ですが、F-ZERO XスターフォックスはリアルタイムでCPU処理の負荷軽減のため、MIDIファイルを圧縮せずにそのまま再生しているので、ROMファイルの大半が音源ファイルであると言われています。

ゲームボーイなどのサンプリングレートは44.1kHz固定ですが、N64ではサンプリング周波数を変更できるので多くの商用ゲームではこの数を割っています。

VR4300iについて

NINTENDO64の名が表す通り、VR4300iは時代を先取りした64-bit CPU でございます。同世代PlayStation1、セガサターンは32-bit幅であるのはもちろんのこと、後の任天堂ハードウェアGCWiiWii Uに至るまで基本的に32-bitであったことを考えるとこの特異性が伺えます。N64は32個も64-bit汎用レジスタを持っています。

かつてのZ80プログラミングにおいては条件分岐やforループがかなりクリティカルポイントになっていたところですが、本ハードウェアは5段パイプライン処理で分岐命令に対して遅延スロットを設けており、パイプラインハザードが大きく軽減されています。

メモリアクセスは64-bit仮想アドレスから32-bit物理アドレスMMUを通して変換されます。従来のハードウェアと違い、手動でメモリバンクを切り替えなくてよくなったのでプログラマのメモリ管理の負担が少なくなったかもしれません。

N64と携帯ゲーム機DSはスペックが似ているイメージがあるかもしれませんが、DSとは違いN64浮動小数点ユニットFPUを備えています。これにより、float値やdouble値を扱うことができるようになるため、正確な数値計算が可能になります。

RSPについて

さて上記で登場したRSPですが、RSPは基本的にはVR4300iと同じMIPSアーキテクチャのCPUが実態となります。詳細は解説しませんが、RSPでは例外処理といった一部機能が制限されている上、アドレス幅は64-bitではなく32-bitであるために64-bit演算命令が使えなかったりします。しかし、RSPの最大の特徴はベクトル演算の命令が可能であることです。これにより、高速な行列変換が可能になり3Dゲームにおける座標変換が効率よく行えます。

RSPの詳細な命令を知りたい方は以下をご確認ください。

現状、RSPのマイクロコードを高級言語で抽象化したものはまだメジャーなものが存在しないので、もし自分でRSPコードを書きたいときはMIPSアセンブリで直接記述する必要がありそうです(libdragon/examples/rspqdemo が参考なると思います)

RSPは、VR4300iとは完全に別のメモリ(SP IMEM / SP DMEM)を使用します。IMEMはマイクロコード書き込み用の命令用のメモリ、DMEMはRSPが使用するデータ用のメモリでどちらも4KiBです。RSPはVR4300iが使用するRDRAMにアクセスできません。つまり、DMEMのみアクセスできます。

RDPについて

上記の通り、RDPには頂点情報やカラー情報、テクスチャ情報などの情報を含んだ描画コマンドを送信してメモリ (4MiBのRDRAM) の描画領域を書き換えます。N64は世界初のアンチエイリアシングが可能なゲームハードウェアですのでアンチエイリアス描画ももちろん可能です。ブレンド、ミップマップ、Zバッファリングといった現代グラフィックAPIで馴染み深い機能も搭載されいます。描画コマンドは以下の資料で確認できますので、少し見てみると雰囲気がわかると思います (が、かなり間違っている部分が多いのでご注意ください)

https://ultra64.ca/files/documentation/silicon-graphics/SGI_RDP_Command_Summary.pdf

上述の通り、コマンド送信はVR4300iからでもRSPからでも可能です。特筆すべきことに、三角形描画のコマンドだけではなく、長方形描画のためのコマンドも別で用意されています。確かlibdragonのRSPQ内部では四角形描画のときに、回転量が0かそうでないかで三角形に分割するかしないか選択してた気がします。

RSPとRDPはまとめてRCP (Reality Co-Processor) と呼ばれることもあります。

さて、ディスプレイ描画に関してですが、実はRDPを介さなくてもVR4300iから直接描画領域を書き換えるソフトウェア描画が可能です。実は、先程のサンプルコードでも使用したgraphics_draw_text 関数はソフトウェア描画になっています。ただし、自明なことですがソフトウェア描画は圧倒的に遅いので基本的にRDPから描画する必要があります。

VCの時のオカリナにおいて、実機と水の神殿のボスのグラフィックが微妙に違っていたりするのは透明処理でソフトウェア描画を使ったりしていて正しくエミュレートできていないからなのではないでしょうか (要検証)

RDPのテクスチャメモリはたったの4KiBしかありません。そのうち、2KiBはTLU(Texture Look up)テーブルに使用されるので、一度にわずかなサイズの画像テクスチャしか使用できません。これは、多くのN64プログラマの悩みの種となったようです。

図 (手書き)

以下は、VR4300iやRCPなどをまとめた簡単な図です。

おしまい

N64ゲーム開発に興味を持っていただけたら幸いです。

素敵なN64ゲームやライブラリを作ってくださいね。