NESプログラミング1 (1)
早速,実際にプログラムを組んでみましょう.(といっても最初は写経ですが・・・)
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という処理しか行っていません. では,次に何か文字を表示してみましょう.
背景に何か表示するためには,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)がストアされていることがわかりました.
この場所をいじってみたり,ひらがなを表示させてみたり,パレットデータ($3f00~)を変更して,色を変えてみて下さい.
遊んでいるうちに,どうして$2000 + x座標 + y座標*$20 + スクリーン番号(画面0,画面1)*$400
という式で座標に対応するメモリアドレスを算出できるのか,理解できると思います.
また,画像処理の仕様などは,
NES研究室 - グラフィックや,
ギコ猫でもわかるファミコンプログラミングの第3-5章,第11章あたりも見てみてください.図を使って解説してくれています.
次回は,コードを見やすくしながら,絵を表示してみます.
© 2025 astsb.net.