読者です 読者をやめる 読者になる 読者になる

NES 基礎知識 - コントローラ

今回は NES/FC の標準コントローラについて扱います。特殊コントローラ(アルカノイドパドルなど)は扱いません。

標準コントローラには I/O レジスタ $4016, $4017 経由でアクセスします。NES と FC で若干細部が異なりますが、基本は同じです。詳細は NesDevWiki の資料を参照。

まず、入力を読み取る前に準備が必要です。$4016 に 1, 0 の順で書き込むことで 1P/2P 両方の入力読み取り準備が完了します。

その後、1P については $4016, 2P については $4017 を読み取ることで 1 回ごとにボタン 1 つの入力状態が得られます(A, B, S, T, U, D, L, R の順)。よって、全ボタンを読み取るには 8 回の読み取りが必要です。大半のゲームでは 1 バイト変数内の各ビットで各ボタンの入力状態を管理しています。

$4016, $4017 読み取り時のデータフォーマットを示します(FC の場合):

$4016 (read)
------------
bit 76543210
    .....MFD

M: マイク入力があれば 1, なければ 0
F: 拡張ポート (3P) のボタンが押されていれば 1, さもなくば 0
D: ボタンが押されていれば 1, さもなくば 0

$4017 (read)
------------
bit 76543210
    ......FD

F: 拡張ポート (4P) のボタンが押されていれば 1, さもなくば 0
D: ボタンが押されていれば 1, さもなくば 0

要するに、マイクや拡張ポートを無視すれば、見る必要があるのは最下位ビットのみです。1P 入力を読み取る最小限のコードを示します:

        ; 1P 入力を読み取って $00 に保存する(形式: ABSTUDLR)

        ; 読み取り準備。$4016 に 1, 0 の順で書き込む
        ldx #1
        stx $4016
        dex
        stx $4016

        ; 1 ボタンずつ読み取り、結果のビットを $00 へシフトインしていく
        ; 擬似コードで書くと:
        ;
        ; for(X = 8; X != 0; --X)
        ;   A = read(0x4016)
        ;   C = A & 1
        ;   mem[0x00] <<= 1
        ;   mem[0x00]  |= C
        ldx #8
@loop:
        lda $4016
        lsr       ; ここでキャリーフラグ C に入力状態が入る
        rol $00   ; ローテートにより C がシフトインする
        dex
        bne @loop

ここでは入力記録用変数 $00 を ABSTUDLR 形式としましたが、もちろん RLDUTSBA 形式でもかまいません(rol を ror に変えるとそうなります)。商用ゲームではどちらの形式も採用されています。

上では簡略化したコードを示しましたが、実際のゲームでは各ボタンを読み取る際に bit0 だけでなく bit1 も調べて両者の OR を入力状態としていることがほとんどです。つまり拡張ポート入力に対応しているということです。

また、特に RPG などではボタンを「離してから押す」ことではじめて入力を認識する方が都合がいいことが多いので、前フレームの入力との XOR をとって「新規入力」を計算するコードが含まれていることが多いです。

大半のゲームでは(まれにマイクを含む)入力状態記録専用の変数群を設けているので、アセンブリ全体からこれらの変数を検索することで裏技探しができたりします。例えば 1 人用ゲームなのに 2P 入力状態を参照しているコードがあったらそれは裏技関連かもしれない、など。

なお、$4017 は読み取り時は 2P 入力状態を返しますが、書き込み時は全く役割が異なる(APU 制御用)ので注意。