Open20

Atari2600開発

balmychanbalmychan

このスクラップについて

Atari2600の開発について書いていくスクラップです

balmychanbalmychan

参考記事

AtariAgeコミュニティ

開発関連のサイト

開発関連の記事

開発ツール

エミュレータ

ハード関連の情報

書籍

その他

balmychanbalmychan

TIAについて1

Atari 2600 Programming for NewbiesのSession_1からSession_12まで読んだ

とにかくグラフィクスに関する勝手がファミコンとは全然違うなぁという感じ。

  • ファミコンだとBGを描画するにはVRAMのパターンテーブルの領域にキャラクタデータを書き込んでおいて、その番号をネームテーブルなりに書き込んでおくと、勝手にBGが敷き詰められたが
  • Atari2600の場合はTIA(Television Interface Adaptor)というグラフィクス用のチップがあって、そのチップに背景色を保持するレジスタ領域があり、それをテレビの走査線のタイミングを考慮しながら書き換えて背景を表示するという感じ。(Session 7: The TV and our Kernel)Atari2600のゲームを見るとファミコンみたいにタイルな感じではないと思ったが、そういうことか。
  • ゲームプログラミングだとティアリングを起こさないためにしばしばVBLANKを意識することはあったが、走査線の細かい位置まで考慮して背景色レジスタを書き換えるなどしなければならないとは、という感じ
  • ファミコンの場合はPPU(PictureProcessingUnit)というグラフィクス用のチップがあり、これがAtari2600とは使い勝手が全然違うなぁという印象。同じ6502のCPUでも、グラフィクス用のチップで作り方が全然変わるなと。

それ以外の要素として面白いのが

  • TIAのレジスタにはストロボタイプのレジスタというのがあって、例えば sta WSYNC は、一見するとAレジスタの値をWSYNCのアドレスに書き込んでいるように見えるが、実際はWSYNC(水平同期)まで停止するというもの。これで水平同期や垂直同期のタイミングまで停止といった動作が実現できる
balmychanbalmychan

TIAについて2

ファミコンだとBGを描画するには

と思ったけど

  • Session 13: Playfield Basicsを見ると、どうもさっきのは単なる背景色に関する話で、ファミコンで言うBGみたいなものは、プレイフィールドという概念がTIAにあって、それを使うような気がする
  • TIAにはスプライトとしてプレイヤー0, 1、ミサイル0, 1、ボールという5つのスプライト機能があって、これとプレイフィールドを組み合わせてゲーム性を構築する感じっぽい(ファミコンと比べてかなり独自のスプライト機能という印象。でも昔のゲーム機はコントローラーも独特で、やれることが限られていたからファミコンみたいな汎用ゲーム機とは違うのだろう)
balmychanbalmychan

TIAについて3

なんとなく分かってきたことは

  • Atari2600にはVRAMがなく、グラフィクスに関するレジスタを設定するのみで、ファミコンのようにあらかじめBG情報やパレット情報をVRAMに書き込んでおくなどはできない
  • HBLANKの間にそのスキャンラインにかかっているオブジェクトのそのラインのビット情報をTIAのプレイフィールドやらプレイヤーのスプライトのレジスタに送っておかないといけない(毎スキャンライン毎にそのスキャンラインで必要な描画情報を渡すという処理が必要)
  • 画面の表現は前述の通り 背景色, プレイフィールド, プレイヤー, ミサイル, ボール のみで、これをHBLANK中(場合によってはスキャンライン中)に書き換えて表現する
balmychanbalmychan

ゾーンというテクニックについて

たった5つのスプライトでどうやって表現するのかと思っていたが、ゾーンで分けてレンダリングを行うという考え方があるらしいことを知った

例えば

こういう感じで画面を6つのゾーンに分けて、各ゾーンのレンダリング処理に入る手前でプレイヤー0, 1のスプライトを書き換えてこのゾーンではこのスプライトを表示し...といった感じでゾーンごとに処理するというもの。

balmychanbalmychan

当たり判定について1

スプライトにはY軸情報がなく、じゃあどうやって当たり判定をするんだ?と思ったら、この記事が参考になった

Collision detection question

