NESプログラミング1 (1)

1. NESでプログラミング

早速,実際にプログラムを組んでみましょう.(といっても最初は写経ですが・・・)
nes_beginner1_v1.zipをダウンロードして使ってもよいです. このフォルダをVSCodeで開いたら,game.asmを開いてみてください.以下のようなコードがあると思います.

.setcpu "6502"
.feature c_comments                     ; /* comments */ の複数行のコメントを許可(通常はセミコロン)
.feature underline_in_numbers           ; 数値の途中に %1010_0010 のようにアンダーバーを挟めるようにする(2進数の表記に便利


; ここで指定しているものはiNESヘッダーという
; https://www.nesdev.org/wiki/INESを参照
.segment "HEADER"
        .byte "NES", $1a
        .byte $02                       ; プログラム領域のサイズ(16KB * 2)
        .byte $01                       ; キャラクタ領域のサイズ(8KB * 1)
        .byte $01                       ; 垂直ミラー(2画面が横方向につながる)
        .byte $00
        .byte $00, $00, $00, $00
        .byte $00, $00, $00, $00


.code                                   ; 以下プログラム領域

.segment "STARTUP"                      ; 起動時のプログラム開始位置

RESET:                                  ; RESETという名前のラベル(ここにジャンプさせることができる)
        sei                             ; IRQ禁止(割り込み処理の禁止)
        cld                             ; BCD禁止(BCD機能はファミコンで除外されているので,やる必要性はわからない)
        ldx #$ff                        ; X = #0xff(アドレスではなく,即値)
        txs                             ; スタックレジスタ(S)に#$ffを指定

        lda #0                          ; #がついてないのは10進数を示す
        ; 画面をOFFに
        sta $2000
        sta $2001

        ; RAMの初期化(ある初期値,ここでは0を入れること)を行う
        lda #0
CLEAR_MEMORY:
        sta $00, x                      ; $00XX番台は$XXと省略できる
        sta $0100, x
        sta $0200, x
        sta $0300, x
        sta $0400, x
        sta $0500, x
        sta $0600, x
        inx
        bne CLEAR_MEMORY                ; X == 0(Xが$ffからオーバーフローして0に戻る)までループ

        ; $0700番台にはスプライト情報を入れる
        ; 都合がいいので#$ffで初期化
        lda #$ff
CLEAR_SPRITE_MEMORY:
        sta $0700, x
        inx
        bne CLEAR_SPRITE_MEMORY

        ; VRAM(PPUのRAM)を初期化する
        ; アドレス$2000を初期化開始位置に指定
        lda #$20
        sta $2006
        lda #$00
        sta $2006

        ; $100回のループ(Xレジスタがカウンタ)を8回ループさせる(Yレジスタがカウンタ)
        ; なので,$2000~$27ff($800の領域)を初期化できる
        ; $2000~$23ffが画面1,$2400~$27ffが画面2(画面1の横隣り)の領域
        ldx #0
        ldy #8
CLEAR_VRAM:
        sta $2007
        inx
        bne CLEAR_VRAM
        dey
        bne CLEAR_VRAM

        lda #$00
        sta $2005                       ; カメラX座標
        sta $2005                       ; カメラY座標

        ; スクリーンオン
        ; 各ビットの意味はhttps://hp.vector.co.jp/authors/VA042397/nes/ppu.html参照
        lda #%0000_1000                 ; 3ビット目が1になっている=スプライト用CHRテーブルの設定(NES研究所参照)
        sta $2000
        lda #%0001_1110                 ; 背景・スプライトを表示,画面の左端を表示
        sta $2001


MAINLOOP:
        jmp MAINLOOP

        ; プログラム終了


.rodata                                 ; データ領域


; キャラクターデータを読み込む
.segment "CHARS"
        .incbin "bg_sprite.chr"         ; バイナリ(bin)データとしてインクルード(inc)する


; プログラムデータの最後に書き込まれる割り込み時のジャンプアドレスのテーブル
; .addr ラベル名で,ラベルのあるプログラムのアドレスを指定できる
.segment "VECINFO"
        .addr $0000                     ; 未指定(NMI割り込み時のジャンプ先)
        .addr RESET                     ; リセットボタンが押されたときのジャンプ先(起動時と同じ動作をする)
        .addr $0000                     ; 未指定(IRQ割り込み時のアドレス)


このプログラムは,プログラムを実行する前に必要なテンプレートや初期化のコードが含まれています.まだ何もしていないに等しいということです. ざっくりいうと,画面OFF→メモリをある一定の値で上書き(初期化),→画面ONという処理しか行っていません.

2. 背景に文字を表示

では,次に何か文字を表示してみましょう. 背景に何か表示するためには,8*8のキャラクタ(ここでのキャラクタとは,マリオやクリボーなどのキャラではなく,8*8のドット絵という意味のキャラクタで,背景/スプライトどちらにも使います)と,キャラクタに塗るパレットデータを組み合わせます. 事前にパレットデータといって,色の組み合わせを保存しておき,背景やスプライトを色づけるときにはパレットの中から適切な色の組み合わせを使って塗り合わせます.後で詳しくやります.
まずは,文字キャラクタを画面上に出現させてみましょう. まず,表示させる場所(座標)に関してですが,8*8ドットごとの格子状に設置が可能です.ファミコンの解像度は256*240なので,32*30の行使で構成されることになります. このマス目で,例えば(15, 14)の座標にAという文字を表示させるときには,$2000+14*$20+15という式でVRAMのアドレスを指定できます.(=$21CFにAを入れると,画面0の(15, 14)に文字が出る)
一般化すると,$2000 + x座標 + y座標*$20 + スクリーン番号(画面0,画面1)*$400 という式を使うと良いです.
これをプログラムにしてみます.(一部省略)

;! 先頭省略

        ; $100回のループ(Xレジスタがカウンタ)を8回ループさせる(Yレジスタがカウンタ)
        ; なので,$2000~$27ff($800の領域)を初期化できる
        ; $2000~$23ffが画面1,$2400~$27ffが画面2(画面1の横隣り)の領域
        ldx #0
        ldy #8
CLEAR_VRAM:
        sta $2007
        inx
        bne CLEAR_VRAM
        dey
        bne CLEAR_VRAM

        ;! ここから追加!!!!!!
        lda #>($2000+15+14*$20)         ; 16bitのうち上位8bitだけ使う書き方
        sta $2006
        lda #<($2000+15+14*$20)         ; 下位8bit
        sta $2006
        lda #$41                        ; Aの文字
        sta $2007

        ; パレットテーブル(色を管理する領域)へパレットデータを転送
        ; $3f00から開始
        lda #$3f
        sta $2006
        lda #$00
        sta $2006
        ldx #$00
TRANSFAR_PALETTE:
        lda PALETTES, x                 ; 高級言語的に書くと A = palettes[x]
        sta $2007                       ; $3f00から順($3f00のあとは$3f01,そのあとは$3f02...)にストアされていく(ストア=RAMへの書き込み)
        inx
        cpx #$10
        bne TRANSFAR_PALETTE            ; X = $10までループ

        lda #$00
        sta $2005                       ; カメラX座標
        sta $2005                       ; カメラY座標

        ; スクリーンオン
        ; 各ビットの意味はhttps://hp.vector.co.jp/authors/VA042397/nes/ppu.html参照
        lda #%0000_1000                 ; 3ビット目が1になっている=スプライト用CHRテーブルの設定(NES研究所参照)
        sta $2000
        lda #%0001_1110                 ; 背景・スプライトを表示,画面の左端を表示
        sta $2001


MAINLOOP:
        jmp MAINLOOP

        ; プログラム終了


.rodata                                 ; データ領域

;! パレットデータも追加
PALETTES:
        ; 背景色,色1,色2,色3の順番
        ; このセットを背景は4つ使い分けられる
        .byte   $22, $00, $10, $20      ; セット1(白黒)
        .byte   $22, $06, $16, $26      ; セット2(赤系)
        .byte   $22, $0a, $1a, $2a      ; セット3(緑系)
        .byte   $22, $0c, $1c, $2c      ; セット4(青系)


; キャラクターデータを読み込む
.segment "CHARS"
        .incbin "bg_sprite.chr"         ; バイナリ(bin)データとしてインクルード(inc)する


; プログラムデータの最後に書き込まれる割り込み時のジャンプアドレスのテーブル
; .addr ラベル名で,ラベルのあるプログラムのアドレスを指定できる
.segment "VECINFO"
        .addr $0000                     ; 未指定(NMI割り込み時のジャンプ先)
        .addr RESET                     ; リセットボタンが押されたときのジャンプ先(起動時と同じ動作をする)
        .addr $0000                     ; 未指定(IRQ割り込み時のアドレス)


これで,以下の画像のように表示できましたか?
文字表示
メモリツールを使ったり,プログラムをいじって,文字色や表示位置を変えてみるのも面白いかもしれません. Ctrl+Mでメモリツールが開きます.
メモリツール
View: CPU MemoryをPPU Memoryに変えて,$21CFを見てみると,きちんとA($41)がストアされていることがわかりました.
文字をVRAMから確認
この場所をいじってみたり,ひらがなを表示させてみたり,パレットデータ($3f00~)を変更して,色を変えてみて下さい.
遊んでいるうちに,どうして$2000 + x座標 + y座標*$20 + スクリーン番号(画面0,画面1)*$400という式で座標に対応するメモリアドレスを算出できるのか,理解できると思います.
また,画像処理の仕様などは,
NES研究室 - グラフィックや,
ギコ猫でもわかるファミコンプログラミングの第3-5章,第11章あたりも見てみてください.図を使って解説してくれています.

次回は,コードを見やすくしながら,絵を表示してみます.

前の記事:NES開発について | ホームへ | 次の記事:NESプログラミング1 (2)

© 2025 astsb.net.