2014年だけどファミコンのゲームを作りませんか? #1

ゲームを作ろうと思い立ったはいいものの、何から始めればいいんだろう…。cocos2d-xでスマホゲームを作る?Unityで3Dバリバリのゲームを作る?いまいちパッとしませんね。そうです、なんだか複雑すぎるのです。
そんなあなたにピッタリなのがファミコンのゲームです。もう30年前のハードですから、きっと単純ですって。たぶん。だって8ビットですよ。

環境構築

今回はOS Xで開発をすることにします。まずはアセンブラコンパイラを準備します。

アセンブラはnesasmを使います。オブログ — なあ藤村くんファミコンプログラミングをやろうじゃないか[1]を参考にmakeしてください。
コンパイラはcc65を使います。なぜコンパイラも準備するかというと、まだできる限りアセンブラを書きたくないからです。簡単なほうがいいですよね :)

% brew install cc65

とりあえず動くものを書いてみる

ビルド手順はライブラリ使用例 - cc65 @ wiki - アットウィキを参考にしてください。

まずはBGにチェック模様を描いてみます。

#define REGIST_PPU_CTRL1 (char *)0x2000
#define REGIST_PPU_CTRL2 (char *)0x2001
#define REGIST_PPU_STS   (char *)0x2002
#define REGIST_SPR_ADR   (char *)0x2003
#define REGIST_SPR_GRA   (char *)0x2004
#define REGIST_SCROLL    (char *)0x2005
#define REGIST_ADR       (char *)0x2006
#define REGIST_GRA       (char *)0x2007
#define REGIST_DMA       (char *)0x4014

void NesMain(void) {
    int i, j;
    byte v_stat, h_stat;

    const char palettebg[] = {
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30
    };

    while (!(*REGIST_PPU_STS & 0x80));
    *REGIST_PPU_CTRL1 = 0x00;
    *REGIST_PPU_CTRL2 = 0x00;

    *REGIST_ADR = 0x3f;
    *REGIST_ADR = 0x00;
    for (i = 0; i < 16; i++) {
        *REGIST_GRA = *(palettebg + i);
    }

    *REGIST_ADR = 0x20;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_PPU_CTRL1 = 0x0;
    *REGIST_PPU_CTRL2 = 0x1e;

    while (1);
}

void NMIProc(void) {
}

ファミコンでBGを描画するにはまずネームテーブルに書き込む必要があります。0x2000-0x2007がPPUのI/Oレジスタです。PPUメモリデータ(0x2007)にパターンテーブル番号を書き込むことでネームテーブルに書き込むことができます。*1
f:id:akiym:20140502220154p:plain
ファミコンの画面は1枚あたり8x8のタイルパターンが32x30個あります。*2 4x4の四角を配置しましたが、縦横の比率が異なるため、上下に余りがあります。この画面を複製してスクロールさせる場合、4x4の四角ができない部分ができてしまいます。もう少し工夫が必要みたいです。
f:id:akiym:20140502212328p:plain

次は画面をスクロールさせてみます。

void NesMain(void) {
    int i, j;
    byte v_stat, h_stat;
    byte bgx = 0, bgy = 0;

    const char palettebg[] = {
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30,
        0x0f, 0x11, 0x21, 0x30
    };

    while (!(*REGIST_PPU_STS & 0x80));
    *REGIST_PPU_CTRL1 = 0x00;
    *REGIST_PPU_CTRL2 = 0x00;

    *REGIST_ADR = 0x3f;
    *REGIST_ADR = 0x00;

    for (i = 0; i < 16; i++) {
        *REGIST_GRA = *(palettebg + i);
    }

    *REGIST_ADR = 0x20;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_ADR = 0x24;
    *REGIST_ADR = 0x0;

    v_stat = h_stat = 1;
    for (i = 0; i < 30; i++) {
        for (j = 0; j < 32; j += 4) {
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            *REGIST_GRA = h_stat ? 0x01 : 0x03;
            h_stat = h_stat ? 0 : 1;
        }
        if (i % 4 == 0) {
            h_stat = h_stat ? 0 : 1;
        }
    }

    *REGIST_PPU_CTRL1 = 0x0;
    *REGIST_PPU_CTRL2 = 0x1e;

    while (1) {
        while (!(*REGIST_PPU_STS & 0x80));
        *REGIST_SCROLL = bgx;
        *REGIST_SCROLL = bgy;
        *REGIST_DMA = 0x07;

        bgx++;
        if (bgx == 255) bgx = 0;
        bgy++;
        if (bgy == 240) bgy = 0;
    }
}

最後にスプライトを表示させて完成です。ちゃんと円を描くようにしてみました。
f:id:akiym:20140502213645p:plain

書き殴ったものをここにおいておきます。
GitHub - akiym/nes-circle: exapmle #1

ところでまだコントローラーを握っていませんね。まだまだ長い道のりになりそうです…。