どうやら、プレイヤー0, 1やミサイル0, 1をレンダリングしたタイミングで、同じピクセル領域にレンダリングされたら、CXM0PやらCXM1Pやらのコリジョン検出用のレジスタの該当のビットが1になるということだった。
(つまり、ピクセル単位で当たり判定がチェックされる)

そうなると、例えばゾーンごとの処理の最後に当たり判定をチェックなどすれば、複数のスプライトの当たり判定もできそうではある。
(ゾーンを超えた大きさのスプライトを表現する場合はちょっと悩むかも)

ゾーンの考え方は4分木空間分割と同じように、描画領域を分けるという意味もあるが、当たり判定のチェックを分けるという用途にもなるなぁと感じた。

balmychanbalmychan

VSYNCについて

VBLANKやVSYNCを待つときに

    lda #2
    sta VBLANK
    sta VSYNC
    REPEAT 3
        sta WSYNC
    REPEND
    lda #0
    sta VSYNC
    REPEAT 37
        sta WSYNC
    REPEND
    sta VBLANK    

といったコードがあり、VSYNCやVBLANKに値2(正確にはD1に1という意味)を入れて、その後に値0を入れるという処理がある。

以前この記事を見て sta VSYNC でVSYNCまでAtari2600を停止するという意味だと思っていたので、この2を入れたり0を入れたりするのがよくわからない。

STELLA PROGRAMMER'S GUIDE を読んでいると、3.3項に

3.3 Vertical timing
When the electron beam has scanned 262 lines, the TV set must be signaled
to blank the beam and position it at the top of the screen to start a new
frame. This signal is called vertical sync, and the TIA must transmit this
signal for at least 3 scan lines. This is accomplished by writing a “1” in D1
of VSYNC to turn it on, count at least 2 scan lines, then write a “0” to D1 of
VSYNC to turn it off.

3.3 縦 タイミング
電子ビームが262ラインを走査したとき、テレビはビームをブラン
クにし、新しいフレームを開始するために画面上部に位置させる
よう信号を送らなければならない。この信号は垂直同期と呼ばれ、
TIAはこの信号を少なくとも3走査線分送信しなければならない。
これは、VSYNCのD1に "1 "を書き込んでオンにし、少なくとも2走
査線をカウントした後、VSYNCのD1に "0 "を書き込んでオフにす
ることで達成される。

という記載がある。これを見ると、どうも垂直同期信号を受け取るのではなく、送るものだというニュアンスが分かる。

で、そもそもテレビとゲーム機は映像信号を送るだけ(と思っている)で制御しているのだから、どのようにして垂直同期のタイミングを知ることができるのかと調べたりChatGPTに聞いてみたところ

次のような点によって垂直同期タイミングが確立されます:

信号フォーマット: テレビは特定の信号フォーマット(例: NTSC、PAL)を解釈します。これには、垂直同期パルスと水平同期パルスが含まれます。

信号の構造: 映像信号には、画像データの他に垂直同期パルスと水平同期パルスが含まれています。これらのパルスは画面の描画を制御するために使用されます。

信号の規格: 映像信号は標準化されており、各信号の要素は特定の規格に従います。これにより、テレビは信号を解釈し、垂直同期や水平同期などのタイミングを特定することができます。

ゲーム機はこれらの信号を生成し、テレビに送信することで、テレビの画面描画を制御します。そのため、テレビは受信した信号から垂直同期や水平同期のタイミングを把握し、正確な映像を表示します。

とのこと。

そもそもテレビに対する信号に垂直同期パルスや水平同期パルスを送ることができ、これをTIAを通して行っているというのがおそらく正しそう

(テレビ→ゲーム機に同期信号が送られていると勘違いしていたがそうではなく、ゲーム機側から同期信号を送るというのが正しそう)

balmychanbalmychan

ビデオ信号について

そもそもアナログテレビに対する信号の理解が足りていないかと思い調べていたら

アナログ/ディジタル・ビデオ信号の基礎知識

が役にたった。

 ビデオ信号には同期信号が必要
動画像は静止画像の集まりですが,走査により電気信号
に変換すると,どこが画像の始まりか分からなくなります.
そこで画像の始まりには印が付けられます.これはアナロ
グもディジタルも同じです.これを同期信号といいます.
ビデオ信号には画像の始まり(上端)と水平走査(ラインと
もいう)の始まり(左端)におのおの,垂直同期信号と水平
同期信号が挿入されています.図5は正常な画像(a)に対
して,(b)は垂直同期が外れた場合,(c)は水平同期が外
れた場合の画像を示します.

とあり、この点からも、同期信号を送るのは映像を出力したい側(ゲーム機)であることが分かる。
テレビとゲーム機で双方向でなんらかの信号を送り合うことはできないが、ゲーム機側から同期信号を送って、双方ともそれに合わせてタイミングを合わせることで、期待した映像を出力できるようになるのかなと思う。

balmychanbalmychan

VBLANKについて

sta VSYNC は垂直同期信号を送るということで理解したが sta VBLANK がいまいち何をやっているのか判然としない

STELLA PROGRAMMER'S GUIDE だと

To physically turn the beam off during its repositioning time, the TV set
needs 37 scan lines of vertical blanks signal from the TIA. This is
accomplished by writing a “1” in D1 of VBLANK to turn it on, count 37
lines, then write a “0” to D1 of VBLANK to turn it off. The microprocessor
is of course free to execute other software during the vertical timing
commands, VSYNC and VBLANK.

再配置時間中にビームを物理的にオフにするには、TV セットは
TIA から 37 スキャンラインの垂直ブランク信号が必要である。こ
れは、VBLANKのD1に "1 "を書き込んでオンにし、37ラインをカウ
ントした後、VBLANKのD1に "0 "を書き込んでオフにすることで達
成される。もちろんマイクロプロセッサは、垂直タイミング・コマ
ンドであるVSYNCとVBLANKの間、自由に他のソフトウェアを実
行することができる。

とあったり

I don't understand screen synchronisation

のフォーラムの質問だとVBLANKは画面上部にリセットみたいな旨が書かれている。(それはVSYNCではないのか?)

色々情報があるが、現時点の解釈だと sta VBLANK は、TIAに垂直ブランク期間であることを伝え、それによってTIAがテレビに映像信号を送らないようにするという感じもしている(その結果テレビのビームはOFFになる)

つまりやってもやらなくてもどうせ垂直ブランク期間は映像が映されないから、どっちでも良いのか?

balmychanbalmychan

スプライト描画について

HMP0, RESP0, HMOVE 周りがかなり理解するのに苦戦

以下の記事を参考にした

なんとなく得られたのは

  • まずそもそもとして、スプライトを表示するX座標を指定することはできない
  • 表示したいタイミングに走査線が来たら sta RESP0 でスプライトの表示タイマーをリセットするとそこから描画が開始され、以降次の走査線でも同じタイミングで描画される
  • つまり、自分の表示したいタイミングまで(クロック数を調整して)待って開始する
  • これに加えて HMP0 の上位4ビットに-7~8の値をセットすることで若干左右に調整できる
  • HMOVE によって水平位置の調整が反映される(多分)

というところ

balmychanbalmychan

サンプルコードを読んで日本語コメント付けた

単純にスプライトを表示して動かすだけの以下のようなサンプルコードを読んで日本語コメントつけて理解を深めたのでそのコードを張っておく

    processor 6502

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; インクルード文
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    include "vcs.h"
    include "macro.h"

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; RAM領域 RAMは $80 から128バイト使える
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    seg.u Variables
    org $80

JetXPos byte ; 機体のX座標
JetYPos byte ; 機体のY座標
Random byte ; ランダム値
Temp byte ; 一時領域

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 定数
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

JET_HEIGHT = 9 ; 機体の高さ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; プログラムコードを $F000 から開始する
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    seg Code
    org $F000

Reset:
    CLEAN_START ; メモリとレジスタをクリアするマクロ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 変数とTIAレジスタを初期化する
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;s
    
    lda #60
    sta JetXPos ; 機体のX座標を 60 に設定
    lda #60
    sta JetYPos ; 機体のY座標を 60 に設定

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; フレームの開始
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

StartFrame:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;  垂直同期前の計算と処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    lda JetXPos ; Aレジスタに機体のX座標をロード
    ldy #0 ; Yレジスタに0をロード
    jsr SetObjectXPos ; SetObjectXPosサブルーチンを呼び出す(多分AとYレジスタを引数代わりにしてる) jsrはそのアドレスの命令にジャンプすること

    sta WSYNC ; 水平同期を待つ
    sta HMOVE ; 水平位置を反映する

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 垂直同期の開始
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    lda #%00000010
    sta VSYNC ; 垂直同期信号を送る
    REPEAT 3
        sta WSYNC ; 水平同期を3つ待つ(垂直同期信号を送ってから3ライン分待つ必要がある)
    REPEND
    lda #%00000000
    sta VSYNC ; 垂直同期信号を停止

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 垂直ブランク中の処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    lda #%00000010
    sta VBLANK ; 垂直ブランクを開始(多分この間TIAからテレビに信号が送られない)
    REPEAT 37
        sta WSYNC ; 垂直ブランク分の期間(水平同期37回分)を待つ
    REPEND
    lda #%00000000
    sta VBLANK ; 垂直ブランクを終了

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 画面の描画処理(192スキャンライン分)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ldx #192 ; 192を X に入れる
.GameLineLoop:
.AreWeInsideJetSprite: ; 機体を描画するかどうかを判定する処理
    txa ; X を A にコピー
    sec ; キャリーフラグを1にセット(キャリーフラグは計算命令で繰り上がりや繰り下がりが起きたときに立つフラグ)
    sbc JetYPos ; A - 機体のY座標 の計算を行いその結果を A に入れる
    cmp JET_HEIGHT ; Aと機体の高さを比較(A - 機体の高さを計算してそのフラグを残す。もし繰り下がりが起きたらキャリーフラグが1になる)
    bcc .DrawSpriteP0 ; キャリーフラグが0なら繰り下がりが発生しており描画範囲内なので機体を描画する
    lda #0 ; 描画範囲内でない場合はA に 0 をセットして描画されないように調整(JetSprite+0は#%00000000のため結果的に何も描画されない)

    ; 処理の流れの例:
    ;   [ ] X:192, JetYPos:60 の場合は 192-60=132 が A に入りそこから 132-9=123 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
    ;   ...
    ;   [ ] X:70,  JetYPos:60 の場合は 70-60=10   が A に入りそこから 10-9=1    になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
    ;   [ ] X:69,  JetYPos:60 の場合は 69-60=9    が A に入りそこから 9-9=0     になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
    ;   [#] X:68,  JetYPos:60 の場合は 68-60=8    が A に入りそこから 8-9=-1    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:67,  JetYPos:60 の場合は 67-60=7    が A に入りそこから 7-9=-2    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:66,  JetYPos:60 の場合は 66-60=6    が A に入りそこから 6-9=-3    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:65,  JetYPos:60 の場合は 65-60=5    が A に入りそこから 5-9=-4    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:64,  JetYPos:60 の場合は 64-60=4    が A に入りそこから 4-9=-5    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:63,  JetYPos:60 の場合は 63-60=3    が A に入りそこから 3-9=-6    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:62,  JetYPos:60 の場合は 62-60=2    が A に入りそこから 2-9=-7    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:61,  JetYPos:60 の場合は 61-60=1    が A に入りそこから 1-9=-8    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [#] X:60,  JetYPos:60 の場合は 60-60=0    が A に入りそこから 0-9=-9    になり繰り下がりが発生してキャリーフラグが0になり描画される
    ;   [ ] X:59,  JetYPos:60 の場合は 59-60=-1   が A に入りそこから 255-9=246 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
    ;   [ ] X:58,  JetYPos:60 の場合は 58-60=-2   が A に入りそこから 254-9=245 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
    ;   ...
    ;   [X] X:0 ,  JetYPos:60 の場合は 0-60=-60   が A に入りそこから 196-9=187 になり繰り下がりが発生せずキャリーフラグは1のままで描画されない
.DrawSpriteP0:
    tay ; A を Y にコピー(もし描画範囲内なら9~0の値がAに入っている。JetSpriteは上下反転になっているので9のときに上端が描画される)
    lda JetSprite,Y ; JetSpriteのアドレスに Y を足してその値を A にロード
    sta WSYNC ; 水平同期を待つ
    sta GRP0 ; プレイヤー0に A の値をセット
    lda JetColor,Y ; JetColorのアドレスに Y を足してその値を A にロード
    sta COLUP0 ; プレイヤー0の色に A の値をセット

    dex ; X をデクリメント
    bne .GameLineLoop ; 計算結果が 0 でない場合は .GameLineLoop に戻る

    sta WSYNC ; 水平同期を待つ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; オーバースキャン中の処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    lda #%00000010
    sta VBLANK ; 垂直ブランクを開始(多分この間TIAからテレビに信号が送られない)
    REPEAT 30
        sta WSYNC ; オーバースキャン分の期間(水平同期30回分)を待つ
    REPEND
    lda #%00000000
    sta VBLANK ; 垂直ブランクを終了

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ジョイスティックの処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

CheckP0Up:
    lda #%00010000 ; ジョイスティック上のビットを立てた値を A にセット
    bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
    bne CheckP0Down ; 計算結果が 0 でない場合はジョイスティック下の処理にジャンプ
    jsr UpJetYPos ; 機体を上に移動するサブルーチンを呼び出す

CheckP0Down:
    lda #%00100000 ; ジョイスティック下のビットを立てた値を A にセット
    bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
    bne CheckP0Left ; 計算結果が 0 でない場合はジョイスティック左の処理にジャンプ
    jsr DownJetYPos ; 機体を下に移動するサブルーチンを呼び出す

CheckP0Left:
    lda #%01000000 ; ジョイスティック左のビットを立てた値を A にセット
    bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
    bne CheckP0Right ; 計算結果が 0 でない場合はジョイスティック右の処理にジャンプ
    jsr LeftJetXPos ; 機体を左に移動するサブルーチンを呼び出す

CheckP0Right:
    lda #%10000000 ; ジョイスティック右のビットを立てた値を A にセット
    bit SWCHA ; SWCHA とのビット比較演算(AND演算される)
    bne EndInputCheck ; 計算結果が 0 でない場合はジョイスティック処理の終了にジャンプ
    jsr RightJetXPos ; 機体を右に移動するサブルーチンを呼び出す

EndInputCheck: ; ジョイスティック処理の終了

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; フレームの終了処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    jmp StartFrame ; フレーム開始にジャンプ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 対象のX座標の位置をセットする
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A は対象のピクセル単位のX座標
;; Y は対象の種類 (0:player0, 1:player1, 2:missile0, 3:missile1, 4:ball)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

SetObjectXPos subroutine
    sta WSYNC ; 水平同期を待つ
    sec ; キャリーフラグを1にセット(キャリーフラグは計算命令で繰り上がりや繰り下がりが起きたときに立つフラグ)
.Div15Loop
    sbc #15 ; A から 15 を減算してその結果を A にセット
    bcs .Div15Loop ; キャリーフラグが 0 になるまで繰り返す(このループを抜ける時は A を 15 で割った余りが A に入る)
    eor #%0111 ; A と 7(%0111) でXORして A を -8~7 に調整する
    asl ; A を左に4ビットシフト(このあとのHMP0には上位4ビットにセットする必要があるため)
    asl
    asl
    asl
    sta HMP0,Y ; 指定のスプライト(Y の値によってどのスプライトかが変わる)の水平位置のオフセット値をセット(-7~8)
    sta RESP0,Y ; 指定のスプライト(Y の値によってどのスプライトかが変わる)の描画を開始
    rts

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ジョイスティックの操作によって機体の座標を動かすサブルーチン群
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LeftJetXPos subroutine
    ldx JetXPos
    dex
    stx JetXPos 
    rts

RightJetXPos subroutine
    ldx JetXPos
    inx
    stx JetXPos 
    rts
    
UpJetYPos subroutine
    ldx JetYPos
    inx
    stx JetYPos 
    rts

DownJetYPos subroutine
    ldx JetYPos
    dex
    stx JetYPos 
    rts

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; データ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

JetSprite:
    .byte #%00000000         ;
    .byte #%00010100         ;   # #
    .byte #%01111111         ; #######
    .byte #%00111110         ;  #####
    .byte #%00011100         ;   ###
    .byte #%00011100         ;   ###
    .byte #%00001000         ;    #
    .byte #%00001000         ;    #
    .byte #%00001000         ;    #

JetColor:
    .byte #$00
    .byte #$FE
    .byte #$0C
    .byte #$0E
    .byte #$0E
    .byte #$04
    .byte #$BA
    .byte #$0E
    .byte #$08

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 末尾
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $FFFC
    word Reset
    word Reset
balmychanbalmychan

簡単なプレイフィールドの描画

囲う感じでプレイフィールドを描画した

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 画面の描画処理(192スキャンライン分)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ldx #192 ; 192を X に入れる
    
.GameLineLoop:
    sta WSYNC ; 水平同期を待つ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; プレイフィールドの描画処理
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    cpx #176
    bcs .DrawTopBottom16Line
    cpx #16
    bcc .DrawTopBottom16Line

.DrawPlayField
    lda #%00110000
    sta PF0
    lda #%00000000
    sta PF1
    lda #%00000000
    sta PF2
    lda #$80
    sta COLUPF
    lda #%00000001
    sta CTRLPF
    jmp .AreWeInsideJetSprite

.DrawTopBottom16Line
    lda #%11110000
    sta PF0
    lda #%11111111
    sta PF1
    lda #%11111111
    sta PF2
    lda #$80
    sta COLUPF
    lda #%00000000
    sta CTRLPF
balmychanbalmychan

タイマーの使い方

他のサンプルコードを見ているとVBLANKを待つのに TIM64TINTIM というニーモニックが出てくるのでそれについて。

STELLA PROGRAMMER'S GUIDE を見ると、これはタイマー機能で例えば

lda #10
sta TIM64T

とすると INTIM の値がデクリメントされていき、マイナスになったらその時間が過ぎたことを表す

TIM64T以外にもいくつかあり、下記

16進アドレス インターバル ニーモニック
294 1クロック TIM1T
295 8クロック TIM8T
296 64クロック TIM64T
297 1024クロック T1024T

サンプルコードだとVBLANK待ちに37回 sta WSYNC をリピートする代わりにタイマーで

lda #42
sta TIM64T

としており、これは1スキャンラインが76クロックなので

  • タイマー 64x42 = 2688
  • 37スキャンライン = 2812

なので、だいたい合う?のか?(だいたいで良いのかよくわからないが)

balmychanbalmychan

疑似乱数テーブルを使った疑似乱数の生成

乱数カウンタとその値で擬似乱数テーブルから乱数値を引くサブルーチンを書いた

    seg.u Variables
    org $80

RandomCounter byte ; 乱数カウンタ
RandomValue byte ; 乱数値

; ...

NextRandomValue subroutine
    inc RandomCounter ; 乱数カウンタをインクリメント
    ldx RandomCounter ; 乱数カウンタを X にロード
    lda RandomTable,X ; 乱数テーブル + X の値を A にロード
    sta RandomValue ; A をRandomValueにセット
    rts

; ...

; 乱数テーブル
RandomTable:
    .byte $24, $3A, $0D, $C3, $56, $AF, $4E, $97
    .byte $1C, $78, $FA, $D5, $09, $B2, $6E, $8C
    .byte $3F, $40, $B9, $E6, $2D, $51, $A8, $C7
    ; 省略(乱数カウンタが1バイトなので0~255の値を取るので、256バイト分乱数テーブルを用意する

balmychanbalmychan

1つのゾーンでスプライト2つとプレイフィールドを同時に表示するには

※準備中
※スプライト2つとプレイフィールドを表示するには処理時間が足りずに思った表示にはならないのでそのことについて書く予定

balmychanbalmychan

音の出し方

音を出すには3種類のレジスタを操作する必要がある。(同じレジスタが2セットあるので、2つの音を同時に出すことが可能

レジスタ 説明
AUDC0,1 トーン。音の種類(0~15)と思ってOK。フルートのような音だったりビームみたいな音だったり
AUDF0,1 頻度・周波数。音の高さ(0~31)と思ってOK
AUDV0,1 ボリューム。音の大きさ(0~15)

単純に音を出すだけなら以下のようになる

    lda #9 ; 音の種類は9版
    sta AUDC0
    lda #8 ; 音の高さは8
    sta AUDF0
    lda #15 ; 音の大きさは最大の15
    sta AUDV0

この値をデータとしてROMに入れておいて、フレームに合わせて次の音を出すというのをやっていくことになる。